Skip to content

feat(sdk): chat.agent — durable conversational task runtime (2/5)#3543

Open
ericallam wants to merge 1 commit intofeature/sessions-primitivefrom
feature/chat-agent-runtime
Open

feat(sdk): chat.agent — durable conversational task runtime (2/5)#3543
ericallam wants to merge 1 commit intofeature/sessions-primitivefrom
feature/chat-agent-runtime

Conversation

@ericallam
Copy link
Copy Markdown
Member

Layer 2 of 5 in the chat.agent stack split

Adds chat.agent({...}), a session-aware task definition for AI chat agents.
The runtime sits on top of the Sessions primitive (L1) and handles the
durable conversational task lifecycle.

Targets feature/sessions-primitive — merge after L1

Delta-only wire + history reconstruction

Each /in/append carries at most one message; the agent rebuilds prior
history at run boot from an S3 snapshot + session.out replay tail.
Awaited snapshot writes after every onTurnComplete keep the chain
durable across idle suspends.

Lifecycle hooks

onChatStart, onTurnStart, onTurnComplete, onAction,
onValidateMessages, hydrateMessages (short-circuits snapshot+replay
if the customer owns history).

Read primitives + helpers

  • chat.history.{getPendingToolCalls, getResolvedToolCalls, extractNewToolResults, findMessage, all, getChain} for HITL flows
  • chat.local — per-run typed data with Proxy access + dirty tracking
  • chat.headStart — first-turn TTFC bridge via customer HTTP handler
  • oomMachine — one-shot OOM-retry on a larger machine; cutoff derived
    from latest trigger:turn-complete chunk on session.out
  • Actions are no longer turns — onAction may mutate via chat.history.*
    without bumping the turn counter
  • gen_ai.conversation.id stamped on chat spans + metrics
  • Skills runtime (loadable per-agent and per-task) + agent skills bundling

Includes snapshot + replay integration tests under apps/webapp/test/.
mockChatAgent test harness updated for the new wire.

Stack

  • L1 → main: Sessions primitive
  • L2 → L1 (this PR)
  • L3 → L2: browser chat client + transport
  • L4 → L3: agent-view dashboard
  • L5 → L4: ai-chat reference + MCP tooling

Adds chat.agent({...}), a session-aware task definition for AI chat
agents. The runtime sits on top of the Sessions primitive and handles:

- Delta-only wire: each /in/append carries at most one message; agent
  rebuilds prior history at run boot from an S3 snapshot + session.out
  replay tail. Awaited snapshot writes after every onTurnComplete keep
  the chain durable across idle suspends.
- Lifecycle hooks: onChatStart, onTurnStart, onTurnComplete, onAction,
  onValidateMessages, hydrateMessages (short-circuits snapshot+replay
  if the customer owns history).
- chat.history read primitives for HITL flows (getPendingToolCalls,
  getResolvedToolCalls, extractNewToolResults, findMessage, all/getChain).
- chat.local — per-run typed data with Proxy access + dirty tracking.
- chat.headStart — first-turn TTFC bridge via a customer HTTP handler.
- oomMachine — one-shot OOM-retry on a larger machine; cutoff derived
  from latest trigger:turn-complete chunk on session.out.
- Actions are no longer turns — onAction may mutate via chat.history.*
  and may return a StreamTextResult without bumping the turn counter.
- gen_ai.conversation.id stamped on chat spans + metrics for telemetry.
- Skills runtime (loadable per-agent and per-task) + agent skills bundling.

Snapshot + replay integration tests in apps/webapp/test/.
mockChatAgent test harness updated for the new wire.

Includes the chat-agent, chat-agent-delta-wire-snapshots,
chat-history-read-primitives, chat-head-start, chat-actions-no-turn,
chat-session-attributes, agent-skills, and mock-chat-agent-test-harness
changesets.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 10, 2026

🦋 Changeset detected

Latest commit: dbc0034

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

This PR includes changesets to release 29 packages
Name Type
@trigger.dev/sdk Minor
@trigger.dev/core Minor
@trigger.dev/build Minor
trigger.dev Minor
@trigger.dev/python Minor
@internal/sdk-compat-tests Patch
d3-chat Patch
references-d3-openai-agents Patch
references-nextjs-realtime Patch
references-realtime-hooks-test Patch
references-realtime-streams Patch
references-telemetry Patch
@trigger.dev/redis-worker Minor
@trigger.dev/schema-to-json Minor
@internal/cache Patch
@internal/clickhouse Patch
@internal/llm-model-catalog Patch
@internal/redis Patch
@internal/replication Patch
@internal/run-engine Patch
@internal/schedule-engine Patch
@internal/testcontainers Patch
@internal/tracing Patch
@internal/tsql Patch
@internal/zod-worker Patch
@trigger.dev/react-hooks Minor
@trigger.dev/rsc Minor
@trigger.dev/database Minor
@trigger.dev/otlp-importer Minor

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

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 10, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2d8d4804-b484-4078-beed-828f3ae7b159

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/chat-agent-runtime

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 4 potential issues.

