Skip to content

docs(plugin): document experimental.chat.{system,messages}.transform and their in-place mutation requirement (Closes #25754) #33025

Description

@ken-jo

Issue for this PR

Closes #25754

Type of change

  • Documentation

What does this PR do?

The two experimental.chat.{system,messages}.transform hooks aren't documented on the Plugins page at all — they're not in the ### Events list, and packages/web/src/content/docs/plugins.mdx never mentions output.system / output.messages. The one mutation gotcha that bites everyone is also undocumented: you have to mutate the array in place (splice/push). Reassigning the whole array (output.system = [...]) is a silent no-op — the runtime keeps the original array reference and never reads the replacement. That's the bug already reported in #25754, whose fix-option #2 explicitly asks to "document the in-place requirement."

What makes this easy to get wrong: the nearby Compaction example sets output.prompt = "..." and that does work (it's a scalar), so a reader naturally assumes output.system = [...] works too. It doesn't.

This PR adds a short subsection under ## Examples documenting both hooks plus a callout about in-place mutation, mirroring the existing ### Compaction hooks example style. Concretely:

import type { Plugin } from "@opencode-ai/plugin"

export const SystemContextPlugin: Plugin = async (ctx) => {
  return {
    "experimental.chat.system.transform": async (input, output) => {
      // Mutate output.system IN PLACE. Insert after the first block so the
      // cached system-prompt prefix stays stable for prompt caching.
      output.system.splice(1, 0, "Extra session context")
      // output.system = [...]  // ⚠️ silently ignored — keeps the original array
    },
  }
}

And the analogous note for experimental.chat.messages.transform: use output.messages.splice(0, output.messages.length, ...next) rather than output.messages = next.

I'd add a :::caution callout: "These hooks hand you an output object whose array property is the live payload. Mutate the array in place (push/splice). Reassigning the whole array is a no-op — see #25754." I'll also cross-reference the related (but separate) guidance in #23660 (prefer merging into the primary system block over pushing extra entries for OpenAI-compatible backends) and the ordering note in #19960.

I hit this building a cross-platform MCP/hook adapter: our generated bridge has to output.system.splice(...) exactly because reassignment is dropped — so I can confirm the contract on a real integration.

How did you verify your code works?

  • Confirmed on current dev: packages/web/src/content/docs/plugins.mdx (389 lines) has zero mentions of experimental.chat.system.transform, messages.transform, splice, in-place, output.system, or output.messages; the hooks aren't even in the ### Events list.
  • Confirmed the type surface in packages/plugin/src/index.ts:282-296: both transform hooks are declared with no JSDoc, while the sibling experimental.session.compacting (:298-308) has a JSDoc block.
  • Verified the in-place requirement matches bug: experimental.chat.messages.transform plugin contract is ambiguous — reassigning output.messages is a silent no-op (requires in-place splice) #25754's repro and the community magic-context plugin, which uses messages.splice(0, messages.length, ...) rather than reassignment.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions