Skip to content

Commit 25254d0

Browse files
authored
fix(sdk): make inferred chat agent types portable for declaration emit (#4218)
## Summary Exporting a `chat.agent` from a project with `declaration: true` failed with TS2742: the inferred type of the agent references `ChatTaskWirePayload`, which was declared in an internal module not reachable through the package exports map, so tsc could only name it via a file path into `node_modules` and refused to emit. Consumers had to hand-mirror the wire type and annotate their export. ## Fix `ChatTaskWirePayload` and `ChatInputChunk` are now declared in `@trigger.dev/sdk/chat` (a public subpath) and re-exported type-only from the internal shared module, so every internal import is unchanged and the browser/server module split is untouched. Declaration emit for an inferred agent type now produces a portable specifier: ```ts export declare const chatAgent: Task<"chat-agent", import("@trigger.dev/sdk/chat").ChatTaskWirePayload<MyUIMessage, MyClientData>, unknown>; ``` As a side effect the wire types are now directly importable, which is what affected users were reconstructing by hand. ## Verification Reproduced against the built 4.5.2-equivalent package: a consumer fixture with declaration emit produced `import("<file path>/ai-shared.js")` in its declaration (the TS2742 trigger); after the fix the same fixture emits the public specifier with zero diagnostics. A regression test now builds that consumer simulation in a temp directory on every test run: it copies the built package into a fake node_modules (copied, not symlinked, because tsc only applies exports-map naming to real node_modules paths), compiles the fixture with the TypeScript API, and asserts no errors, no relative-path imports, and no internal module references in the emit.
1 parent 6b0588b commit 25254d0

4 files changed

Lines changed: 255 additions & 139 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
---
4+
5+
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.

packages/trigger-sdk/src/v3/ai-shared.ts

Lines changed: 6 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -16,150 +16,19 @@
1616
*/
1717

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

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

27-
/**
28-
* The wire payload shape sent by `TriggerChatTransport`.
29-
* Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
30-
*
31-
* Slim wire: at most ONE message per record. The agent runtime
32-
* reconstructs prior history at run boot from a durable S3 snapshot +
33-
* `session.out` replay (or `hydrateMessages` if registered). The wire is
34-
* delta-only — see plan `vivid-humming-bonbon.md`.
35-
*/
36-
export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
37-
/**
38-
* The single message being delivered on this trigger. Set for:
39-
* - `submit-message`: the new user message OR a tool-approval-responded
40-
* assistant message (with `state: "approval-responded"` tool parts).
41-
* - `regenerate-message`: omitted (the agent slices its own history).
42-
* - `preload` / `close` / `action`: omitted.
43-
* - `handover-prepare`: omitted (use `headStartMessages` instead).
44-
*/
45-
message?: TMessage;
46-
/**
47-
* Bespoke escape hatch for `chat.headStart`. The customer's HTTP route
48-
* handler ships full `UIMessage[]` history at the very first turn — before
49-
* any snapshot exists. The route handler isn't subject to the
50-
* `MAX_APPEND_BODY_BYTES` cap on `/in/append` because it goes through the
51-
* customer's own HTTP endpoint. Used ONLY by `trigger: "handover-prepare"`.
52-
* Ignored on every other trigger.
53-
*/
54-
headStartMessages?: TMessage[];
55-
chatId: string;
56-
trigger:
57-
| "submit-message"
58-
| "regenerate-message"
59-
| "preload"
60-
| "close"
61-
| "action"
62-
/**
63-
* The customer's `chat.handover` route handler kicked us off in
64-
* parallel with the first-turn `streamText` running in the warm
65-
* Next.js process. The run sits idle on `session.in` waiting for
66-
* a `kind: "handover"` (continue from tool execution) or
67-
* `kind: "handover-skip"` (handler finished pure-text, exit
68-
* cleanly). See `chat.handover` in `@trigger.dev/sdk/chat-server`.
69-
*/
70-
| "handover-prepare";
71-
messageId?: string;
72-
metadata?: TMetadata;
73-
/** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
74-
action?: unknown;
75-
/** Whether this run is continuing an existing chat whose previous run ended. */
76-
continuation?: boolean;
77-
/** The run ID of the previous run (only set when `continuation` is true). */
78-
previousRunId?: string;
79-
/** Override idle timeout for this run (seconds). Set by transport.preload(). */
80-
idleTimeoutInSeconds?: number;
81-
/**
82-
* The friendlyId of the Session primitive backing this chat. The
83-
* transport opens (or lazy-creates) the session with
84-
* `externalId = chatId` on first message, then sends this friendlyId
85-
* through to the run so the agent can attach to `.in` / `.out`
86-
* without needing to round-trip through the control plane again.
87-
* Optional for backward-compat while the migration is in flight;
88-
* required once the legacy run-scoped stream path is removed.
89-
*/
90-
sessionId?: string;
91-
};
92-
93-
/**
94-
* One chunk on the chat input stream. `kind` discriminates the variants —
95-
* a single ordered stream now carries all the signals the old three-stream
96-
* split did (`chat-messages`, `chat-stop`, plus action messages piggybacked
97-
* on `chat-messages`).
98-
*/
99-
export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = unknown> =
100-
| {
101-
kind: "message";
102-
/**
103-
* Full wire payload for a new user message or regeneration. Mirrors
104-
* what the legacy `chat-messages` input stream carried.
105-
*/
106-
payload: ChatTaskWirePayload<TMessage, TMetadata>;
107-
}
108-
| {
109-
kind: "stop";
110-
/** Optional human-readable reason. Maps to the legacy `chat-stop` record. */
111-
message?: string;
112-
}
113-
| {
114-
/**
115-
* Sent by `chat.headStart` when the customer's first-turn
116-
* `streamText` finishes. The agent run (currently parked in
117-
* `handover-prepare`) wakes, seeds its accumulators with
118-
* `partialAssistantMessage`, and runs the normal turn loop
119-
* (`onChatStart` → `onTurnStart` → … → `onTurnComplete`).
120-
*
121-
* What happens after that depends on `isFinal`:
122-
*
123-
* - `isFinal: false` — step 1 ended with `finishReason:
124-
* "tool-calls"`. The partial carries the assistant's
125-
* tool-call(s) wrapped in AI SDK's tool-approval round. The
126-
* agent's `streamText` runs the approved tools and continues
127-
* from step 2.
128-
* - `isFinal: true` — step 1 ended pure-text (no tool calls).
129-
* The partial carries the final assistant text. The agent
130-
* skips the LLM call entirely (the response is already
131-
* complete on the customer side) and runs `onTurnComplete`
132-
* with the partial as `responseMessage` so persistence and
133-
* any post-turn work fire normally.
134-
*/
135-
kind: "handover";
136-
/** Customer's step-1 response messages (ModelMessage form). */
137-
partialAssistantMessage: ModelMessage[];
138-
/**
139-
* The UI messageId the customer's handler used for its step-1
140-
* assistant message. The agent reuses this so any post-handover
141-
* chunks (tool-output-available, step-2 text, data-* parts
142-
* written by hooks) merge into the SAME assistant message on
143-
* the browser side instead of starting a new one.
144-
*/
145-
messageId?: string;
146-
/**
147-
* Whether the customer's step 1 is the final response. See
148-
* `kind` description above for the two branches.
149-
*/
150-
isFinal: boolean;
151-
}
152-
| {
153-
/**
154-
* Sent by `chat.headStart` only when the customer's handler
155-
* ABORTS before producing a finishReason (e.g., dispatch error,
156-
* stream cancelled before any tokens). The agent run exits
157-
* cleanly without firing turn hooks. Normal pure-text and
158-
* tool-call finishes go through `kind: "handover"` with the
159-
* appropriate `isFinal` flag.
160-
*/
161-
kind: "handover-skip";
162-
};
27+
// Declared in `chat.ts` (a public subpath) so customer declaration emit can
28+
// name them — declaring them here breaks `declaration: true` consumers with
29+
// TS2742, since this module isn't reachable via the package exports map.
30+
import type { ChatTaskWirePayload } from "./chat.js";
31+
export type { ChatTaskWirePayload, ChatInputChunk } from "./chat.js";
16332