View 5 additional findings in Devin Review.

Open in Devin Review

*/
handoverResponse(result: StreamTextResult<any, any>): Response;
/** Manually dispatch the `handover` signal on `session.in`. */
handover(args: { partialAssistantMessage: ModelMessage[] }): Promise<void>;
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.

🔴 Public HeadStartSession.handover type omits required isFinal parameter, causing agent to always enter non-final path

The public HeadStartSession type declares handover(args: { partialAssistantMessage: ModelMessage[] }) at packages/trigger-sdk/src/v3/chat-server.ts:133, but the internal function it maps to (chat-server.ts:382-394) requires isFinal: boolean. When a customer calls handle.handover({ partialAssistantMessage: msgs }) through the chat.openSession() escape-hatch API, args.isFinal is undefined. In JSON.stringify at line 393, isFinal: undefined is omitted from the wire payload. The agent receiving this kind: "handover" chunk interprets the missing isFinal as falsy, so it always enters the non-final branch (runs streamText to execute tool-calls) — even when the customer intended a final handover (pure-text, no LLM call). There is no way for a customer using the manual handover() method to signal a final response.

Suggested change
handover(args: { partialAssistantMessage: ModelMessage[] }): Promise<void>;
handover(args: { partialAssistantMessage: ModelMessage[]; isFinal?: boolean; messageId?: string }): Promise<void>;
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +602 to +604
void handoverWhenDone(result)
.finally(() => clearTimeout(idleTimer))
.catch(() => {});
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.

🟡 Idle timer leak when handoverWhenDone is called outside handoverResponse

In openHandoverSession, an idleTimer is created at chat-server.ts:321 that aborts the session's AbortController after the timeout. The clearTimeout(idleTimer) call only exists inside handoverResponse at line 603 (chained on handoverWhenDone's .finally()). When a customer uses the lower-level chat.openSession() API and calls handle.tee() + handle.handoverWhenDone() directly — without going through handle.handoverResponse() — the timer is never cleared. After idleTimeoutInSeconds (default 60s), the timer fires and calls abortController.abort(), which may cancel in-progress operations or SSE subscriptions that the customer still needs.

Prompt for agents
The idleTimer created at line 321 is only cleared inside handoverResponse (line 603). The HeadStartSession handle exposes handoverWhenDone as a standalone public method (line 640), but if a customer calls it directly via the chat.openSession() escape hatch, the timer never gets cleared. Move the clearTimeout(idleTimer) into the handoverWhenDone function itself (e.g. in its finally block), so the timer is always cleared regardless of which code path invokes handoverWhenDone.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +664 to 676
async append(value, options) {
// Use a single-write writer so objects are serialized the same way
// as stream.writer() — the raw append API sends BodyInit which
// doesn't serialize objects correctly for SSE consumers.
const { waitUntilComplete } = writer(opts.id, {
...options,
spanName: "streams.append()",
execute: ({ write }) => {
write(value);
},
});
await waitUntilComplete();
},
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.

🚩 streams.define().append() changed from raw append to writer-based serialization

The define<TPart>().append() method at streams.ts:664-676 was changed from delegating to the raw append() function (which sends BodyInit) to using writer() with a single write() call. The comment explains this ensures objects are serialized the same way as stream.writer() — the raw append API sends BodyInit which doesn't serialize objects correctly for SSE consumers. This is a behavioral change to the RealtimeDefinedStream.append() method. Any existing code that relied on the old raw-append behavior (e.g., passing pre-stringified data) will now get double-serialized through the writer path. The change is intentional and an improvement, but callers should be aware of the semantic shift.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

"peerDependencies": {
"zod": "^3.0.0 || ^4.0.0",
"ai": "^4.2.0 || ^5.0.0 || ^6.0.0"
"ai": "^5.0.0 || ^6.0.0",
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.

🚩 peerDependency range for 'ai' package narrowed — drops v4 support

In packages/trigger-sdk/package.json, the ai peer dependency range changed from ^4.2.0 || ^5.0.0 || ^6.0.0 to ^5.0.0 || ^6.0.0, dropping v4 support. The changeset mentions this is intentional (new features require AI SDK v5+). This is a breaking change for any existing users on [email protected] — they'll see peer dependency warnings on install. The devDependency was also bumped from ^6.0.0 to ^6.0.116.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

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.

1 participant