Problem
Plugins authoring chat.message hook can mutate the user message and parts, but they cannot signal "I handled this turn out-of-band, please skip the assistant generation". The hook always falls through to the LLM call.
This blocks plugin-only implementations of side-question features (/btw-style commands) where the plugin synthesizes the answer itself and does not want a real assistant turn after that. Today the parent assistant still runs once on the mutated user message, which produces an extra unwanted reply.
Existing pieces that already do most of this
PromptInput.noReply already exists and short-circuits loop():
|
if (input.noReply === true) return message |
experimental.compaction.autocontinue is the same shape: a hook output flag (enabled) read back by the caller to skip work:
|
(yield* plugin.trigger( |
|
"experimental.compaction.autocontinue", |
|
{ |
|
sessionID: input.sessionID, |
|
agent: userMessage.agent, |
|
model: yield* provider.getModel(userMessage.model.providerID, userMessage.model.modelID), |
|
provider: { |
|
source: info.source, |
|
info, |
|
options: info.options, |
|
}, |
|
message: userMessage, |
|
overflow: input.overflow === true, |
|
}, |
|
{ enabled: true }, |
|
)).enabled |
|
) { |
plugin.trigger() already returns the mutated output object, so reading it back from chat.message is a no-op infra-wise:
|
const trigger = Effect.fn("Plugin.trigger")(function* < |
|
Name extends TriggerName, |
|
Input = Parameters<Required<Hooks>[Name]>[0], |
|
Output = Parameters<Required<Hooks>[Name]>[1], |
|
>(name: Name, input: Input, output: Output) { |
|
if (!name) return output |
|
const s = yield* InstanceState.get(state) |
|
for (const hook of s.hooks) { |
|
const fn = hook[name] as any |
|
if (!fn) continue |
|
yield* Effect.promise(async () => fn(input, output)) |
|
} |
|
return output |
|
}) |
Proposed change (minimal)
Add an optional noReply?: boolean to the chat.message hook output, and have createUserMessage/prompt honor it.
// packages/plugin/src/index.ts
"chat.message"?: (
input: { sessionID, agent?, model?, messageID?, variant? },
output: {
message: UserMessage
parts: Part[]
/** If true, skip the assistant turn after this user message is persisted. */
noReply?: boolean
},
) => Promise<void>
// packages/opencode/src/session/prompt.ts createUserMessage
const triggered = yield* plugin.trigger("chat.message", { ... }, { message: info, parts, noReply: false as boolean })
// ...
return { info, parts, noReply: triggered.noReply === true }
// packages/opencode/src/session/prompt.ts prompt()
if (input.noReply === true || message.noReply === true) return message
That is the whole change. No new hook event, no new abort API, reuses PromptInput.noReply semantics that already exist.
Use case
Building a /btw-style side-question slash command in a plugin. The plugin runs the question in an isolated child session, writes the answer back into the parent user message via chat.message, and currently has no way to tell the parent session "we are done, do not run the assistant on this turn".
Related
Happy to send a PR if maintainers are open to this. Wanted to file the issue first per CONTRIBUTING.md.
Problem
Plugins authoring
chat.messagehook can mutate the user message and parts, but they cannot signal "I handled this turn out-of-band, please skip the assistant generation". The hook always falls through to the LLM call.This blocks plugin-only implementations of side-question features (
/btw-style commands) where the plugin synthesizes the answer itself and does not want a real assistant turn after that. Today the parent assistant still runs once on the mutated user message, which produces an extra unwanted reply.Existing pieces that already do most of this
PromptInput.noReplyalready exists and short-circuitsloop():opencode/packages/opencode/src/session/prompt.ts
Line 1387 in aa3c99a
experimental.compaction.autocontinueis the same shape: a hook output flag (enabled) read back by the caller to skip work:opencode/packages/opencode/src/session/compaction.ts
Lines 511 to 527 in aa3c99a
plugin.trigger()already returns the mutated output object, so reading it back fromchat.messageis a no-op infra-wise:opencode/packages/opencode/src/plugin/index.ts
Lines 258 to 271 in aa3c99a
Proposed change (minimal)
Add an optional
noReply?: booleanto thechat.messagehook output, and havecreateUserMessage/prompthonor it.That is the whole change. No new hook event, no new abort API, reuses
PromptInput.noReplysemantics that already exist.Use case
Building a
/btw-style side-question slash command in a plugin. The plugin runs the question in an isolated child session, writes the answer back into the parent user message viachat.message, and currently has no way to tell the parent session "we are done, do not run the assistant on this turn".Related
/btwfeature request (resolved by plugin author tradition; this would unblock cleaner plugin implementations)noReplyrequest forcommand.execute.before(closed for inactivity; same pattern wanted here for a different hook)command.execute.beforehook #18554 cancellation support forcommand.execute.before(open; same underlying gap, sibling hook)noReplyalready existing inPromptInputHappy to send a PR if maintainers are open to this. Wanted to file the issue first per CONTRIBUTING.md.