Skip to content
Closed
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-headstart-trigger-config.md
Original file line number Diff line number Diff line change
@@ -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()`.
61 changes: 61 additions & 0 deletions packages/trigger-sdk/src/v3/chat-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down
50 changes: 40 additions & 10 deletions packages/trigger-sdk/src/v3/chat-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -195,6 +196,12 @@ export type HeadStartHandlerOptions<TTools extends Record<string, Tool>> = {
* 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<SessionTriggerConfig>;
};

// ---------------------------------------------------------------------------
Expand All @@ -220,6 +227,7 @@ export const chat = {
req,
agentId: opts.agentId,
idleTimeoutInSeconds: opts.idleTimeoutInSeconds,
triggerConfig: opts.triggerConfig,
});

const helper: HeadStartChatHelper<TTools> = {
Expand Down Expand Up @@ -249,6 +257,7 @@ export const chat = {
req: Request;
agentId: string;
idleTimeoutInSeconds?: number;
triggerConfig?: Partial<SessionTriggerConfig>;
}): Promise<HeadStartSession> {
return openHandoverSession(opts).then((s) => s.handle);
},
Expand Down Expand Up @@ -304,6 +313,7 @@ async function openHandoverSession(opts: {
req: Request;
agentId: string;
idleTimeoutInSeconds?: number;
triggerConfig?: Partial<SessionTriggerConfig>;
}): Promise<InternalSession> {
const wirePayload = (await opts.req.json()) as ChatTaskWirePayload;
const chatId = wirePayload.chatId;
Expand All @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Tag limit discrepancy: headStart uses 5 (correct), createStartSessionAction uses 10 (pre-existing issue)

The new headStart code at packages/trigger-sdk/src/v3/chat-server.ts:340 truncates tags with .slice(0, 5), which correctly matches the SessionTriggerConfig schema at packages/core/src/v3/schemas/api.ts:1577 (.max(5)). However, the analogous createStartSessionAction at packages/trigger-sdk/src/v3/ai.ts:9714 uses .slice(0, 10) with a comment saying "Platform cap is 10 tags per run" — that 10-tag cap applies to CreateSessionRequestBody.tags (line 1615), not triggerConfig.tags. If a customer passes >4 user tags through createStartSessionAction, the server would reject the request due to schema validation. The headStart code is the correct one; the pre-existing createStartSessionAction has a latent bug for users with many tags.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


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
Expand All @@ -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;

Expand Down