feat(coding-agent): add hold condition API for extensions#2138
feat(coding-agent): add hold condition API for extensions#2138tintinweb wants to merge 2 commits intobadlogic:mainfrom
Conversation
|
Sorry, will take a while until I have time to review this. Like the general direction, but it's a bit tricky. |
|
easy, take your time 🤗 ❤️ lmk if I can help evaluating smth PS:
|
Allow extensions to register hold conditions via setHoldCondition() that keep print mode alive after prompts complete. Hold condition callbacks return follow-up messages that trigger additional agent turns, enabling async workflows like MCP approval flows. Errors in callbacks are caught and routed through emitError() to prevent crashes.
…loading Hold conditions registered in activate() were hitting "not initialized" because setHoldCondition was called before bindCore(). Queue them in pendingHoldConditions and flush on bind, matching the registerProvider pattern. Adds unit tests for waitForPendingWork().
3b067a2 to
9e803da
Compare
|
Sooo, I'm afraid I need to close this down. Here are the reasons:
TL;DR: use RPC mode. re: extension-to-extension, i think this is good! |
|
got it and I understand that it's more complex. Currently the "one prompt, wait for answer, print answer, exit" returns before sub-activities resolve which kinda breaks the contract with the user (finishes before the entirety of the prompt is done - when bg agents are used). I don't think CC in print mode returns before subagents finish. I'll keep my hack in for now, document it as know-issue with a fix for headless print-mode and explore how other extensions wait for their sub-activities 🙌 thanks for looking into it! 🤗 |
add
setHoldCondition()to the extension API so extensions can keep headless/print mode alive while async work completes. @tintinweb/pi-subagents integrates this so background agents can finish before print mode exits. Callbacks are awaited in a loop - returned strings become follow-up agent turns, empty array signals completion. Errors are caught per-callback and routed throughemitError().closes #2071
Details
setHoldCondition()API to the extension system, allowing extensions to keep print mode alive after prompts complete for async workflows (e.g. subagent orchestration, MCP approval flows)emitError()to prevent print mode crashesbindCore(), matching theregisterProviderpatternMotivation
Print mode (
pi -p "do something") is fire-and-forget: it sends prompts, waits for the agent to finish, then exits the process. This is a problem for extensions that kick off async work during a prompt — like spawning subagents or waiting for an external MCP approval — because print mode exits before that work completes. There was no mechanism for an extension to say "don't exit yet, I'm still working."Interactive mode doesn't have this problem because the session stays alive between prompts, giving async work time to finish naturally.
Design decisions
Why a loop instead of a single
await?Hold conditions can produce follow-up messages (e.g. "subagent X finished with result Y") that need to be fed back into the agent as new user turns. A single await can't express this back-and-forth. The loop awaits all conditions, delivers any messages, lets the agent process them, then checks again — repeating until all conditions return empty arrays.
Why
string[]instead of a boolean?A boolean would only tell us "keep waiting." Returning strings lets conditions inject context back into the conversation, so the agent can react to results (e.g. summarize subagent output, handle approval decisions). Empty array = "I'm done", non-empty = "here's what happened, keep going."
Why
.catch()per callback instead of a top-level try/catch?A single throwing condition shouldn't kill the entire loop or block other conditions. The per-callback
.catch()isolates failures: the broken condition returns[](gracefully completes) while others continue. Errors route throughemitError(), matching the pattern used by every other extension callback inrunner.ts.Why on
AgentSessioninstead ofExtensionRunner?Hold conditions need access to
agent.followUp()andagent.continue()to deliver messages and trigger turns. These live onAgentSession, not the runner. The runner only handles event dispatch. Putting the loop on the session keeps it close to the agent lifecycle it controls.Why queue hold conditions during extension loading?
Extensions call
setHoldCondition()in theiractivatefunction, which runs beforebindCore()wires up the real session actions. LikeregisterProvider, hold conditions registered pre-bind are queued inpendingHoldConditionsand flushed when the runner binds to the session. This avoids forcing extensions to defer registration to an event handler.Example: Extension registering hold condition
npm run checkpasses no errors./test.shpasses (1 error:FAIL test/context-overflow.test.tsbut this is unrelated )pi -e ../subagents/src/index.ts -p "run 2 subagents wait for 10 seconds"actually waits 10 seconds