summaryrefslogtreecommitdiffhomepage
path: root/notes
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-23 23:04:30 +0900
committerAdam Malczewski <[email protected]>2026-06-23 23:04:30 +0900
commit674853d87d54dba1cd83c4e51fce5411602f4d5d (patch)
tree07455f9753a09a5ca66f8cb885a37ba3c0cb7787 /notes
parent4158e699e3c8ff556684fe2fc7a39ffab040623e (diff)
downloaddispatch-674853d87d54dba1cd83c4e51fce5411602f4d5d.tar.gz
dispatch-674853d87d54dba1cd83c4e51fce5411602f4d5d.zip
feat(system-prompt): template-based system prompt builder extension
New @dispatch/system-prompt extension (standard tier): - Pure parser: [type:name] variables, [if]/[else]/[endif] conditionals, negated [if !...], nested blocks, unmatched-tag pass-through. - Variable resolver (injected adapters): system:time/date/os/hostname, prompt:cwd/model/conversation_id, git:branch/status, file:<path> (dynamic). - Service handle: construct (resolve+persist) + get (cached, cache-safe). - Default template: persona + AGENTS.md if exists + cwd. - 52 tests (parser 29, resolver 12, catalog 3, service 8). transport-contract 0.17.0→0.18.0: SystemPromptTemplateResponse, SetSystemPromptTemplateRequest, SystemPromptVariable, SystemPromptVariablesResponse. Design: notes/system-prompt-design.md (caching constraint, compaction integration, wave plan). 1384 vitest pass.
Diffstat (limited to 'notes')
-rw-r--r--notes/system-prompt-design.md213
1 files changed, 213 insertions, 0 deletions
diff --git a/notes/system-prompt-design.md b/notes/system-prompt-design.md
new file mode 100644
index 0000000..32e13e0
--- /dev/null
+++ b/notes/system-prompt-design.md
@@ -0,0 +1,213 @@
+# System Prompt Builder — design + plan
+
+> Orchestrator-authored plan. Drives the `prompts/<unit>.md` TASK blocks.
+> User-approved: all variable types, `[else]`, negated `[if !...]`, caching, compaction append.
+
+## 1. Goal
+
+A **template-based system prompt builder** that lets the user define a system prompt
+with variable placeholders (`[type:name]`) and conditionals (`[if]`/`[else]`/`[endif]`).
+Variables are resolved at **construction time** (not every turn) to preserve the prompt
+cache.
+
+Hard requirements:
+- Template persisted globally; resolved system prompt persisted **per conversation**.
+- **Cache-safe:** constructed once (first turn), reused on all subsequent turns.
+ Reconstructed only on **compaction** (fresh variable resolution).
+- Compaction appends its instructions to the constructed system prompt.
+- API: GET/PUT template, GET available variables.
+- Frontend: full-page modal (text editor + variable selector buttons).
+
+## 2. Template format
+
+### Variable insertion
+```
+[type:name]
+```
+Resolves the variable at construction time. Unknown type → blank string.
+Non-existent variable (e.g. file not found) → blank string.
+
+### Conditional blocks
+```
+[if type:name]
+ ...content if variable exists...
+[else]
+ ...content if variable does NOT exist...
+[endif]
+```
+Negated condition:
+```
+[if !type:name]
+ ...content if variable does NOT exist...
+[endif]
+```
+- Nested `[if]` blocks: supported.
+- Multi-line content between tags: supported.
+- Unmatched `[if]`/`[endif]`: treated as literal text (not parsed).
+
+### Edge cases
+- Unknown variable type (e.g. `[unknown:foo]`) → blank string.
+- File read failure → variable is "not existing" → blank string; `[if]` → skipped.
+- Empty template → no system prompt sent (current behavior preserved).
+
+## 3. Variable types
+
+| Type:Name | Description | Source |
+|---|---|---|
+| `system:time` | Current time (ISO 8601) | `new Date().toISOString()` |
+| `system:date` | Current date (YYYY-MM-DD) | derived from `new Date()` |
+| `system:os` | Operating system | `process.platform` |
+| `system:hostname` | Machine hostname | `require("node:os").hostname()` |
+| `prompt:cwd` | Conversation's working directory | passed from session-orchestrator |
+| `prompt:model` | Current model name | passed from session-orchestrator |
+| `prompt:conversation_id` | Conversation ID | passed from session-orchestrator |
+| `git:branch` | Current git branch | `git rev-parse --abbrev-ref HEAD` in cwd |
+| `git:status` | Short git status | `git status --short` in cwd |
+| `file:<path>` | File contents (relative to cwd, or absolute if starts `/`) | `fs.readText` |
+
+The `file:` type is **dynamic** — any path is a valid variable name. All others have
+fixed names. The `git:` variables require `spawn` capability; failures (not a git repo,
+git not installed) → variable is "not existing" → blank string.
+
+## 4. Caching constraint (critical)
+
+**The system prompt is constructed ONCE (on the first turn of a conversation) and
+persisted.** Subsequent turns reuse the persisted system prompt — no reconstruction.
+This preserves the prompt cache (the system prompt is part of the cacheable prefix).
+
+**Reconstruction happens only on compaction** (the conversation is being summarized,
+so fresh variable resolution makes sense — files may have changed, cwd may have changed,
+the time is stale).
+
+Flow:
+1. **First turn** (new conversation): session-orchestrator detects `getConversationMeta
+ === null` → calls `systemPromptService.construct(conversationId, cwd, {model})` →
+ resolves all variables → persists the result → returns the string → sets on
+ `providerOpts.systemPrompt`.
+2. **Subsequent turns:** session-orchestrator calls
+ `systemPromptService.get(conversationId)` → returns the persisted string (or null if
+ never constructed) → sets on `providerOpts.systemPrompt` (or omits if null).
+3. **Compaction turn:** session-orchestrator calls
+ `systemPromptService.construct(conversationId, cwd, {model})` → gets fresh resolved
+ system prompt → appends `COMPACTION_SYSTEM_PROMPT` → uses the combined string as the
+ compaction turn's system prompt. The `construct` call also persists the reconstructed
+ system prompt (without compaction instructions) for future turns.
+4. **Post-compaction turns:** session-orchestrator calls
+ `systemPromptService.get(conversationId)` → returns the reconstructed system prompt.
+
+Changing the template (via `PUT /system-prompt`) does NOT affect existing conversations
+until they are compacted. New conversations use the new template on their first turn.
+
+## 5. Default template
+
+When no template has been set, a built-in default is used:
+```
+You are a helpful coding assistant.
+
+[if file:AGENTS.md]
+[file:AGENTS.md]
+[endif]
+
+The current working directory is [prompt:cwd].
+```
+
+## 6. Architecture
+
+### `system-prompt` extension (new, standard tier)
+
+**Manifest:** `id: "system-prompt"`, `capabilities: { fs: true, spawn: true }`,
+`dependsOn: []` (depends only on kernel contracts).
+
+**Pure parser** (`parser.ts`):
+- `parseTemplate(template: string, resolvedVars: Map<string, string | null>): string`
+- Handles `[type:name]` insertion, `[if]`/`[else]`/`[endif]` conditionals, nesting.
+- Pure: input → output, no I/O. Fully testable with zero mocks.
+
+**Variable resolver** (`resolver.ts`):
+- `resolveVariables(cwd: string, context?: { model?: string; conversationId?: string }):
+ Promise<Map<string, string | null>>`
+- Reads files (`fs` capability), runs git commands (`spawn` capability), gets system
+ info. Injected adapters (same pattern as LSP extension).
+- Returns `Map<string, string | null>` — `null` means "not existing" (file not found,
+ git not available, etc.).
+
+**Variable catalog** (`catalog.ts`):
+- `getVariableCatalog(): SystemPromptVariable[]` — the static list of available variables
+ (for `GET /system-prompt/variables`). The `file:` type is marked `dynamic: true`.
+
+**Service handle** (`types.ts`):
+```ts
+export interface SystemPromptService {
+ construct(conversationId: string, cwd: string, context?: {
+ readonly model?: string;
+ }): Promise<string>;
+ get(conversationId: string): Promise<string | null>;
+}
+```
+- `construct`: reads the template from storage, resolves all variables, persists the
+ result under `system-prompt:resolved:<conversationId>`, returns the string.
+- `get`: reads the persisted result (or null).
+
+**Storage:**
+- Template: `host.storage("system-prompt")` key `"template"` (global).
+- Resolved system prompt: `host.storage("system-prompt")` key
+ `"resolved:<conversationId>"` (per conversation).
+
+### Session-orchestrator integration
+
+The session-orchestrator wires `systemPromptService` as an **optional dep** (same pattern
+as `message-queue` / `toolsFilter` — lazily via `host.getService(systemPromptHandle)` in
+`activate`). If the system-prompt extension isn't loaded, no system prompt is sent
+(current behavior preserved).
+
+Changes in `orchestrator.ts`:
+1. **Turn flow (~line 461):** after resolving `effectiveCwd`, determine if this is the
+ first turn (new conversation) or a subsequent turn:
+ - First turn (new conversation, detected via `getConversationMeta === null` already
+ checked earlier for workspace assignment): call
+ `deps.resolveSystemPrompt?.construct(conversationId, effectiveCwd, { model: modelName })`.
+ - Subsequent turns: call `deps.resolveSystemPrompt?.get(conversationId)`.
+ - If the result is non-null, set it on `providerOpts.systemPrompt`.
+2. **Compaction flow (~line 911):** currently sets `systemPrompt: COMPACTION_SYSTEM_PROMPT`.
+ Change to: call `deps.resolveSystemPrompt?.construct(conversationId, cwd, { model })` →
+ get the constructed system prompt → append `"\n\n" + COMPACTION_SYSTEM_PROMPT` → set on
+ `providerOpts.systemPrompt`. If the service is unavailable, fall back to just
+ `COMPACTION_SYSTEM_PROMPT` (current behavior).
+
+### API endpoints (transport-http)
+
+| Method | Path | Description |
+|---|---|---|
+| GET | `/system-prompt` | Returns `{ template: string }` |
+| PUT | `/system-prompt` | Body `{ template: string }` → persists, returns `{ template: string }` |
+| GET | `/system-prompt/variables` | Returns `{ variables: SystemPromptVariable[] }` |
+
+The transport-http extension calls the `systemPromptHandle` service for these (same
+pattern as `lspServiceHandle` for the LSP status endpoint). If the service is unavailable,
+`GET` returns the default template, `PUT` returns 503, `GET /variables` returns the
+static catalog.
+
+## 7. Units & waves
+
+### Wave 0 (orchestrator, contracts)
+- `transport-contract`: add `SystemPromptTemplateResponse`,
+ `SetSystemPromptTemplateRequest`, `SystemPromptVariable`,
+ `SystemPromptVariablesResponse`. Version bump.
+
+### Wave 1
+- `system-prompt` (NEW extension): pure parser + variable resolver + catalog + storage
+ + service handle. Depends only on kernel contracts. Tests: parser (pure, zero mocks),
+ resolver (injected adapters), catalog.
+
+### Wave 2 (parallel — disjoint packages)
+- `session-orchestrator`: wire `systemPromptService` as optional dep; construct on first
+ turn, get on subsequent, construct+append on compaction. + tests.
+- `transport-http`: add `GET/PUT /system-prompt` + `GET /system-prompt/variables` routes.
+ + tests.
+
+### Wave 3
+- `host-bin`: register `system-prompt` in `CORE_EXTENSIONS`.
+- Orchestrator: root tsconfig ref + `bun install`.
+
+### Wave 4
+- Full-graph verify + FE courier handoff.