Files
anomalyco_opencode/packages/opencode/specs/v2.md
T
Dax Raad 03b7c2d3fb core: add message shape design options for v2 prompt hooks
Document two approaches for handling synthetic messages in prompt hooks:
- Option 1: Separate PromptMessage type for lightweight prompt surgery
- Option 2: PromptEditor API with append/prepend/insert mutators

This enables plugin developers to inject instructions or context into prompts without manually fabricating message IDs and timestamps. The design supports resumable sessions while keeping the API simple for common prompt manipulation use cases.
2026-04-05 19:12:27 -04:00

2.8 KiB

2.0

What we would change if we could

Keybindings vs. Keymappings

Make it keymappings, closer to neovim. Can be layered like <leader>abc. Commands don't define their binding, but have an id that a key can be mapped to like

{ key: "ctrl+w", cmd: string | function, description }

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.

type User = {
  role: "user"
  time: { created: number }
  request: {
    agent: string
    model: ModelRef
    variant?: string
    format?: OutputFormat
    system?: string
    tools?: Record<string, boolean>
  }
}

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.

type PromptMessage = {
  role: "user" | "assistant"
  parts: PromptPart[]
}

Plugin hook example:

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.

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:

prompt.append({
  role: "user",
  parts: [{ type: "text", text: "Summarize the tool output above and continue." }],
})
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.