Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/chat-wire-types-portable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@trigger.dev/sdk": patch
---

Fix TS2742 ("inferred type cannot be named") when exporting a `chat.agent` from a project with declaration emit: `ChatTaskWirePayload` and `ChatInputChunk` are now declared in the public `@trigger.dev/sdk/chat` subpath, so inferred agent types emit portable declarations and the wire types are directly importable.
143 changes: 6 additions & 137 deletions packages/trigger-sdk/src/v3/ai-shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,150 +16,19 @@
*/

import type { Task, AnyTask } from "@trigger.dev/core/v3";
import type { InferUITools, ModelMessage, ToolSet, UIDataTypes, UIMessage } from "ai";
import type { InferUITools, ToolSet, UIDataTypes, UIMessage } from "ai";

/**
* Message-part `type` value for the pending-message data part the agent
* injects when a follow-up message arrives mid-turn.
*/
export const PENDING_MESSAGE_INJECTED_TYPE = "data-pending-message-injected" as const;

/**
* The wire payload shape sent by `TriggerChatTransport`.
* Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
*
* Slim wire: at most ONE message per record. The agent runtime
* reconstructs prior history at run boot from a durable S3 snapshot +
* `session.out` replay (or `hydrateMessages` if registered). The wire is
* delta-only — see plan `vivid-humming-bonbon.md`.
*/
export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
/**
* The single message being delivered on this trigger. Set for:
* - `submit-message`: the new user message OR a tool-approval-responded
* assistant message (with `state: "approval-responded"` tool parts).
* - `regenerate-message`: omitted (the agent slices its own history).
* - `preload` / `close` / `action`: omitted.
* - `handover-prepare`: omitted (use `headStartMessages` instead).
*/
message?: TMessage;
/**
* Bespoke escape hatch for `chat.headStart`. The customer's HTTP route
* handler ships full `UIMessage[]` history at the very first turn — before
* any snapshot exists. The route handler isn't subject to the
* `MAX_APPEND_BODY_BYTES` cap on `/in/append` because it goes through the
* customer's own HTTP endpoint. Used ONLY by `trigger: "handover-prepare"`.
* Ignored on every other trigger.
*/
headStartMessages?: TMessage[];
chatId: string;
trigger:
| "submit-message"
| "regenerate-message"
| "preload"
| "close"
| "action"
/**
* The customer's `chat.handover` route handler kicked us off in
* parallel with the first-turn `streamText` running in the warm
* Next.js process. The run sits idle on `session.in` waiting for
* a `kind: "handover"` (continue from tool execution) or
* `kind: "handover-skip"` (handler finished pure-text, exit
* cleanly). See `chat.handover` in `@trigger.dev/sdk/chat-server`.
*/
| "handover-prepare";
messageId?: string;
metadata?: TMetadata;
/** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
action?: unknown;
/** Whether this run is continuing an existing chat whose previous run ended. */
continuation?: boolean;
/** The run ID of the previous run (only set when `continuation` is true). */
previousRunId?: string;
/** Override idle timeout for this run (seconds). Set by transport.preload(). */
idleTimeoutInSeconds?: number;
/**
* The friendlyId of the Session primitive backing this chat. The
* transport opens (or lazy-creates) the session with
* `externalId = chatId` on first message, then sends this friendlyId
* through to the run so the agent can attach to `.in` / `.out`
* without needing to round-trip through the control plane again.
* Optional for backward-compat while the migration is in flight;
* required once the legacy run-scoped stream path is removed.
*/
sessionId?: string;
};

/**
* One chunk on the chat input stream. `kind` discriminates the variants —
* a single ordered stream now carries all the signals the old three-stream
* split did (`chat-messages`, `chat-stop`, plus action messages piggybacked
* on `chat-messages`).
*/
export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = unknown> =
| {
kind: "message";
/**
* Full wire payload for a new user message or regeneration. Mirrors
* what the legacy `chat-messages` input stream carried.
*/
payload: ChatTaskWirePayload<TMessage, TMetadata>;
}
| {
kind: "stop";
/** Optional human-readable reason. Maps to the legacy `chat-stop` record. */
message?: string;
}
| {
/**
* Sent by `chat.headStart` when the customer's first-turn
* `streamText` finishes. The agent run (currently parked in
* `handover-prepare`) wakes, seeds its accumulators with
* `partialAssistantMessage`, and runs the normal turn loop
* (`onChatStart` → `onTurnStart` → … → `onTurnComplete`).
*
* What happens after that depends on `isFinal`:
*
* - `isFinal: false` — step 1 ended with `finishReason:
* "tool-calls"`. The partial carries the assistant's
* tool-call(s) wrapped in AI SDK's tool-approval round. The
* agent's `streamText` runs the approved tools and continues
* from step 2.
* - `isFinal: true` — step 1 ended pure-text (no tool calls).
* The partial carries the final assistant text. The agent
* skips the LLM call entirely (the response is already
* complete on the customer side) and runs `onTurnComplete`
* with the partial as `responseMessage` so persistence and
* any post-turn work fire normally.
*/
kind: "handover";
/** Customer's step-1 response messages (ModelMessage form). */
partialAssistantMessage: ModelMessage[];
/**
* The UI messageId the customer's handler used for its step-1
* assistant message. The agent reuses this so any post-handover
* chunks (tool-output-available, step-2 text, data-* parts
* written by hooks) merge into the SAME assistant message on
* the browser side instead of starting a new one.
*/
messageId?: string;
/**
* Whether the customer's step 1 is the final response. See
* `kind` description above for the two branches.
*/
isFinal: boolean;
}
| {
/**
* Sent by `chat.headStart` only when the customer's handler
* ABORTS before producing a finishReason (e.g., dispatch error,
* stream cancelled before any tokens). The agent run exits
* cleanly without firing turn hooks. Normal pure-text and
* tool-call finishes go through `kind: "handover"` with the
* appropriate `isFinal` flag.
*/
kind: "handover-skip";
};
// Declared in `chat.ts` (a public subpath) so customer declaration emit can
// name them — declaring them here breaks `declaration: true` consumers with
// TS2742, since this module isn't reachable via the package exports map.
import type { ChatTaskWirePayload } from "./chat.js";
export type { ChatTaskWirePayload, ChatInputChunk } from "./chat.js";