16433
/**
16534
* Extracts the client-data (`metadata`) type from a chat task.

packages/trigger-sdk/src/v3/chat.ts

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,13 @@
2323
* ```
2424
*/
2525

26-
import type { ChatTransport, UIMessage, UIMessageChunk, ChatRequestOptions } from "ai";
26+
import type {
27+
ChatTransport,
28+
ModelMessage,
29+
UIMessage,
30+
UIMessageChunk,
31+
ChatRequestOptions,
32+
} from "ai";
2733
import {
2834
controlSubtype,
2935
headerValue,
@@ -37,10 +43,123 @@ function byteLength(body: string): number {
3743
return new TextEncoder().encode(body).byteLength;
3844
}
3945
import { ChatTabCoordinator } from "./chat-tab-coordinator.js";
40-
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
4146
import { slimSubmitMessageForWire } from "./ai-shared.js";
4247

4348
const DEFAULT_BASE_URL = "https://api.trigger.dev";
49+
50+
/**
51+
* The wire payload shape sent by `TriggerChatTransport`.
52+
* Uses `metadata` to match the AI SDK's `ChatRequestOptions` field name.
53+
*
54+
* Slim wire: at most ONE message per record. The agent runtime
55+
* reconstructs prior history at run boot from a durable S3 snapshot +
56+
* `session.out` replay (or `hydrateMessages` if registered).
57+
*
58+
* Declared here (rather than the internal shared module) so `declaration:
59+
* true` consumers can name inferred agent types via this public subpath.
60+
*/
61+
export type ChatTaskWirePayload<TMessage extends UIMessage = UIMessage, TMetadata = unknown> = {
62+
/**
63+
* The single message being delivered on this trigger. Set for:
64+
* - `submit-message`: the new user message OR a tool-approval-responded
65+
* assistant message (with `state: "approval-responded"` tool parts).
66+
* - `regenerate-message`: omitted (the agent slices its own history).
67+
* - `preload` / `close` / `action`: omitted.
68+
* - `handover-prepare`: omitted (use `headStartMessages` instead).
69+
*/
70+
message?: TMessage;
71+
/**
72+
* Bespoke escape hatch for `chat.headStart`. The customer's HTTP route
73+
* handler ships full `UIMessage[]` history at the very first turn — before
74+
* any snapshot exists. The route handler isn't subject to the
75+
* `MAX_APPEND_BODY_BYTES` cap on `/in/append` because it goes through the
76+
* customer's own HTTP endpoint. Used ONLY by `trigger: "handover-prepare"`.
77+
* Ignored on every other trigger.
78+
*/
79+
headStartMessages?: TMessage[];
80+
chatId: string;
81+
trigger:
82+
| "submit-message"
83+
| "regenerate-message"
84+
| "preload"
85+
| "close"
86+
| "action"
87+
/**
88+
* The customer's `chat.handover` route handler kicked us off in
89+
* parallel with the first-turn `streamText` running in the warm
90+
* Next.js process. The run sits idle on `session.in` waiting for
91+
* a `kind: "handover"` (continue from tool execution) or
92+
* `kind: "handover-skip"` (handler finished pure-text, exit
93+
* cleanly). See `chat.handover` in `@trigger.dev/sdk/chat-server`.
94+
*/
95+
| "handover-prepare";
96+
messageId?: string;
97+
metadata?: TMetadata;
98+
/** Custom action payload when `trigger` is `"action"`. Validated against `actionSchema` on the backend. */
99+
action?: unknown;
100+
/** Whether this run is continuing an existing chat whose previous run ended. */
101+
continuation?: boolean;
102+
/** The run ID of the previous run (only set when `continuation` is true). */
103+
previousRunId?: string;
104+
/** Override idle timeout for this run (seconds). Set by transport.preload(). */
105+
idleTimeoutInSeconds?: number;
106+
/**
107+
* The friendlyId of the Session primitive backing this chat. The
108+
* transport opens (or lazy-creates) the session with
109+
* `externalId = chatId` on first message, then sends this friendlyId
110+
* through to the run so the agent can attach to `.in` / `.out`
111+
* without needing to round-trip through the control plane again.
112+
* Optional for backward-compat while the migration is in flight;
113+
* required once the legacy run-scoped stream path is removed.
114+
*/
115+
sessionId?: string;
116+
};
117+
118+
/**
119+
* One chunk on the chat input stream. `kind` discriminates the variants —
120+
* a single ordered stream carries all the signals (`message`, `stop`,
121+
* `handover`, `handover-skip`).
122+
*/
123+
export type ChatInputChunk<TMessage extends UIMessage = UIMessage, TMetadata = unknown> =
124+
| {
125+
kind: "message";
126+
/** Full wire payload for a new user message or regeneration. */
127+
payload: ChatTaskWirePayload<TMessage, TMetadata>;
128+
}
129+
| {
130+
kind: "stop";
131+
/** Optional human-readable reason. */
132+
message?: string;
133+
}
134+
| {
135+
/**
136+
* Sent by `chat.headStart` when the customer's first-turn
137+
* `streamText` finishes. The agent run (parked in
138+
* `handover-prepare`) wakes, seeds its accumulators with
139+
* `partialAssistantMessage`, and runs the normal turn loop.
140+
* `isFinal: false` means step 1 ended in tool-calls (the agent
141+
* executes them and continues); `isFinal: true` means step 1 was
142+
* the complete response (the agent skips the LLM call).
143+
*/
144+
kind: "handover";
145+
/** Customer's step-1 response messages (ModelMessage form). */
146+
partialAssistantMessage: ModelMessage[];
147+
/**
148+
* The UI messageId the customer's handler used for its step-1
149+
* assistant message, reused so post-handover chunks merge into
150+
* the SAME assistant message on the browser side.
151+
*/
152+
messageId?: string;
153+
isFinal: boolean;
154+
}
155+
| {
156+
/**
157+
* Sent by `chat.headStart` only when the customer's handler
158+
* ABORTS before producing a finishReason. The agent run exits
159+
* cleanly without firing turn hooks.
160+
*/
161+
kind: "handover-skip";
162+
};
44163
const DEFAULT_STREAM_TIMEOUT_SECONDS = 120;
45164

46165
/**

0 commit comments

Comments
 (0)