@trigger.dev/sdk 4.7.4 → 4.8.0-webhooks.20261020092920

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/commonjs/v3/ai.d.ts +626 -8
  2. package/dist/commonjs/v3/ai.js +2343 -185
  3. package/dist/commonjs/v3/ai.js.map +1 -1
  4. package/dist/commonjs/v3/chat.d.ts +23 -0
  5. package/dist/commonjs/v3/chat.js.map +1 -1
  6. package/dist/commonjs/v3/index.d.ts +2 -0
  7. package/dist/commonjs/v3/index.js +23 -17
  8. package/dist/commonjs/v3/index.js.map +1 -1
  9. package/dist/commonjs/v3/modelFileDownload.d.ts +10 -0
  10. package/dist/commonjs/v3/modelFileDownload.js +164 -0
  11. package/dist/commonjs/v3/modelFileDownload.js.map +1 -0
  12. package/dist/commonjs/v3/query.d.ts +434 -1
  13. package/dist/commonjs/v3/query.js +115 -0
  14. package/dist/commonjs/v3/query.js.map +1 -1
  15. package/dist/commonjs/v3/test/index.d.ts +2 -1
  16. package/dist/commonjs/v3/test/index.js +4 -1
  17. package/dist/commonjs/v3/test/index.js.map +1 -1
  18. package/dist/commonjs/v3/test/mock-channel-connector.d.ts +172 -0
  19. package/dist/commonjs/v3/test/mock-channel-connector.js +173 -0
  20. package/dist/commonjs/v3/test/mock-channel-connector.js.map +1 -0
  21. package/dist/commonjs/v3/test/mock-chat-agent.d.ts +81 -1
  22. package/dist/commonjs/v3/test/mock-chat-agent.js +137 -10
  23. package/dist/commonjs/v3/test/mock-chat-agent.js.map +1 -1
  24. package/dist/commonjs/v3/transcriptStorage.d.ts +44 -1
  25. package/dist/commonjs/v3/transcriptStorage.js +24 -0
  26. package/dist/commonjs/v3/transcriptStorage.js.map +1 -1
  27. package/dist/commonjs/v3/webhookWaiters.d.ts +80 -0
  28. package/dist/commonjs/v3/webhookWaiters.js +156 -0
  29. package/dist/commonjs/v3/webhookWaiters.js.map +1 -0
  30. package/dist/commonjs/v3/webhooks.d.ts +184 -2
  31. package/dist/commonjs/v3/webhooks.js +323 -1
  32. package/dist/commonjs/v3/webhooks.js.map +1 -1
  33. package/dist/commonjs/version.js +1 -1
  34. package/dist/esm/v3/ai.d.ts +626 -8
  35. package/dist/esm/v3/ai.js +2336 -184
  36. package/dist/esm/v3/ai.js.map +1 -1
  37. package/dist/esm/v3/chat.d.ts +23 -0
  38. package/dist/esm/v3/chat.js.map +1 -1
  39. package/dist/esm/v3/index.d.ts +2 -0
  40. package/dist/esm/v3/index.js +1 -0
  41. package/dist/esm/v3/index.js.map +1 -1
  42. package/dist/esm/v3/modelFileDownload.d.ts +10 -0
  43. package/dist/esm/v3/modelFileDownload.js +161 -0
  44. package/dist/esm/v3/modelFileDownload.js.map +1 -0
  45. package/dist/esm/v3/query.d.ts +434 -1
  46. package/dist/esm/v3/query.js +115 -0
  47. package/dist/esm/v3/query.js.map +1 -1
  48. package/dist/esm/v3/test/index.d.ts +2 -1
  49. package/dist/esm/v3/test/index.js +2 -1
  50. package/dist/esm/v3/test/index.js.map +1 -1
  51. package/dist/esm/v3/test/mock-channel-connector.d.ts +172 -0
  52. package/dist/esm/v3/test/mock-channel-connector.js +170 -0
  53. package/dist/esm/v3/test/mock-channel-connector.js.map +1 -0
  54. package/dist/esm/v3/test/mock-chat-agent.d.ts +81 -1
  55. package/dist/esm/v3/test/mock-chat-agent.js +137 -11
  56. package/dist/esm/v3/test/mock-chat-agent.js.map +1 -1
  57. package/dist/esm/v3/transcriptStorage.d.ts +44 -1
  58. package/dist/esm/v3/transcriptStorage.js +24 -0
  59. package/dist/esm/v3/transcriptStorage.js.map +1 -1
  60. package/dist/esm/v3/webhookWaiters.d.ts +80 -0
  61. package/dist/esm/v3/webhookWaiters.js +149 -0
  62. package/dist/esm/v3/webhookWaiters.js.map +1 -0
  63. package/dist/esm/v3/webhooks.d.ts +184 -2
  64. package/dist/esm/v3/webhooks.js +319 -1
  65. package/dist/esm/v3/webhooks.js.map +1 -1
  66. package/dist/esm/version.js +1 -1
  67. package/docs/ai-chat/actions.mdx +6 -1
  68. package/docs/ai-chat/backend.mdx +40 -0
  69. package/docs/ai-chat/patterns/redacting-telemetry.mdx +117 -0
  70. package/docs/ai-chat/pending-messages.mdx +2 -0
  71. package/docs/ai-chat/reference.mdx +53 -2
  72. package/docs/ai-chat/transcript-storage.mdx +1 -1
  73. package/docs/apikeys.mdx +6 -4
  74. package/docs/cli-runs-commands.mdx +2 -2
  75. package/docs/cli-tasks-commands.mdx +95 -0
  76. package/docs/config/config-file.mdx +58 -0
  77. package/docs/config/extensions/prismaExtension.mdx +4 -4
  78. package/docs/deploy-environment-variables.mdx +4 -0
  79. package/docs/deployment/atomic-deployment.mdx +3 -3
  80. package/docs/deployment/overview.mdx +1 -1
  81. package/docs/deployment/preview-branches.mdx +1 -1
  82. package/docs/github-actions.mdx +3 -3
  83. package/docs/limits.mdx +6 -0
  84. package/docs/manual-setup.mdx +1 -1
  85. package/docs/mcp-tools.mdx +99 -3
  86. package/docs/observability/dashboards.mdx +62 -0
  87. package/docs/runs/max-duration.mdx +4 -0
  88. package/docs/self-hosting/env/supervisor.mdx +1 -0
  89. package/docs/webhooks/channels.mdx +132 -0
  90. package/docs/webhooks/connect.mdx +84 -0
  91. package/docs/webhooks/deliveries.mdx +96 -0
  92. package/docs/webhooks/filters.mdx +142 -0
  93. package/docs/webhooks/human-in-the-loop.mdx +155 -0
  94. package/docs/webhooks/linear.mdx +189 -0
  95. package/docs/webhooks/overview.mdx +103 -0
  96. package/docs/webhooks/session-routing.mdx +138 -0
  97. package/docs/webhooks/slack.mdx +368 -0
  98. package/docs/webhooks/sources.mdx +221 -0
  99. package/docs/webhooks/waiters.mdx +194 -0
  100. package/package.json +4 -2
