Skip to content

[FEATURE]: allow chat.message hook to skip the assistant turn (noReply) #26022

@IYENTeam

Description

@IYENTeam

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.

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