diff --git a/packages/opencode/specs/v2.md b/packages/opencode/specs/v2.md index 66b4d2dea4..897b98ba6a 100644 --- a/packages/opencode/specs/v2.md +++ b/packages/opencode/specs/v2.md @@ -12,3 +12,88 @@ Make it `keymappings`, closer to neovim. Can be layered like `abc`. Comm _Why_ Currently its keybindings that have an `id` like `message_redo` and then a command can use that or define it's own binding. While some keybindings are just used with `.match` in arbitrary key handlers and there is no info what the key is used for, except the binding id maybe. It also is unknown in which context/scope what binding is active, so a plugin like `which-key` is nearly impossible to get right. + +## Message Shape + +Problem: + +- stored messages need enough data to replay and resume a session later +- prompt hooks often just want to append a synthetic user/assistant message +- today that means faking ids, timestamps, and request metadata + +### Option 1: Two Message Shapes + +Keep `User` / `Assistant` for stored history, but clean them up. + +```ts +type User = { + role: "user" + time: { created: number } + request: { + agent: string + model: ModelRef + variant?: string + format?: OutputFormat + system?: string + tools?: Record + } +} + +type Assistant = { + role: "assistant" + run: { agent: string; model: ModelRef; path: { cwd: string; root: string } } + usage: { cost: number; tokens: Tokens } + result: { finish?: string; error?: Error; structured?: unknown; kind: "reply" | "summary" } +} +``` + +Add a separate transient `PromptMessage` for prompt surgery. + +```ts +type PromptMessage = { + role: "user" | "assistant" + parts: PromptPart[] +} +``` + +Plugin hook example: + +```ts +prompt.push({ + role: "user", + parts: [{ type: "text", text: "Summarize the tool output above and continue." }], +}) +``` + +Tradeoff: prompt hooks get easy lightweight messages, but there are now two message shapes. + +### Option 2: Prompt Mutators + +Keep `User` / `Assistant` as the stored history model. + +Prompt hooks do not build messages directly. The runtime gives them prompt mutators. + +```ts +type PromptEditor = { + append(input: { role: "user" | "assistant"; parts: PromptPart[] }): void + prepend(input: { role: "user" | "assistant"; parts: PromptPart[] }): void + appendTo(target: "last-user" | "last-assistant", parts: PromptPart[]): void + insertAfter(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void + insertBefore(messageID: string, input: { role: "user" | "assistant"; parts: PromptPart[] }): void +} +``` + +Plugin hook examples: + +```ts +prompt.append({ + role: "user", + parts: [{ type: "text", text: "Summarize the tool output above and continue." }], +}) +``` + +```ts +prompt.appendTo("last-user", [{ type: "text", text: BUILD_SWITCH }]) +``` + +Tradeoff: avoids a second full message type and avoids fake ids/timestamps, but moves more magic into the hook API.