@@ -0,0 +1,172 @@
1
+ import type { ChannelAckCtx, ChannelCancelledToolCall, ChannelDownloadedFile, ChannelConnector, ChannelControl, ChannelInbound, ChannelInteractionCtx, ChannelInteractionResolution, ChannelLifecycleCtx, ChannelLifecycleState, ChannelMessage, ChannelPendingToolCall, ChannelProgressEvent, ChannelReaction, ChannelReactions, ChannelReply, ChannelReplyStreamEnd, ChannelSeedMessage, ChannelSendCtx, ChannelTurnCtx } from "../ai.js";
2
+ /** The default event shape {@link recordingChannelConnector} maps when no `inbound` is given. */
3
+ export type TestChannelEvent = {
4
+ text: string;
5
+ threadId?: string;
6
+ userId?: string;
7
+ };
8
+ /** The posting handle the default `bind` derives: the event's `threadId`. */
9
+ export type TestChannelHandle = {
10
+ threadId: string;
11
+ };
12
+ /** A single `send()` call captured by {@link recordingChannelConnector}. */
13
+ export type RecordedSend<TEvent = unknown> = {
14
+ /** The channel message the connector was asked to post (or edit into place). */
15
+ message: ChannelMessage;
16
+ /** The egress context: `final`, `mode`, `previousRef`, the raw `event`, `deliveryId`. */
17
+ ctx: ChannelSendCtx<TEvent>;
18
+ /** The ref this send resolved to (echoes `previousRef` on an edit, else a fresh id). */
19
+ ref: string;
20
+ };
21
+ /** A reaction applied to the triggering message, captured by {@link recordingChannelConnector}. */
22
+ export type RecordedReaction<TEvent = unknown> = {
23
+ reaction: ChannelReaction;
24
+ event: TEvent;
25
+ };
26
+ /** A `progress()` call, captured by {@link recordingChannelConnector}. */
27
+ type RecordedProgress<TEvent = unknown> = {
28
+ event: ChannelProgressEvent;
29
+ ctx: ChannelTurnCtx<TEvent>;
30
+ };
31
+ /** A `lifecycle()` report, captured by {@link recordingChannelConnector}. */
32
+ export type RecordedLifecycle<TEvent = unknown> = {
33
+ state: ChannelLifecycleState;
34
+ ctx: ChannelLifecycleCtx<TEvent>;
35
+ };
36
+ /** One turn's native reply stream (`stream: true`), captured by {@link recordingChannelConnector}. */
37
+ export type RecordedReplyStream<TEvent = unknown> = {
38
+ ctx: ChannelTurnCtx<TEvent>;
39
+ /** Every text delta, in order. */
40
+ deltas: string[];
41
+ progress: ChannelProgressEvent[];
42
+ /** How the runtime ended the stream; unset while the turn runs. */
43
+ ended?: ChannelReplyStreamEnd;
44
+ /** How many times `end()` was called (the runtime ends each stream once). */
45
+ endCalls: number;
46
+ };
47
+ /** A HITL `cancelInteraction()` call, captured by {@link recordingChannelConnector}. */
48
+ export type RecordedCancel<TEvent = unknown> = {
49
+ cancelled: ChannelCancelledToolCall[];
50
+ ctx: ChannelTurnCtx<TEvent>;
51
+ };
52
+ /** A HITL `finalizeInteraction()` call, captured by {@link recordingChannelConnector}. */
53
+ export type RecordedFinalize<TEvent = unknown> = {
54
+ event: TEvent;
55
+ resolution: ChannelInteractionResolution;
56
+ };
57
+ /**
58
+ * A real {@link ChannelConnector} whose egress hooks record what they were
59
+ * asked to do instead of touching a network, so a test can assert the channel
60
+ * round-trip a `chat.agent` turn produced. Built through the real
61
+ * `chat.channels.custom` factory, so it is a genuinely-shaped connector; only
62
+ * `send` / `ack` / `react` / `finalizeInteraction` are swapped for recorders.
63
+ */
64
+ export type RecordingChannelConnector<TEvent = unknown> = ChannelConnector<TEvent> & {
65
+ /** Every `send()` call in order: the ack, any stream edits, and the final reply. */
66
+ readonly sent: ReadonlyArray<RecordedSend<TEvent>>;
67
+ /** Turn-start placeholder posts ("final" delivery): `final: false`, no `previousRef`. */
68
+ readonly acks: ReadonlyArray<RecordedSend<TEvent>>;
69
+ /** Stream-mode intermediate edits: `mode: "stream"`, `final: false`, with `previousRef`. */
70
+ readonly edits: ReadonlyArray<RecordedSend<TEvent>>;
71
+ /** Every reaction applied to the triggering message (lifecycle + `run().channel`). */
72
+ readonly reactionsApplied: ReadonlyArray<RecordedReaction<TEvent>>;
73
+ /** Every HITL `finalizeInteraction()` call. */
74
+ readonly finalized: ReadonlyArray<RecordedFinalize<TEvent>>;
75
+ /** Every `seed()` call's triggering event, in order. */
76
+ readonly seeded: ReadonlyArray<TEvent>;
77
+ /** Every `progress()` call (tool calls and results the turn streamed), in order. */
78
+ readonly progressed: ReadonlyArray<RecordedProgress<TEvent>>;
79
+ /** Every `lifecycle()` report, in order (`lifecycle: true`). */
80
+ readonly lifecycles: ReadonlyArray<RecordedLifecycle<TEvent>>;
81
+ /** Every native reply stream the runtime opened, one per turn (`stream: true`). */
82
+ readonly streams: ReadonlyArray<RecordedReplyStream<TEvent>>;
83
+ /** Every `cancelInteraction()` call. */
84
+ readonly cancelled: ReadonlyArray<RecordedCancel<TEvent>>;
85
+ /** Every `endInterruptedStream()` call: the ref of a stream a dead worker left open. */
86
+ readonly interrupted: ReadonlyArray<{
87
+ ref: string;
88
+ ctx: ChannelTurnCtx<TEvent>;
89
+ }>;
90
+ /** The ref of the most recent `send()`, i.e. the current edit target. */
91
+ readonly lastRef: string | undefined;
92
+ /** Text of the final reply (`final: true`), or `undefined` if none posted yet. */
93
+ finalText(): string | undefined;
94
+ };
95
+ /** Options for {@link recordingChannelConnector}. */
96
+ export type RecordingChannelConnectorOptions<TEvent = TestChannelEvent> = {
97
+ /** Connector id. Defaults to {@link DEFAULT_TEST_CONNECTOR_ID} so it lines up with `sendChannelEvent`. */
98
+ id?: string;
99
+ /** The endpoint id the connector subscribes to. Server-side routing only; defaults to `"test-channel"`. */
100
+ endpointId?: string;
101
+ /** `"final"` (default: ack then edit-to-answer) or `"stream"` (debounced live edits). */
102
+ delivery?: "final" | "stream";
103
+ /** Session key template. Unused in-run (server-side routing only); defaults to `"{event.threadId}"`. */
104
+ key?: string;
105
+ /** Map the raw event to the turn's message. Defaults to `event.text`, attributed to `event.userId`. */
106
+ inbound?: (event: TEvent) => ChannelInbound;
107
+ /** Map the reply to the posted message. Defaults to `{ text }`; receives the full {@link ChannelReply}. */
108
+ outbound?: (reply: ChannelReply) => ChannelMessage | null;
109
+ /**
110
+ * Derive the session's posting handle from an event, so turns that no channel delivery started
111
+ * (an action turn, a browser message) still egress through this connector with `ctx.handle` and
112
+ * `ctx.bound: true`. Defaults to `{ threadId: event.threadId }`; pass `null` to make the
113
+ * connector unbindable.
114
+ */
115
+ bind?: ((event: TEvent) => unknown | null) | null;
116
+ /** Prior surface messages to seed a new session with (what `seed()` returns). Recorded per call. */
117
+ seed?: (event: TEvent) => Promise<ChannelSeedMessage[]> | ChannelSeedMessage[];
118
+ /**
119
+ * Placeholder posted at turn start ("final" delivery). Defaults to `{ text: "..." }`.
120
+ * Pass `null` (or a function returning `null`) to post no ack, so the final reply
121
+ * arrives as a fresh message instead of an edit.
122
+ */
123
+ ack?: ChannelMessage | null | ((event: TEvent, ctx: ChannelAckCtx) => ChannelMessage | null);
124
+ /** HITL: map a verified callback event to a tool resolution (null => treat as a normal message). */
125
+ onInteraction?: (event: TEvent) => ChannelInteractionResolution | null;
126
+ /** Fetch a file URL the connector owns for the model; null for URLs it does not own. */
127
+ download?: (url: URL, ctx: {
128
+ handle?: unknown;
129
+ }) => Promise<ChannelDownloadedFile | null>;
130
+ /** Map a click on an agent-posted control to an action for `onAction` (null for anything else). */
131
+ toAction?: (event: TEvent) => unknown | null;
132
+ /** Acknowledge a message that arrives while a turn is running. */
133
+ received?: (ctx: ChannelTurnCtx<TEvent>) => Promise<void> | void;
134
+ /** HITL: map the pending tool call(s) to the controls posted in the thread. */
135
+ renderInteraction?: (pending: ChannelPendingToolCall[], ctx: ChannelInteractionCtx<TEvent>) => ChannelMessage | null;
136
+ /** Lifecycle reaction choices (working/done/error). Requires nothing extra; `react` is always recorded. */
137
+ reactions?: ChannelReactions<TEvent>;
138
+ /** Record tool-call progress (`progress()` hook). Off by default so existing turns post nothing extra. */
139
+ progress?: boolean;
140
+ /** Provider helpers merged into `run({ channel })`. */
141
+ surface?: (ctx: ChannelTurnCtx<TEvent>) => Record<string, unknown>;
142
+ /** Map an event to a control (`{ kind: "stop" }`) while a turn runs. */
143
+ onControl?: (event: TEvent) => ChannelControl | null;
144
+ /** Record `lifecycle()` reports. Off by default. */
145
+ lifecycle?: boolean;
146
+ /**
147
+ * Stream replies natively (`delivery: "stream"` with a recording `stream()` hook): each turn's
148
+ * deltas and end outcome land in `streams`, the stream reports `opened("stream_<n>")` on its
149
+ * first delta, and it reports delivered when it saw text. `{ delivered: false }` reports every
150
+ * stream undelivered, like a Slack stream whose fallback post of the rest of the reply failed.
151
+ */
152
+ stream?: boolean | {
153
+ delivered: false;
154
+ };
155
+ };
156
+ /**
157
+ * Create a {@link RecordingChannelConnector} for driving a `chat.agent`
158
+ * channel turn offline. Pair it with `mockChatAgent(...).sendChannelEvent(...)`:
159
+ * list the connector on the agent's `channels`, deliver an event, then assert
160
+ * against `connector.sent` / `.acks` / `.edits` / `.finalText()`.
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const channel = recordingChannelConnector();
165
+ * const agent = chat.agent({ id: "support", channels: [channel], run: ... });
166
+ * const harness = mockChatAgent(agent);
167
+ * await harness.sendChannelEvent({ event: { text: "hi", threadId: "t1" } });
168
+ * expect(channel.finalText()).toBe("hello");
169
+ * ```
170
+ */
171
+ export declare function recordingChannelConnector<TEvent = TestChannelEvent>(options?: RecordingChannelConnectorOptions<TEvent>): RecordingChannelConnector<TEvent>;
172
+ export {};
@@ -0,0 +1,170 @@
1
+ import { chat } from "../ai.js";
2
+ import { DEFAULT_TEST_CONNECTOR_ID } from "./mock-chat-agent.js";
3
+ const DEFAULT_TEST_ENDPOINT_ID = "test-channel";
4
+ /**
5
+ * Create a {@link RecordingChannelConnector} for driving a `chat.agent`
6
+ * channel turn offline. Pair it with `mockChatAgent(...).sendChannelEvent(...)`:
7
+ * list the connector on the agent's `channels`, deliver an event, then assert
8
+ * against `connector.sent` / `.acks` / `.edits` / `.finalText()`.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * const channel = recordingChannelConnector();
13
+ * const agent = chat.agent({ id: "support", channels: [channel], run: ... });
14
+ * const harness = mockChatAgent(agent);
15
+ * await harness.sendChannelEvent({ event: { text: "hi", threadId: "t1" } });
16
+ * expect(channel.finalText()).toBe("hello");
17
+ * ```
18
+ */
19
+ export function recordingChannelConnector(options = {}) {
20
+ const sent = [];
21
+ const reactionsApplied = [];
22
+ const finalized = [];
23
+ const seeded = [];
24
+ const progressed = [];
25
+ const lifecycles = [];
26
+ const streams = [];
27
+ const cancelled = [];
28
+ const interrupted = [];
29
+ let refCounter = 0;
30
+ let lastRef;
31
+ const inbound = options.inbound ??
32
+ ((event) => {
33
+ const e = event;
34
+ return { message: e?.text ?? "", externalUserId: e?.userId };
35
+ });
36
+ const bind = options.bind === null
37
+ ? undefined
38
+ : (options.bind ??
39
+ ((event) => {
40
+ const threadId = event?.threadId;
41
+ return threadId ? { threadId } : null;
42
+ }));
43
+ const ackFn = options.ack === undefined
44
+ ? () => ({ text: "..." })
45
+ : typeof options.ack === "function"
46
+ ? options.ack
47
+ : () => options.ack;
48
+ const send = async (message, ctx) => {
49
+ const ref = ctx.previousRef ?? `ref_${++refCounter}`;
50
+ sent.push({ message, ctx, ref });
51
+ lastRef = ref;
52
+ return { ref };
53
+ };
54
+ const react = async (reaction, ctx) => {
55
+ reactionsApplied.push({ reaction, event: ctx.event });
56
+ };
57
+ const finalizeInteraction = async (event, resolution) => {
58
+ finalized.push({ event, resolution });
59
+ };
60
+ const connector = chat.channels.custom({
61
+ id: options.id ?? DEFAULT_TEST_CONNECTOR_ID,
62
+ endpoint: {
63
+ id: options.endpointId ?? DEFAULT_TEST_ENDPOINT_ID,
64
+ source: "custom",
65
+ },
66
+ key: (options.key ?? "{event.threadId}"),
67
+ inbound: inbound,
68
+ ...(bind ? { bind: bind } : {}),
69
+ ...(options.seed
70
+ ? {
71
+ seed: (async (event) => {
72
+ seeded.push(event);
73
+ return options.seed(event);
74
+ }),
75
+ }
76
+ : {}),
77
+ ack: ackFn,
78
+ ...(options.outbound ? { outbound: options.outbound } : {}),
79
+ send: send,
80
+ react: react,
81
+ finalizeInteraction: finalizeInteraction,
82
+ ...(options.onInteraction ? { onInteraction: options.onInteraction } : {}),
83
+ ...(options.download ? { download: options.download } : {}),
84
+ ...(options.toAction ? { toAction: options.toAction } : {}),
85
+ ...(options.received ? { received: options.received } : {}),
86
+ ...(options.renderInteraction ? { renderInteraction: options.renderInteraction } : {}),
87
+ ...(options.reactions ? { reactions: options.reactions } : {}),
88
+ ...(options.progress
89
+ ? {
90
+ progress: ((event, ctx) => {
91
+ progressed.push({ event, ctx });
92
+ }),
93
+ }
94
+ : {}),
95
+ ...(options.surface ? { surface: options.surface } : {}),
96
+ ...(options.onControl ? { onControl: options.onControl } : {}),
97
+ ...(options.lifecycle
98
+ ? {
99
+ lifecycle: ((state, ctx) => {
100
+ lifecycles.push({ state, ctx });
101
+ }),
102
+ }
103
+ : {}),
104
+ ...(options.stream
105
+ ? {
106
+ stream: ((ctx) => {
107
+ const recorded = {
108
+ ctx,
109
+ deltas: [],
110
+ progress: [],
111
+ endCalls: 0,
112
+ };
113
+ streams.push(recorded);
114
+ return {
115
+ text: (delta) => {
116
+ if (recorded.deltas.length === 0)
117
+ ctx.opened(`stream_${streams.length}`);
118
+ recorded.deltas.push(delta);
119
+ },
120
+ progress: (event) => {
121
+ recorded.progress.push(event);
122
+ },
123
+ end: async (outcome) => {
124
+ recorded.endCalls += 1;
125
+ recorded.ended = outcome;
126
+ const undelivered = typeof options.stream === "object" && options.stream.delivered === false;
127
+ return { delivered: !undelivered && recorded.deltas.join("").length > 0 };
128
+ },
129
+ };
130
+ }),
131
+ }
132
+ : {}),
133
+ endInterruptedStream: ((ref, ctx) => {
134
+ interrupted.push({ ref, ctx });
135
+ }),
136
+ cancelInteraction: ((cancelledCalls, ctx) => {
137
+ cancelled.push({ cancelled: cancelledCalls, ctx });
138
+ }),
139
+ delivery: options.delivery ?? (options.stream ? "stream" : "final"),
140
+ });
141
+ Object.defineProperties(connector, {
142
+ sent: { get: () => sent, enumerable: true },
143
+ reactionsApplied: { get: () => reactionsApplied, enumerable: true },
144
+ finalized: { get: () => finalized, enumerable: true },
145
+ seeded: { get: () => seeded, enumerable: true },
146
+ progressed: { get: () => progressed, enumerable: true },
147
+ lifecycles: { get: () => lifecycles, enumerable: true },
148
+ streams: { get: () => streams, enumerable: true },
149
+ cancelled: { get: () => cancelled, enumerable: true },
150
+ interrupted: { get: () => interrupted, enumerable: true },
151
+ lastRef: { get: () => lastRef, enumerable: true },
152
+ acks: {
153
+ get: () => sent.filter((s) => s.ctx.final === false && s.ctx.previousRef === undefined),
154
+ enumerable: true,
155
+ },
156
+ edits: {
157
+ get: () => sent.filter((s) => s.ctx.mode === "stream" && s.ctx.final === false && s.ctx.previousRef !== undefined),
158
+ enumerable: true,
159
+ },
160
+ });
161
+ connector.finalText = () => {
162
+ for (let i = sent.length - 1; i >= 0; i--) {
163
+ if (sent[i].ctx.final)
164
+ return sent[i].message.text;
165
+ }
166
+ return undefined;
167
+ };
168
+ return connector;
169
+ }
170
+ //# sourceMappingURL=mock-channel-connector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mock-channel-connector.js","sourceRoot":"","sources":["../../../../src/v3/test/mock-channel-connector.ts"],"names":[],"mappings":"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"}
@@ -13,6 +13,19 @@ type ChatAgentHandle = {
13
13
  /**
14
14
  * Options for `mockChatAgent`.
15
15
  */
16
+ /**
17
+ * A channel delivery to seed on the `session.in` tail for the next boot's replay, alongside browser
18
+ * messages, so a test can check that recovery dispatches both kinds in the order they arrived.
19
+ */
20
+ export type SeededChannelDelivery = {
21
+ channelEvent: {
22
+ event: unknown;
23
+ connectorId?: string;
24
+ source?: string;
25
+ headers?: Record<string, string>;
26
+ deliveryId?: string;
27
+ };
28
+ };
16
29
  export type MockChatAgentOptions = {
17
30
  /** The chat session id passed into every wire payload. Defaults to `"test-chat"`. */
18
31
  chatId?: string;
@@ -147,6 +160,20 @@ export type MockChatAgentHarness = {
147
160
  }): Promise<MockChatAgentTurn>;
148
161
  /** Send a custom action and wait for the next turn-complete. */
149
162
  sendAction(action: unknown): Promise<MockChatAgentTurn>;
163
+ /**
164
+ * Deliver a `chat.event` webhook action: the `{ type, event, source, headers, deliveryId, externalDeliveryId }`
165
+ * envelope the hosted ingress appends to `session.in` with `actionSource: "webhook"`, so it
166
+ * bypasses `actionSchema` and reaches `onAction` typed by the listed events. Waits for the
167
+ * turn-complete the action (or the turn it becomes via `chat.turn()`) writes.
168
+ */
169
+ sendWebhookAction(args: {
170
+ type: string;
171
+ event: unknown;
172
+ source?: string;
173
+ headers?: Record<string, string>;
174
+ deliveryId?: string;
175
+ externalDeliveryId?: string;
176
+ }): Promise<MockChatAgentTurn>;
150
177
  /**
151
178
  * Deliver a message mid-turn without waiting for it, the way the browser's
152
179
  * steering path does. With a `pendingMessages` config the agent routes it into
@@ -157,6 +184,52 @@ export type MockChatAgentHarness = {
157
184
  * call this. Awaiting the turn first leaves nothing to steer.
158
185
  */
159
186
  sendPendingMessage(message: UIMessage): Promise<void>;
187
+ /**
188
+ * Deliver a channel event mid-turn without waiting for it, the way a Slack message lands while
189
+ * the agent is answering. With a `pendingMessages` config it can be steered into the running
190
+ * turn; otherwise, or when `shouldInject` declines, it runs as the next turn.
191
+ */
192
+ sendPendingChannelEvent(args: {
193
+ event: unknown;
194
+ connectorId?: string;
195
+ source?: string;
196
+ deliveryId?: string;
197
+ }): Promise<void>;
198
+ /**
199
+ * Deliver a verified channel event (Slack or any `chat.channels.*`
200
+ * connector) and wait for the next turn-complete. Mirrors what the hosted
201
+ * webhook ingress appends to `session.in`: a `submit-message` wire payload
202
+ * carrying `channelEvent` instead of `message`. The run resolves the
203
+ * connector by `connectorId`, maps the event with its `inbound()`, runs the
204
+ * turn, and posts the reply back through the connector's `send()`.
205
+ *
206
+ * `connectorId` defaults to `DEFAULT_TEST_CONNECTOR_ID`, which is also the id
207
+ * `recordingChannelConnector()` uses when you don't set one, so the common
208
+ * single-connector case lines up. Pass it explicitly for a connector with a
209
+ * custom id or for multi-connector agents.
210
+ */
211
+ sendChannelEvent(args: {
212
+ event: unknown;
213
+ connectorId?: string;
214
+ source?: string;
215
+ headers?: Record<string, string>;
216
+ deliveryId?: string;
217
+ externalDeliveryId?: string;
218
+ }): Promise<MockChatAgentTurn>;
219
+ /**
220
+ * Deliver a channel event without waiting for a turn. Mirrors an event that
221
+ * the run may not turn into a turn: a filtered or deduped delivery, or a
222
+ * stale interaction callback that is dropped (matches no pending tool call).
223
+ * Resolves once the run has had a chance to process and possibly drop it.
224
+ */
225
+ deliverChannelEvent(args: {
226
+ event: unknown;
227
+ connectorId?: string;
228
+ source?: string;
229
+ headers?: Record<string, string>;
230
+ deliveryId?: string;
231
+ externalDeliveryId?: string;
232
+ }): Promise<void>;
160
233
  /** Fire a stop signal. Does not wait for the turn — the task keeps running. */
161
234
  sendStop(message?: string): Promise<void>;
162
235
  /**
@@ -219,7 +292,7 @@ export type MockChatAgentHarness = {
219
292
  *
220
293
  * Effective on the next run boot only.
221
294
  */
222
- seedSessionInTail(messages: UIMessage[]): void;
295
+ seedSessionInTail(entries: Array<UIMessage | SeededChannelDelivery>): void;
223
296
  /**
224
297
  * Append a `trigger: "close"` record to `session.in` — what the server does
225
298
  * when a session is closed from outside (dashboard, `sessions.close()`, MCP).
@@ -270,6 +343,13 @@ export type MockChatAgentHarness = {
270
343
  /** Every raw chunk (including control chunks) emitted since the harness was created. */
271
344
  readonly allRawChunks: unknown[];
272
345
  };
