diff --git a/.changeset/chat-headstart-trigger-config.md b/.changeset/chat-headstart-trigger-config.md new file mode 100644 index 0000000000..8ac7f88233 --- /dev/null +++ b/.changeset/chat-headstart-trigger-config.md @@ -0,0 +1,5 @@ +--- +"@trigger.dev/sdk": patch +--- + +Add `triggerConfig` support to `chat.headStart()` so handover-prepare runs inherit tags, queue, and other session trigger options like `chat.createStartSessionAction()`. diff --git a/packages/trigger-sdk/src/v3/ai.ts b/packages/trigger-sdk/src/v3/ai.ts index 72e0cf1080..ddc4c45da3 100644 --- a/packages/trigger-sdk/src/v3/ai.ts +++ b/packages/trigger-sdk/src/v3/ai.ts @@ -9710,8 +9710,8 @@ function createChatStartSessionAction( // run-list filter by chat works without the customer having to wire it // up. Mirrors the browser-mediated `TriggerChatTransport.doStart` path. const userTags = params.triggerConfig?.tags ?? options?.triggerConfig?.tags ?? []; - // Platform cap is 10 tags per run; the auto chat tag takes one slot. - const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 10); + // SessionTriggerConfig.tags allows at most 5; the auto chat tag takes one slot. + const tags = [`chat:${params.chatId}`, ...userTags].slice(0, 5); const clientDataMetadata = params.clientData !== undefined ? { metadata: params.clientData } : {}; diff --git a/packages/trigger-sdk/src/v3/chat-server.test.ts b/packages/trigger-sdk/src/v3/chat-server.test.ts index dc9ef11788..b8def19699 100644 --- a/packages/trigger-sdk/src/v3/chat-server.test.ts +++ b/packages/trigger-sdk/src/v3/chat-server.test.ts @@ -216,6 +216,67 @@ describe("chat.headStart (route handler)", () => { expect(body.triggerConfig.basePayload.idleTimeoutInSeconds).toBe(60); }); + it("merges triggerConfig tags and queue into createSession", async () => { + const requests: CapturedRequest[] = []; + global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { + const urlStr = typeof url === "string" ? url : url.toString(); + requests.push({ url: urlStr, init }); + if (urlStr.endsWith("/api/v1/sessions") || urlStr.endsWith("/api/v1/sessions/")) { + return createSessionResponse("chat-1"); + } + if (urlStr.includes("/realtime/v1/sessions/") && urlStr.endsWith("/in/append")) { + return appendOkResponse(); + } + if (/\/realtime\/v1\/sessions\/[^/]+\/out$/.test(urlStr)) { + return new Response(new ReadableStream({ start(c) { c.close(); } }), { + status: 200, + headers: { "content-type": "text/event-stream" }, + }); + } + throw new Error(`Unexpected URL: ${urlStr}`); + }); + + const handler = chat.headStart({ + agentId: "test-agent", + triggerConfig: { + tags: ["org:acme", "agentic-run:xyz"], + queue: "my-queue", + }, + run: async ({ chat: chatHelper }) => { + return streamText({ + ...chatHelper.toStreamTextOptions(), + model: new MockLanguageModelV3({ + doStream: async () => ({ stream: textStream("hi back") }), + }), + }); + }, + }); + + await withApiContext(() => + handler( + makeRequest({ + chatId: "chat-1", + trigger: "submit-message", + headStartMessages: [{ id: "m1", role: "user", parts: [{ type: "text", text: "hi" }] }], + }) + ) + ); + + const sessionCreate = requests.find((r) => + r.url.endsWith("/api/v1/sessions") || r.url.endsWith("/api/v1/sessions/") + ); + expect(sessionCreate).toBeDefined(); + const body = JSON.parse(sessionCreate!.init!.body as string); + expect(body.triggerConfig.tags).toEqual([ + "chat:chat-1", + "org:acme", + "agentic-run:xyz", + ]); + expect(body.triggerConfig.queue).toBe("my-queue"); + expect(body.triggerConfig.basePayload.trigger).toBe("handover-prepare"); + expect(body.triggerConfig.basePayload.chatId).toBe("chat-1"); + }); + it("dispatches handover with isFinal=true on pure-text finishReason", async () => { const requests: CapturedRequest[] = []; global.fetch = vi.fn().mockImplementation(async (url: string | URL, init?: RequestInit) => { diff --git a/packages/trigger-sdk/src/v3/chat-server.ts b/packages/trigger-sdk/src/v3/chat-server.ts index 95e4fb529c..36d394f248 100644 --- a/packages/trigger-sdk/src/v3/chat-server.ts +++ b/packages/trigger-sdk/src/v3/chat-server.ts @@ -59,6 +59,7 @@ import { SessionStreamInstance, TRIGGER_CONTROL_SUBTYPE, apiClientManager, + type SessionTriggerConfig, } from "@trigger.dev/core/v3"; // Runtime VALUES via the ESM/CJS shim so the CJS build can `require` ESM-only // `ai@7` (see ../imports/ai-runtime.ts). @@ -195,6 +196,12 @@ export type HeadStartHandlerOptions> = { * exiting. Defaults to 60. */ idleTimeoutInSeconds?: number; + /** + * Run options for the auto-triggered `handover-prepare` session run — + * tags, queue, machine, etc. Mirrors `chat.createStartSessionAction`. + * The `chat:{chatId}` tag is prepended automatically. + */ + triggerConfig?: Partial; }; // --------------------------------------------------------------------------- @@ -220,6 +227,7 @@ export const chat = { req, agentId: opts.agentId, idleTimeoutInSeconds: opts.idleTimeoutInSeconds, + triggerConfig: opts.triggerConfig, }); const helper: HeadStartChatHelper = { @@ -249,6 +257,7 @@ export const chat = { req: Request; agentId: string; idleTimeoutInSeconds?: number; + triggerConfig?: Partial; }): Promise { return openHandoverSession(opts).then((s) => s.handle); }, @@ -304,6 +313,7 @@ async function openHandoverSession(opts: { req: Request; agentId: string; idleTimeoutInSeconds?: number; + triggerConfig?: Partial; }): Promise { const wirePayload = (await opts.req.json()) as ChatTaskWirePayload; const chatId = wirePayload.chatId; @@ -323,7 +333,35 @@ async function openHandoverSession(opts: { const modelMessages = await convertToModelMessages(uiMessages); const apiClient = resolveApiClient(); - const idleTimeoutInSeconds = opts.idleTimeoutInSeconds ?? 60; + const idleTimeoutInSeconds = + opts.idleTimeoutInSeconds ?? opts.triggerConfig?.idleTimeoutInSeconds ?? 60; + + const userTags = opts.triggerConfig?.tags ?? []; + const tags = [`chat:${chatId}`, ...userTags].slice(0, 5); + + const triggerConfig: SessionTriggerConfig = { + basePayload: { + ...(opts.triggerConfig?.basePayload ?? {}), + ...wirePayload, + chatId, + trigger: "handover-prepare", + idleTimeoutInSeconds, + }, + ...(opts.triggerConfig?.machine ? { machine: opts.triggerConfig.machine } : {}), + ...(opts.triggerConfig?.queue ? { queue: opts.triggerConfig.queue } : {}), + tags, + ...(opts.triggerConfig?.maxAttempts !== undefined + ? { maxAttempts: opts.triggerConfig.maxAttempts } + : {}), + ...(opts.triggerConfig?.maxDuration !== undefined + ? { maxDuration: opts.triggerConfig.maxDuration } + : {}), + ...(opts.triggerConfig?.region ? { region: opts.triggerConfig.region } : {}), + ...(opts.triggerConfig?.lockToVersion + ? { lockToVersion: opts.triggerConfig.lockToVersion } + : {}), + idleTimeoutInSeconds, + }; // Create the session and trigger the chat.agent's `handover-prepare` // run atomically. `createSession` is idempotent on `(env, externalId @@ -342,15 +380,7 @@ async function openHandoverSession(opts: { type: "chat.agent", externalId: chatId, taskIdentifier: opts.agentId, - triggerConfig: { - basePayload: { - ...wirePayload, - chatId, - trigger: "handover-prepare", - idleTimeoutInSeconds, - }, - idleTimeoutInSeconds, - }, + triggerConfig, }); const sessionPublicAccessToken = created.publicAccessToken; diff --git a/packages/trigger-sdk/src/v3/createStartSessionAction.test.ts b/packages/trigger-sdk/src/v3/createStartSessionAction.test.ts index 2b3214b77d..2753716550 100644 --- a/packages/trigger-sdk/src/v3/createStartSessionAction.test.ts +++ b/packages/trigger-sdk/src/v3/createStartSessionAction.test.ts @@ -96,6 +96,25 @@ describe("chat.createStartSessionAction — runtime", () => { expect(lastStartBody?.triggerConfig.basePayload).not.toHaveProperty("metadata"); }); + it("prepends chat:{chatId} to triggerConfig.tags and caps at 5", async () => { + installStartFixture(); + + const start = chat.createStartSessionAction("fake-chat", { + triggerConfig: { + tags: ["org:acme", "a", "b", "c", "d", "e"], + }, + }); + await start({ chatId: "chat-tags" }); + + expect(lastStartBody?.triggerConfig.tags).toEqual([ + "chat:chat-tags", + "org:acme", + "a", + "b", + "c", + ]); + }); + it("keeps session-level metadata distinct from per-turn clientData", async () => { installStartFixture();