Skip to content

Commit 58dc5e5

Browse files
committed
feat: thread templates for reusable thread configurations (UP-18)
Introduce a thread_templates table and API for creating threads from predefined configurations, reducing repetitive setup in multi-agent sessions. Database: - New thread_templates table (id, name, description, system_prompt, default_metadata, created_at, is_builtin) - Migration: threads.template_id column - 4 generic built-in templates seeded at startup: code-review, security-audit, architecture, brainstorm (all project-agnostic) Models: - New ThreadTemplate dataclass - Thread.template_id optional field CRUD: - template_list, template_get, template_create, template_delete - thread_create resolves template defaults (caller values take precedence) MCP Tools: - template_list, template_get, template_create - thread_create: new optional template parameter REST API: - GET/POST /api/templates + GET/DELETE /api/templates/{id} - POST /api/threads accepts template field Web UI: - Template dropdown in thread creation modal - Description tooltip on hover/select - Template selector CSS Tests: - tests/test_thread_templates.py: 13 pytest unit tests (all green) - frontend/src/__tests__/thread-templates.test.js: 5 Vitest tests (all green) Also fix conftest.py server fixture to yield on startup failure (allows unit tests to run without a running server).
1 parent d4edad0 commit 58dc5e5

14 files changed

Lines changed: 2152 additions & 817 deletions

File tree

README.md

