feat(plugin): add experimental.session.stopping hook - #47300
YASoftwareDev wants to merge 1 commit into
Conversation
Fires right before the agent loop ends because the assistant's last message finished without tool calls. A plugin that sets continue=true and a message gets it appended as a synthetic user message and the loop runs another step. Errored turns and runs at the agent's step limit are never extended.
|
The following comment was made by an LLM, it may be inaccurate: Found a potentially related PR: PR #44712: This appears to be addressing the same feature—a plugin hook that runs before the session loop ends and allows continuation with a message. The current PR (47300) mentions that related issue #16598 implemented the same idea and was closed by age cleanup, so PR #44712 may be that previous attempt or a similar one. |
|
Thanks, bot: #44712 is indeed the same feature and predates this PR. I have added it to the description. This PR is the smaller variant: one trigger at the natural loop exit, one synthetic user message, no re-entry cap beyond the existing step limit, three tests. Happy to close it in favour of #44712 if that is the direction maintainers prefer; either way the hook is what #16626 asks for. |
Issue for this PR
Closes #16626
Type of change
What does this PR do?
Plugins have no hook that runs before a turn ends.
session.idlefires after the loop has already broken, so a plugin that wants to run a check (tests, type check, lint) and send the findings back to the model can only re-prompt afterwards: the report shows up as a visible user message in a new turn, and withopencode runit races process teardown.This adds
experimental.session.stoppingto the pluginHooksinterface. It is triggered inrunLoopright before the loop breaks because the last assistant message finished without tool calls. Input is{ sessionID, messageID }, output is{ continue: boolean; message?: string }. If a plugin setscontinue: trueand amessage, the message is stored as a user message with onesynthetictext part (same shape the compaction auto-continue uses) and the loop runs another step. With no plugin, or when the plugin leaves the output alone, nothing changes.Two guards: the hook is not called for turns that ended with an error, and not once
stephas reached the agent'sstepslimit, so a plugin cannot extend a run past that limit. Beyond that, a plugin that always continues will loop until the limit; plugins keep their own stopping condition, as #16626 proposed.Naming: the issue asks for
session.stopping. I used theexperimental.prefix because the other hooks that touch the loop and the message list carry it (experimental.text.complete,experimental.session.compacting,experimental.compaction.autocontinue). Dropping the prefix is a one-line change if you prefer the bare name.Related: #16598 implemented the same idea and was closed by the age cleanup; #9272 (
session.before.idle) is a similar hook without the continuation message; #47216 (stophook) triggered at theresult === "stop"branch, which only runs for blocked or errored turns, not for a normal finish. This PR triggers at the actual exit check and stays limited to the one hook.Files:
packages/opencode/src/session/prompt.ts(trigger + continuation),packages/plugin/src/index.ts(interface),packages/opencode/test/session/prompt.test.ts(tests),packages/web/src/content/docs/plugins.mdxandpackages/core/src/plugin/skill/customize-opencode.md(docs).How did you verify your code works?
Three tests in
packages/opencode/test/session/prompt.test.ts, using the existing fake LLM server and a real plugin file loaded throughopencode.json(no mocks):synthetictext part) and two assistant messages, the second one parented to the injected messagecontinuewithout a message: one model hit, one user and one assistant messageCommands, run from
packages/opencodeandpackages/plugin:Screenshots / recordings
Not a UI change.
Checklist
Related, found after opening: #44712 (open since 2026-08-24) adds the same hook with a re-entry cap, a sticky veto across listeners and an exit-matrix test suite. This PR is the minimal form of the same idea (one trigger at the natural exit, one synthetic user message, three tests). If maintainers prefer #44712, this one can be closed; the two are not meant to compete.