feat: allow user message when turn is running - #778
sr07asthana wants to merge 13 commits into
Conversation
🦋 Changeset detectedLatest commit: 564171d The changes in this PR will be included in the next version bump. This PR includes changesets to release 4 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 2dcf006. Configure here.
Co-authored-by: Cursor <cursoragent@cursor.com> # Conflicts: # python/trueforge_sdk/.fern/metadata.json
| private deferredTool?: DeferredTool | undefined; | ||
| private convertedTools: ConvertToolsResult | undefined; | ||
| private pendingSandboxCreatedEvents: SandboxCreatedEvent[] = []; | ||
| private pendingPreSendOutputEvents: ToolResponseEvent[] = []; |
There was a problem hiding this comment.
can you explain what this is for
There was a problem hiding this comment.
createTurn only drains send() for context (collectContextAppends ignores output). If we yielded those events from send(), they would never be persisted.
So this:
Copies tool.response items into pendingPreSendOutputEvents.
Yields the append with output: [] so context still lands in the snapshot.
and when we run execute we flush pendingPreSendOutputEvents
There was a problem hiding this comment.
but do we need to persist?
There was a problem hiding this comment.
suppose client already received HITL required actions, on continuing turn we simply close them on BE without yielding and persisting tool closure events, how will client know this was closed synthetically?
we need this so the session event log and UI to show those calls as closed
| } | ||
|
|
||
| public async *send(messages: AgentThreadSendBatch): AsyncGenerator<AgentThreadAppendContext, void, unknown> { | ||
| if (messages.length > 0 && !isUserToolApprovalOrResponseBatch(messages)) { |
There was a problem hiding this comment.
We should have a separate CancelTurn method that does all this ( that we later reuse in cancel API as well)
We shouldn't add such conditional handling directly in orchestrator.
Perhaps we can model it as a "send cancel" event to orchestrator? @chiragjn thoughts here?
There was a problem hiding this comment.
cancel api is separate in our case right? we decided cancel and closing these calls will be two separate processess right?
The previous turn is already frozen. You start a new turn with a real user message while a sub-agent (or approval) was still open in the snapshot.
Then this orchestrator flow runs, in send() of that new turn
We discussed yesterday that cancel itself should not close all pending subagent/pending actions. It should be done when next user message arrives. cc @chiragjn
There was a problem hiding this comment.
I did not get this.
A user message creates a new turn, which should cancel the previous turn. So sending a user message should have the exact same behaviour as say clicking cancel turn button.
This was what we discussed.
The problem here is that we already mark the turn as done. I believe this change will make more sense once we pause the turn instead.
Currently the only scenario we need to handle is that the turn is running, and we send a user message - which interrupts the turn.
There was a problem hiding this comment.
freeze/cancel-button stops the old turn, this send() path stops leftover threads/tools on the next turn.

Summary
Closes #
Changes
How was this tested?
Checklist
pnpm build,pnpm test,pnpm typecheck,pnpm lint:ci, andpnpm format:checkpass locallypackages/trueforge-sdk,python/trueforge_sdk,.github/fern/openapi/openapi.json,docs/openapi.json) — fork PRs omit SDK regen; maintainers regenerate after merge.env.exampleupdated if configuration or behavior changedNote
Medium Risk
Changes core turn validation, tool-call repair, and multi-thread orchestration behavior; incorrect handling could drop sub-agent work or mis-order durable events, though coverage was added for steer/cancel paths.
Overview
Users can send a new user message to start a turn even when tool approval, client-side tool responses, or sub-agent threads are still open, instead of being blocked or forced to wait for an empty resume.
OpenToolCallCloser now takes a
userMessageIncomingflag: on steer, it synthetically closes pending regular, approval, client-side, and sub-agent tool calls with a cancellation message and emits matchingtool.responseevents (queued so they appear after turn setup). Resume-only batches still auto-close dangling regular calls only.AgentThread drops the “no user message while approvals pending” validation when the batch includes a user message; AgentThreadOrchestrator marks non-root threads cancelled on steer, flushes
thread.donewithstatus: cancelled, and no longer rejects user input when sub-agents are running (cancelled sub-agents skip parent tool handoff).The public
ThreadStateunion and generated OpenAPI / TS / Python SDK types addcancelled;TurnHandlemaps internal cancelled completion to harness events accordingly.Reviewed by Cursor Bugbot for commit 564171d. Bugbot is set up for automated code reviews on this repo. Configure here.