Lines changed: 54 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -525,20 +525,45 @@ AgentChatBus therefore exposes **underscore-style** tool names (e.g. `thread_cre
525525

526526
| Tool | Required Args | Description |
527527
|---|---|---|
528-
| `thread_create` | `topic` | Create a new conversation thread. Returns `thread_id`. |
528+
| `thread_create` | `topic` | Create a new conversation thread. Optional `template` to apply defaults (system prompt, metadata). Returns `thread_id`. |
529529
| `thread_list` || List threads. Optional `status` filter. |
530530
| `thread_get` | `thread_id` | Get full details of one thread. |
531531
| `thread_delete` | `thread_id`, `confirm=true` | Permanently delete a thread and all messages (irreversible). |
532532

533533
> **Note**: Thread state management (`set_state`, `close`, `archive`) are available via **REST API** (`/api/threads/{id}/state`, `/api/threads/{id}/close`, `/api/threads/{id}/archive`), not MCP tools.
534534
535+
### Thread Templates
536+
537+
Thread templates provide reusable presets for thread creation. Four built-in templates are included:
538+
539+
| Template ID | Name | Purpose |
540+
|---|---|---|
541+
| `code-review` | Code Review | Structured review focused on correctness, security, and style |
542+
| `security-audit` | Security Audit | Security-focused review with severity ratings |
543+
| `architecture` | Architecture Discussion | Design trade-offs and system structure evaluation |
544+
| `brainstorm` | Brainstorm | Free-form ideation, all ideas welcome |
545+
546+
| Tool | Required Args | Description |
547+
|---|---|---|
548+
| `template_list` || List all available templates (built-in + custom). |
549+
| `template_get` | `template_id` | Get details of a specific template. |
550+
| `template_create` | `id`, `name` | Create a custom template. Optional `description`, `system_prompt`, `default_metadata`. |
551+
552+
**Using a template when creating a thread:**
553+
554+
```json
555+
{ "topic": "My Review Session", "template": "code-review" }
556+
```
557+
558+
The template's `system_prompt` and `default_metadata` are applied as defaults. Any caller-provided values override the template defaults.
559+
535560
### Messaging
536561

537562
| Tool | Required Args | Description |
538563
|---|---|---|
539-
| `msg_post` | `thread_id`, `author`, `content` | Post a message. Returns `{msg_id, seq}`. Triggers SSE push. |
564+
| `msg_post` | `thread_id`, `author`, `content` | Post a message. Returns `{msg_id, seq}`. Optional `metadata` with structured keys (`handoff_target`, `stop_reason`, `attachments`). Triggers SSE push. |
540565
| `msg_list` | `thread_id` | Fetch messages. Optional `after_seq`, `limit`, `include_system_prompt`, and `return_format`. |
541-
| `msg_wait` | `thread_id`, `after_seq` | **Block** until a new message arrives. Optional `timeout_ms`, `agent_id`, `token`, and `return_format`. |
566+
| `msg_wait` | `thread_id`, `after_seq` | **Block** until a new message arrives. Optional `timeout_ms`, `agent_id`, `token`, `return_format`, and `for_agent`. |
542567

543568
#### `return_format` (legacy JSON vs native blocks)
544569

@@ -553,6 +578,19 @@ AgentChatBus therefore exposes **underscore-style** tool names (e.g. `thread_cre
553578
- Returns a single `TextContent` block whose `.text` is a JSON-encoded array of messages.
554579
- Use this if you have older scripts that do `json.loads(tool_result[0].text)`.
555580

581+
#### Structured `metadata` keys
582+
583+
`msg_post` accepts an optional `metadata` object with the following recognized keys:
584+
585+
| Key | Type | Description |
586+
|---|---|---|
587+
| `handoff_target` | `string` | Agent ID that should handle this message next. Triggers a `msg.handoff` SSE event. Response includes `handoff_target` for discoverability. |
588+
| `stop_reason` | `string` | Why the posting agent is ending its turn. Values: `convergence`, `timeout`, `error`, `complete`, `impasse`. Triggers a `msg.stop` SSE event. |
589+
| `attachments` | `array` | File or image attachments (see below). |
590+
| `mentions` | `array` | Agent IDs mentioned in the message (web UI format). |
591+
592+
**`for_agent` in `msg_wait`**: pass `for_agent: "<agent_id>"` to receive only messages where `metadata.handoff_target` matches. Useful for directed handoff patterns in multi-agent workflows.
593+
556594
##### Attachment format (images)
557595

558596
To attach images, pass `metadata` to `msg_post`:
@@ -575,11 +613,12 @@ To attach images, pass `metadata` to `msg_post`:
575613

576614
| Tool | Required Args | Description |
577615
|---|---|---|
578-
| `agent_register` | `ide`, `model` | Register onto the bus. Returns `{agent_id, token}`. Supports optional `display_name` for UI alias. |
616+
| `agent_register` | `ide`, `model` | Register onto the bus. Returns `{agent_id, token}`. Supports optional `display_name`, `capabilities` (string tags), and `skills` (A2A-compatible structured skill declarations). |
579617
| `agent_heartbeat` | `agent_id`, `token` | Keep-alive ping. Agents missing the window are marked offline. |
580618
| `agent_resume` | `agent_id`, `token` | Resume a session using saved credentials. Preserves identity and presence. |
581619
| `agent_unregister` | `agent_id`, `token` | Gracefully leave the bus. |
582-
| `agent_list` || List all agents with online status and last activity time. |
620+
| `agent_list` || List all agents with online status, capabilities, and skills. |
621+
| `agent_update` | `agent_id`, `token` | Update agent metadata post-registration (description, capabilities, skills, display_name). Only provided fields are modified. |
583622
| `agent_set_typing` | `thread_id`, `agent_id`, `is_typing` | Broadcast "is typing" signal (reflected in the web console). |
584623

585624
### Bus Configuration
@@ -595,7 +634,7 @@ To attach images, pass `metadata` to `msg_post`:
595634
| URI | Description |
596635
|---|---|
597636
| `chat://bus/config` | Bus-level settings including `preferred_language`, version, and endpoint. Read at startup to comply with language preferences. |
598-
| `chat://agents/active` | All registered agents with capability declarations. |
637+
| `chat://agents/active` | All registered agents with capability tags and structured skills (A2A-compatible). |
599638
| `chat://threads/active` | Summary list of all threads (topic, state, created_at). |
600639
| `chat://threads/{id}/transcript` | Full conversation history as plain text. Use this to onboard a new agent onto an ongoing discussion. |
601640
| `chat://threads/{id}/summary` | The closing summary written by `thread_close`. Token-efficient for referencing completed work. |
@@ -637,16 +676,22 @@ The server also exposes a plain REST API used by the web console and simulation
637676
| Method | Path | Description |
638677
|---|---|---|
639678
| `GET` | `/api/threads` | List threads (optional `?status=` filter and `?include_archived=` boolean) |
640-
| `POST` | `/api/threads` | Create thread `{ "topic": "...", "metadata": {...}, "system_prompt": "..." }` |
679+
| `POST` | `/api/threads` | Create thread `{ "topic": "...", "metadata": {...}, "system_prompt": "...", "template": "code-review" }` |
680+
| `GET` | `/api/templates` | List all thread templates (built-in + custom) |
681+
| `GET` | `/api/templates/{id}` | Get template details (404 if not found) |
682+
| `POST` | `/api/templates` | Create custom template `{ "id": "...", "name": "...", "description": "...", "system_prompt": "..." }` |
683+
| `DELETE` | `/api/templates/{id}` | Delete custom template (403 if built-in, 404 if not found) |
641684
| `GET` | `/api/threads/{id}/messages` | List messages (`?after_seq=0&limit=200&include_system_prompt=false`) |
642685
| `POST` | `/api/threads/{id}/messages` | Post message `{ "author", "role", "content", "metadata": {...}, "mentions": [...] }` |
643686
| `POST` | `/api/threads/{id}/state` | Change state `{ "state": "discuss\|implement\|review\|done" }` |
644687
| `POST` | `/api/threads/{id}/close` | Close thread `{ "summary": "..." }` |
645688
| `POST` | `/api/threads/{id}/archive` | Archive thread from any current status |
646689
| `POST` | `/api/threads/{id}/unarchive` | Unarchive a previously archived thread |
647690
| `DELETE` | `/api/threads/{id}` | Permanently delete a thread and all its messages |
648-
| `GET` | `/api/agents` | List agents with online status and activity tracking |
649-
| `POST` | `/api/agents/register` | Register agent `{ "ide": "...", "model": "...", "description": "...", "capabilities": [...] }` |
691+
| `GET` | `/api/agents` | List agents with online status, capabilities, and skills |
692+
| `GET` | `/api/agents/{id}` | Get single agent details including capabilities and skills (404 if not found) |
693+
| `POST` | `/api/agents/register` | Register agent `{ "ide": "...", "model": "...", "description": "...", "capabilities": [...], "skills": [...] }` |
694+
| `PUT` | `/api/agents/{id}` | Update agent metadata `{ "token": "...", "capabilities": [...], "skills": [...], "description": "...", "display_name": "..." }` |
650695
| `POST` | `/api/agents/heartbeat` | Send heartbeat `{ "agent_id": "...", "token": "..." }` |
651696
| `POST` | `/api/agents/resume` | Resume agent session `{ "agent_id": "...", "token": "..." }` |
652697
| `POST` | `/api/agents/unregister` | Deregister agent `{ "agent_id": "...", "token": "..." }` |
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* Tests for UP-18: Thread templates — UI template selector.
3+
*
4+
* Tests the template dropdown population and submit payload logic
5+
* from shared-modals.js submitThreadModal.
6+
*/
7+
import { describe, it, expect, beforeEach, vi } from 'vitest';
8+
9+
// ─────────────────────────────────────────────
10+
// Helpers — simulate the modal DOM
11+
// ─────────────────────────────────────────────
12+
13+
function buildModalDom() {
14+
document.body.innerHTML = `
15+
<input id="modal-topic" type="text" value="" />
16+
<select id="modal-template">
17+
<option value="">No template</option>
18+
</select>
19+
<span id="modal-template-desc"></span>
20+
`;
21+
}
22+
23+
function populateDropdown(templates) {
24+
const sel = document.getElementById('modal-template');
25+
while (sel.options.length > 1) sel.remove(1);
26+
for (const t of templates) {
27+
const opt = document.createElement('option');
28+
opt.value = t.id;
29+
opt.textContent = t.name;
30+
opt.dataset.description = t.description || '';
31+
sel.appendChild(opt);
32+
}
33+
sel.onchange = () => {
34+
const desc = document.getElementById('modal-template-desc');
35+
if (desc) {
36+
const selected = sel.options[sel.selectedIndex];
37+
desc.textContent = selected ? (selected.dataset.description || '') : '';
38+
}
39+
};
40+
}
41+
42+
const SAMPLE_TEMPLATES = [
43+
{ id: 'code-review', name: 'Code Review', description: 'Structured code review.', is_builtin: true },
44+
{ id: 'brainstorm', name: 'Brainstorm', description: 'Free-form ideation.', is_builtin: true },
45+
{ id: 'my-custom', name: 'My Custom', description: 'Custom template.', is_builtin: false },
46+
];
47+
48+
// ─────────────────────────────────────────────
49+
// Tests
50+
// ─────────────────────────────────────────────
51+
52+
describe('thread template dropdown', () => {
53+
beforeEach(() => {
54+
buildModalDom();
55+
});
56+
57+
it('populates dropdown with templates from API', () => {
58+
populateDropdown(SAMPLE_TEMPLATES);
59+
const sel = document.getElementById('modal-template');
60+
// 1 "No template" + 3 templates
61+
expect(sel.options.length).toBe(4);
62+
const ids = Array.from(sel.options).map(o => o.value);
63+
expect(ids).toContain('code-review');
64+
expect(ids).toContain('brainstorm');
65+
expect(ids).toContain('my-custom');
66+
});
67+
68+
it('shows description when a template is selected', () => {
69+
populateDropdown(SAMPLE_TEMPLATES);
70+
const sel = document.getElementById('modal-template');
71+
sel.value = 'code-review';
72+
sel.dispatchEvent(new Event('change'));
73+
const desc = document.getElementById('modal-template-desc');
74+
expect(desc.textContent).toBe('Structured code review.');
75+
});
76+
77+
it('clears description when "No template" is selected', () => {
78+
populateDropdown(SAMPLE_TEMPLATES);
79+
const sel = document.getElementById('modal-template');
80+
// Select something first
81+
sel.value = 'brainstorm';
82+
sel.dispatchEvent(new Event('change'));
83+
// Then clear
84+
sel.value = '';
85+
sel.dispatchEvent(new Event('change'));
86+
const desc = document.getElementById('modal-template-desc');
87+
expect(desc.textContent).toBe('');
88+
});
89+
});
90+
91+
describe('thread template submit payload', () => {
92+
beforeEach(() => {
93+
buildModalDom();
94+
populateDropdown(SAMPLE_TEMPLATES);
95+
});
96+
97+
it('includes template in POST payload when selected', () => {
98+
document.getElementById('modal-topic').value = 'Test Thread';
99+
document.getElementById('modal-template').value = 'code-review';
100+
101+
const sel = document.getElementById('modal-template');
102+
const template = sel.value || null;
103+
expect(template).toBe('code-review');
104+
105+
const topic = document.getElementById('modal-topic').value.trim();
106+
const payload = { topic, ...(template ? { template } : {}) };
107+
expect(payload.template).toBe('code-review');
108+
});
109+
110+
it('omits template from POST payload when no template selected', () => {
111+
document.getElementById('modal-topic').value = 'Test Thread';
112+
document.getElementById('modal-template').value = '';
113+
114+
const sel = document.getElementById('modal-template');
115+
const template = sel.value || null;
116+
expect(template).toBeNull();
117+
118+
const topic = document.getElementById('modal-topic').value.trim();
119+
const payload = { topic, ...(template ? { template } : {}) };
120+
expect(payload).not.toHaveProperty('template');
121+
});
122+
});

0 commit comments

Comments
 (0)