Skip to content
Open
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()`.
4 changes: 2 additions & 2 deletions packages/trigger-sdk/src/v3/ai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9710,8 +9710,8 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
// 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 } : {};
Expand Down
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);

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,
};
Comment on lines +342 to +364

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.

🚩 headStart handles more triggerConfig fields than createStartSessionAction

The new openHandoverSession in chat-server.ts:342-364 forwards maxDuration, lockToVersion, and region from the user's triggerConfig, while the existing createChatStartSessionAction in ai.ts:9719-9750 does NOT handle these three fields. This means customers using chat.headStart can configure maxDuration/lockToVersion/region, but customers using chat.createStartSessionAction cannot. This is a pre-existing gap in createChatStartSessionAction, not introduced by this PR, but the two APIs are now inconsistent. Consider adding these fields to createChatStartSessionAction in a follow-up for parity.

Open in Devin Review

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


// 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
19 changes: 19 additions & 0 deletions packages/trigger-sdk/src/v3/createStartSessionAction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down
Loading