/**
* Extracts the client-data (`metadata`) type from a chat task.
Expand Down
123 changes: 121 additions & 2 deletions packages/trigger-sdk/src/v3/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,13 @@
* ```
*/

import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
import type {
ChatTransport,
ModelMessage,
UIMessage,
UIMessageChunk,
ChatRequestOptions,
} from "ai";
import {
controlSubtype,
headerValue,
Expand All @@ -37,10 +43,123 @@ function byteLength(body: string): number {
return new TextEncoder().encode(body).byteLength;
}
import { ChatTabCoordinator } from "./chat-tab-coordinator.js";
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
import { slimSubmitMessageForWire } from "./ai-shared.js";

const DEFAULT_BASE_URL = "https://api.trigger.dev";

/**
* The wire payload shape sent by `TriggerChatTransport`.
* Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
*
* Slim wire: at most ONE message per record. The agent runtime
* reconstructs prior history at run boot from a durable S3 snapshot +
* `session.out` replay (or `hydrateMessages` if registered).
*
* Declared here (rather than the internal shared module) so `declaration:
* true` consumers can name inferred agent types via this public subpath.
*/
export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
/**
* The single message being delivered on this trigger. Set for:
* - `submit-message`: the new user message OR a tool-approval-responded
* assistant message (with `state: "approval-responded"` tool parts).
* - `regenerate-message`: omitted (the agent slices its own history).
* - `preload` / `close` / `action`: omitted.
* - `handover-prepare`: omitted (use `headStartMessages` instead).
*/
message?: TMessage;
/**
* Bespoke escape hatch for `chat.headStart`. The customer's HTTP route
* handler ships full `UIMessage[]` history at the very first turn — before
* any snapshot exists. The route handler isn't subject to the
* `MAX_APPEND_BODY_BYTES` cap on `/in/append` because it goes through the
* customer's own HTTP endpoint. Used ONLY by `trigger: "handover-prepare"`.
* Ignored on every other trigger.
*/
headStartMessages?: TMessage[];
chatId: string;
trigger:
| "submit-message"
| "regenerate-message"
| "preload"
| "close"
| "action"
/**
* The customer's `chat.handover` route handler kicked us off in
* parallel with the first-turn `streamText` running in the warm
* Next.js process. The run sits idle on `session.in` waiting for
* a `kind: "handover"` (continue from tool execution) or
* `kind: "handover-skip"` (handler finished pure-text, exit
* cleanly). See `chat.handover` in `@trigger.dev/sdk/chat-server`.
*/
| "handover-prepare";
messageId?: string;
metadata?: TMetadata;
/** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
action?: unknown;
/** Whether this run is continuing an existing chat whose previous run ended. */
continuation?: boolean;
/** The run ID of the previous run (only set when `continuation` is true). */
previousRunId?: string;
/** Override idle timeout for this run (seconds). Set by transport.preload(). */
idleTimeoutInSeconds?: number;
/**
* The friendlyId of the Session primitive backing this chat. The
* transport opens (or lazy-creates) the session with
* `externalId = chatId` on first message, then sends this friendlyId
* through to the run so the agent can attach to `.in` / `.out`
* without needing to round-trip through the control plane again.
* Optional for backward-compat while the migration is in flight;
* required once the legacy run-scoped stream path is removed.
*/
sessionId?: string;
};

/**
* One chunk on the chat input stream. `kind` discriminates the variants —
* a single ordered stream carries all the signals (`message`, `stop`,
* `handover`, `handover-skip`).
*/
export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = unknown> =
| {
kind: "message";
/** Full wire payload for a new user message or regeneration. */
payload: ChatTaskWirePayload<TMessage, TMetadata>;
}
| {
kind: "stop";
/** Optional human-readable reason. */
message?: string;
}
| {
/**
* Sent by `chat.headStart` when the customer's first-turn
* `streamText` finishes. The agent run (parked in
* `handover-prepare`) wakes, seeds its accumulators with
* `partialAssistantMessage`, and runs the normal turn loop.
* `isFinal: false` means step 1 ended in tool-calls (the agent
* executes them and continues); `isFinal: true` means step 1 was
* the complete response (the agent skips the LLM call).
*/
kind: "handover";
/** Customer's step-1 response messages (ModelMessage form). */
partialAssistantMessage: ModelMessage[];
/**
* The UI messageId the customer's handler used for its step-1
* assistant message, reused so post-handover chunks merge into
* the SAME assistant message on the browser side.
*/
messageId?: string;
isFinal: boolean;
}
| {
/**
* Sent by `chat.headStart` only when the customer's handler
* ABORTS before producing a finishReason. The agent run exits
* cleanly without firing turn hooks.
*/
kind: "handover-skip";
};
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;

/**
Expand Down
Loading
Loading