346
+ /**
347
+ * Default `connectorId` used by {@link MockChatAgentHarness.sendChannelEvent}
348
+ * when the caller omits it. Matches the default `id` of
349
+ * {@link recordingChannelConnector}, so a single-connector agent needs no
350
+ * explicit id on either side.
351
+ */
352
+ export declare const DEFAULT_TEST_CONNECTOR_ID = "test-channel";
273
353
  /**
274
354
  * Create an offline test harness for a `chat.agent` task.
275
355
  *
@@ -1,9 +1,37 @@
1
1
  import { resourceCatalog, sessionStreams } from "@trigger.dev/core/v3";
2
2
  import { runInMockTaskContext } from "@trigger.dev/core/v3/test";
3
3
  import { __setSessionCloseImplForTests, __setSessionOpenImplForTests, __setSessionStartImplForTests, } from "../sessions.js";
4
- import { __resetChatInputRouterForTests, __setReadChatSnapshotImplForTests, __setReplaySessionInTailImplForTests, __setReplaySessionOutTailImplForTests, __setWriteChatSnapshotImplForTests, } from "../ai.js";
4
+ import { __resetChatInputRouterForTests, __setReadChatSnapshotImplForTests, __waitForChannelEgressForTests, __setReplaySessionInTailImplForTests, __setReplaySessionOutTailImplForTests, __setWriteChatSnapshotImplForTests, } from "../ai.js";
5
5
  import { createTestSessionHandle } from "./test-session-handle.js";
6
6
  import { reduceUIMessageChunks } from "../uiMessageChunks.js";
7
+ /**
8
+ * Default `connectorId` used by {@link MockChatAgentHarness.sendChannelEvent}
9
+ * when the caller omits it. Matches the default `id` of
10
+ * {@link recordingChannelConnector}, so a single-connector agent needs no
11
+ * explicit id on either side.
12
+ */
13
+ export const DEFAULT_TEST_CONNECTOR_ID = "test-channel";
14
+ /**
15
+ * Wait for a channel turn's post-completion egress to land. The run loop
16
+ * writes the `trigger:turn-complete` chunk (which unblocks the harness's
17
+ * turn-complete latch) BEFORE it awaits the connector's final `send()` and
18
+ * its done/error reactions, and settles a per-chat latch once those have
19
+ * finished. Awaiting that latch, rather than draining timers, makes the
20
+ * connector's recorded sends and reactions deterministic for any connector
21
+ * latency. A latch that never settles is a runtime bug, so it fails loudly.
22
+ */
23
+ async function settlePostTurnChannelEgress(chatId) {
24
+ let timer;
25
+ const timeout = new Promise((_, reject) => {
26
+ timer = setTimeout(() => reject(new Error(`channel egress for ${chatId} did not settle within 10s`)), 10_000);
27
+ });
28
+ try {
29
+ await Promise.race([__waitForChannelEgressForTests(chatId), timeout]);
30
+ }
31
+ finally {
32
+ clearTimeout(timer);
33
+ }
34
+ }
7
35
  const CONTROL_CHUNK_TYPES = new Set(["trigger:turn-complete", "trigger:upgrade-required"]);
8
36
  function isControlChunk(chunk) {
9
37
  if (typeof chunk !== "object" || chunk === null)
@@ -88,6 +116,7 @@ export function mockChatAgent(agent, options = {}) {
88
116
  let sendSessionInput;
89
117
  let closeSessionInput;
90
118
  let runSignal;
119
+ let channelDeliveryCounter = 0;
91
120
  // A latch that resolves every time `trigger:turn-complete` appears on the chat stream.
92
121
  // We use a shared pending promise and replace it after each completion.
93
122
  let turnCompleteResolvers = [];
@@ -109,7 +138,7 @@ export function mockChatAgent(agent, options = {}) {
109
138
  let lastWrittenSnapshot;
110
139
  let seededReplayChunks = [];
111
140
  let seededReplayPartial;
112
- let seededSessionInMessages = [];
141
+ let seededSessionInTail = [];
113
142
  const closeCalls = [];
114
143
  let failCloseCalls = 0;
115
144
  __resetChatInputRouterForTests();
@@ -137,11 +166,27 @@ export function mockChatAgent(agent, options = {}) {
137
166
  };
138
167
  });
139
168
  __setReplaySessionInTailImplForTests(async () => {
140
- return seededSessionInMessages.map((message, i) => ({
141
- message,
142
- metadata: clientData,
143
- seqNum: i + 1,
144
- }));
169
+ const messages = [];
170
+ const channelEvents = [];
171
+ seededSessionInTail.forEach((entry, i) => {
172
+ if ("channelEvent" in entry) {
173
+ channelEvents.push({
174
+ channelEvent: {
175
+ connectorId: entry.channelEvent.connectorId ?? DEFAULT_TEST_CONNECTOR_ID,
176
+ event: entry.channelEvent.event,
177
+ source: entry.channelEvent.source ?? "custom",
178
+ headers: entry.channelEvent.headers ?? {},
179
+ deliveryId: entry.channelEvent.deliveryId ?? `dlv_seeded_${i + 1}`,
180
+ },
181
+ metadata: clientData,
182
+ seqNum: i + 1,
183
+ });
184
+ }
185
+ else {
186
+ messages.push({ message: entry, metadata: clientData, seqNum: i + 1 });
187
+ }
188
+ });
189
+ return { messages, channelEvents };
145
190
  });
146
191
  // Install the session open override so `sessions.open(id)` returns a
147
192
  // SessionHandle with an in-memory `.out` that captures writes. The
@@ -344,6 +389,52 @@ export function mockChatAgent(agent, options = {}) {
344
389
  metadata: clientData,
345
390
  });
346
391
  },
392
+ async sendWebhookAction(args) {
393
+ const deliveryId = args.deliveryId ?? `dlv_${++channelDeliveryCounter}`;
394
+ const turn = await sendPayloadAndWait({
395
+ chatId,
396
+ trigger: "action",
397
+ actionSource: "webhook",
398
+ action: {
399
+ type: args.type,
400
+ event: args.event,
401
+ source: args.source ?? "custom",
402
+ headers: args.headers ?? {},
403
+ deliveryId,
404
+ ...(args.externalDeliveryId ? { externalDeliveryId: args.externalDeliveryId } : {}),
405
+ },
406
+ metadata: clientData,
407
+ });
408
+ await settlePostTurnChannelEgress(chatId);
409
+ return turn;
410
+ },
411
+ async sendPendingChannelEvent(args) {
412
+ await harnessReady;
413
+ const seqBefore = sessionStreams.lastSeqNum(chatId, "in") ?? -1;
414
+ const deliveryId = args.deliveryId ?? `dlv_${++channelDeliveryCounter}`;
415
+ await sendSessionInput(sessionId, {
416
+ kind: "message",
417
+ payload: {
418
+ chatId,
419
+ trigger: "submit-message",
420
+ channelEvent: {
421
+ connectorId: args.connectorId ?? DEFAULT_TEST_CONNECTOR_ID,
422
+ event: args.event,
423
+ source: args.source ?? "custom",
424
+ headers: {},
425
+ deliveryId,
426
+ },
427
+ metadata: clientData,
428
+ },
429
+ });
430
+ const deadline = Date.now() + 5_000;
431
+ while ((sessionStreams.lastSeqNum(chatId, "in") ?? -1) <= seqBefore) {
432
+ if (Date.now() > deadline) {
433
+ throw new Error(`sendPendingChannelEvent: ${deliveryId} never landed on session.in`);
434
+ }
435
+ await new Promise((resolve) => setTimeout(resolve, 5));
436
+ }
437
+ },
347
438
  async sendPendingMessage(message) {
348
439
  await harnessReady;
349
440
  const seqBefore = sessionStreams.lastSeqNum(chatId, "in") ?? -1;
@@ -371,6 +462,41 @@ export function mockChatAgent(agent, options = {}) {
371
462
  await new Promise((resolve) => setTimeout(resolve, 5));
372
463
  }
373
464
  },
