Skip to content

feat: allow user message when turn is running - #778

Open
sr07asthana wants to merge 13 commits into
mainfrom
sr-AGE-2264
Open

sr07asthana wants to merge 13 commits into
mainfrom
sr-AGE-2264

Conversation

@sr07asthana

@sr07asthana sr07asthana commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #

Changes

How was this tested?

Checklist

  • I have read the contributing guidelines
  • pnpm build, pnpm test, pnpm typecheck, pnpm lint:ci, and pnpm format:check pass locally
  • Tests added/updated where it makes sense
  • No hand-edits to generated code (packages/trueforge-sdk, python/trueforge_sdk, .github/fern/openapi/openapi.json, docs/openapi.json) — fork PRs omit SDK regen; maintainers regenerate after merge
  • Docs / .env.example updated if configuration or behavior changed

Note

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 userMessageIncoming flag: on steer, it synthetically closes pending regular, approval, client-side, and sub-agent tool calls with a cancellation message and emits matching tool.response events (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.done with status: cancelled, and no longer rejects user input when sub-agents are running (cancelled sub-agents skip parent tool handoff).

The public ThreadState union and generated OpenAPI / TS / Python SDK types add cancelled; TurnHandle maps 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.

@changeset-bot

changeset-bot Bot commented Sep 17, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 564171d

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@truefoundry/trueforge-sdk Patch
@truefoundry/trueforge-core Patch
@truefoundry/trueforge-ui Patch
@truefoundry/trueforge Patch

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

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 2 potential issues.

Fix All in Cursor

❌ 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.

Comment thread .changeset/steer-session-anytime.md
Comment thread packages/trueforge-core/src/core/runtime/AgentThreadOrchestrator.ts
Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	python/trueforge_sdk/.fern/metadata.json
Comment thread .github/fern/openapi/openapi.json
Comment thread packages/trueforge-core/src/core/runtime/AgentThread.ts
Comment thread packages/trueforge-core/src/core/runtime/AgentThread.ts Outdated
private deferredTool?: DeferredTool | undefined;
private convertedTools: ConvertToolsResult | undefined;
private pendingSandboxCreatedEvents: SandboxCreatedEvent[] = [];
private pendingPreSendOutputEvents: ToolResponseEvent[] = [];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you explain what this is for

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but do we need to persist?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Comment thread packages/trueforge-core/src/core/runtime/AgentThreadOrchestrator.ts Outdated
Comment thread packages/trueforge-core/src/core/runtime/AgentThreadOrchestrator.ts Outdated
Comment thread packages/trueforge-core/src/core/runtime/AgentThreadOrchestrator.ts Outdated
}

public async *send(messages: AgentThreadSendBatch): AsyncGenerator<AgentThreadAppendContext, void, unknown> {
if (messages.length > 0 && !isUserToolApprovalOrResponseBatch(messages)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

@heerambavi1998 heerambavi1998 Sep 17, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

freeze/cancel-button stops the old turn, this send() path stops leftover threads/tools on the next turn.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants