Skip to content

Commit df96c66

Browse files
feat(sdk): add triggerConfig support to chat.headStart()
Handover-prepare session runs now accept the same tags, queue, and session trigger options as chat.createStartSessionAction(), with chat:{chatId} auto-prepended to tags. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent de8231c commit df96c66

3 files changed

Lines changed: 106 additions & 10 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+
Add `triggerConfig` support to `chat.headStart()` so handover-prepare runs inherit tags, queue, and other session trigger options like `chat.createStartSessionAction()`.

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

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,67 @@ describe("chat.headStart (route handler)", () => {
216216
expect(body.triggerConfig.basePayload.idleTimeoutInSeconds).toBe(60);
217217
});
218218

219+
it("merges triggerConfig tags and queue into createSession", async () => {
220+
const requests: CapturedRequest[] = [];
221+
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {
222+
const urlStr = typeof url === "string" ? url : url.toString();
223+
requests.push({ url: urlStr, init });
224+
if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) {
225+
return createSessionResponse("chat-1");
226+
}
227+
if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) {
228+
return appendOkResponse();
229+
}
230+
if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) {
231+
return new Response(new ReadableStream({ start(c) { c.close(); } }), {
232+
status: 200,
233+
headers: { "content-type": "text/event-stream" },
234+
});
235+
}
236+
throw new Error(`Unexpected URL: ${urlStr}`);
237+
});
238+
239+
const handler = chat.headStart({
240+
agentId: "test-agent",
241+
triggerConfig: {
242+
tags: ["org:acme", "agentic-run:xyz"],
243+
queue: "my-queue",
244+
},
245+
run: async ({ chat: chatHelper }) => {
246+
return streamText({
247+
...chatHelper.toStreamTextOptions(),
248+
model: new MockLanguageModelV3({
249+
doStream: async () => ({ stream: textStream("hi back") }),
250+
}),
251+
});
252+
},
253+
});
254+
255+
await withApiContext(() =>
256+
handler(
257+
makeRequest({
258+
chatId: "chat-1",
259+
trigger: "submit-message",
260+
headStartMessages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }],
261+
})
262+
)
263+
);
264+
265+
const sessionCreate = requests.find((r) =>
266+
r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/")
267+
);
268+
expect(sessionCreate).toBeDefined();
269+
const body = JSON.parse(sessionCreate!.init!.body as string);
270+
expect(body.triggerConfig.tags).toEqual([
271+
"chat:chat-1",
272+
"org:acme",
273+
"agentic-run:xyz",
274+
]);
275+
expect(body.triggerConfig.queue).toBe("my-queue");
276+
expect(body.triggerConfig.basePayload.trigger).toBe("handover-prepare");
277+
expect(body.triggerConfig.basePayload.chatId).toBe("chat-1");
278+
});
279+
219280
it("dispatches handover with isFinal=true on pure-text finishReason", async () => {
220281
const requests: CapturedRequest[] = [];
221282
global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => {

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

Lines changed: 40 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
SessionStreamInstance,
6060
TRIGGER_CONTROL_SUBTYPE,
6161
apiClientManager,
62+
type SessionTriggerConfig,
6263
} from "@trigger.dev/core/v3";
6364
// Runtime VALUES via the ESM/CJS shim so the CJS build can `require` ESM-only
6465
// `ai@7` (see ../imports/ai-runtime.ts).
@@ -195,6 +196,12 @@ export type HeadStartHandlerOptions<TTools extends Record<string, Tool>> = {
195196
* exiting. Defaults to 60.
196197
*/
197198
idleTimeoutInSeconds?: number;
199+
/**
200+
* Run options for the auto-triggered `handover-prepare` session run —
201+
* tags, queue, machine, etc. Mirrors `chat.createStartSessionAction`.
202+
* The `chat:{chatId}` tag is prepended automatically.
203+
*/
204+
triggerConfig?: Partial<SessionTriggerConfig>;
198205
};
199206

200207
// ---------------------------------------------------------------------------
@@ -220,6 +227,7 @@ export const chat = {
220227
req,
221228
agentId: opts.agentId,
222229
idleTimeoutInSeconds: opts.idleTimeoutInSeconds,
230+
triggerConfig: opts.triggerConfig,
223231
});
224232

225233
const helper: HeadStartChatHelper<TTools> = {
@@ -249,6 +257,7 @@ export const chat = {
249257
req: Request;
250258
agentId: string;
251259
idleTimeoutInSeconds?: number;
260+
triggerConfig?: Partial<SessionTriggerConfig>;
252261
}): Promise<HeadStartSession> {
253262
return openHandoverSession(opts).then((s) => s.handle);
254263
},
@@ -304,6 +313,7 @@ async function openHandoverSession(opts: {
304313
req: Request;
305314
agentId: string;
306315
idleTimeoutInSeconds?: number;
316+
triggerConfig?: Partial<SessionTriggerConfig>;
307317
}): Promise<InternalSession> {
308318
const wirePayload = (await opts.req.json()) as ChatTaskWirePayload;
309319
const chatId = wirePayload.chatId;
@@ -323,7 +333,35 @@ async function openHandoverSession(opts: {
323333
const modelMessages = await convertToModelMessages(uiMessages);
324334

325335
const apiClient = resolveApiClient();
326-
const idleTimeoutInSeconds = opts.idleTimeoutInSeconds ?? 60;
336+
const idleTimeoutInSeconds =
337+
opts.idleTimeoutInSeconds ?? opts.triggerConfig?.idleTimeoutInSeconds ?? 60;
338+
339+
const userTags = opts.triggerConfig?.tags ?? [];
340+
const tags = [`chat:${chatId}`, ...userTags].slice(0, 5);
341+
342+
const triggerConfig: SessionTriggerConfig = {
343+
basePayload: {
344+
...(opts.triggerConfig?.basePayload ?? {}),
345+
...wirePayload,
346+
chatId,
347+
trigger: "handover-prepare",
348+
idleTimeoutInSeconds,
349+
},
350+
...(opts.triggerConfig?.machine ? { machine: opts.triggerConfig.machine } : {}),
351+
...(opts.triggerConfig?.queue ? { queue: opts.triggerConfig.queue } : {}),
352+
tags,
353+
...(opts.triggerConfig?.maxAttempts !== undefined
354+
? { maxAttempts: opts.triggerConfig.maxAttempts }
355+
: {}),
356+
...(opts.triggerConfig?.maxDuration !== undefined
357+
? { maxDuration: opts.triggerConfig.maxDuration }
358+
: {}),
359+
...(opts.triggerConfig?.region ? { region: opts.triggerConfig.region } : {}),
360+
...(opts.triggerConfig?.lockToVersion
361+
? { lockToVersion: opts.triggerConfig.lockToVersion }
362+
: {}),
363+
idleTimeoutInSeconds,
364+
};
327365

328366
// Create the session and trigger the chat.agent's `handover-prepare`
329367
// run atomically. `createSession` is idempotent on `(env, externalId
@@ -342,15 +380,7 @@ async function openHandoverSession(opts: {
342380
type: "chat.agent",
343381
externalId: chatId,
344382
taskIdentifier: opts.agentId,
345-
triggerConfig: {
346-
basePayload: {
347-
...wirePayload,
348-
chatId,
349-
trigger: "handover-prepare",
350-
idleTimeoutInSeconds,
351-
},
352-
idleTimeoutInSeconds,
353-
},
383+
triggerConfig,
354384
});
355385
const sessionPublicAccessToken = created.publicAccessToken;
356386

0 commit comments

Comments
 (0)