465
+ async sendChannelEvent(args) {
466
+ const deliveryId = args.deliveryId ?? `dlv_${++channelDeliveryCounter}`;
467
+ const turn = await sendPayloadAndWait({
468
+ chatId,
469
+ trigger: "submit-message",
470
+ channelEvent: {
471
+ connectorId: args.connectorId ?? DEFAULT_TEST_CONNECTOR_ID,
472
+ event: args.event,
473
+ source: args.source ?? "custom",
474
+ headers: args.headers ?? {},
475
+ deliveryId,
476
+ ...(args.externalDeliveryId ? { externalDeliveryId: args.externalDeliveryId } : {}),
477
+ },
478
+ metadata: clientData,
479
+ });
480
+ await settlePostTurnChannelEgress(chatId);
481
+ return turn;
482
+ },
483
+ async deliverChannelEvent(args) {
484
+ const deliveryId = args.deliveryId ?? `dlv_${++channelDeliveryCounter}`;
485
+ await sendPayloadAndWait({
486
+ chatId,
487
+ trigger: "submit-message",
488
+ channelEvent: {
489
+ connectorId: args.connectorId ?? DEFAULT_TEST_CONNECTOR_ID,
490
+ event: args.event,
491
+ source: args.source ?? "custom",
492
+ headers: args.headers ?? {},
493
+ deliveryId,
494
+ ...(args.externalDeliveryId ? { externalDeliveryId: args.externalDeliveryId } : {}),
495
+ },
496
+ metadata: clientData,
497
+ });
498
+ await settlePostTurnChannelEgress(chatId);
499
+ },
374
500
  async sendStop(message) {
375
501
  await harnessReady;
376
502
  await sendSessionInput(sessionId, { kind: "stop", message });
@@ -442,12 +568,12 @@ export function mockChatAgent(agent, options = {}) {
442
568
  seedSessionOutPartial(partial) {
443
569
  seededReplayPartial = partial;
444
570
  },
445
- seedSessionInTail(messages) {
446
- seededSessionInMessages = messages;
571
+ seedSessionInTail(entries) {
572
+ seededSessionInTail = entries;
447
573
  // The seeded tail occupies seqNums 1..n of the channel, so anything sent
448
574
  // live afterwards has to continue above it.
449
- if (messages.length > 0) {
450
- sessionStreams.setLastSeqNum(sessionId, "in", messages.length);
575
+ if (entries.length > 0) {
576
+ sessionStreams.setLastSeqNum(sessionId, "in", entries.length);
451
577
  }
452
578
  },
453
579
  async deliverSessionInAtSeq(message, seqNum) {