Skip to content

Commit 51221f4

Browse files
committed
feat(sdk,core,webapp): chat sessions accept concurrencyKey and trigger-time named limits
Chat agents already accept the task-level concurrency option, but the session trigger path had no way to scope runs: SessionTriggerConfig now carries concurrencyKey and up to two named limits, threaded through the session run trigger (initial, continuation, and upgrade re-triggers) and forwarded by all three session starters (createStartSessionAction, the AgentChat client, and handover). Keys are never defaulted from the chat ID; a session without one shares the task's keyless pool. Named limits are validated client-side with the same rules as tasks.trigger.
1 parent c0175c7 commit 51221f4

9 files changed

Lines changed: 81 additions & 2 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/core": patch
4+
---
5+
6+
Chat agents can now scope concurrency per session. Pass `concurrencyKey` (for example your chat or tenant ID) and trigger-time named limits via `triggerConfig.concurrency` when starting a chat session, from `chat.createStartSessionAction`, the `AgentChat` client, or a handover. Keys are never defaulted, so a session without one shares the task's keyless pool.
7+
8+
```ts
9+
const start = chat.createStartSessionAction("support-chat", {
10+
triggerConfig: { concurrencyKey: user.id },
11+
});
12+
```

apps/webapp/app/services/realtime/sessionRunManager.server.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ async function triggerSessionRun(params: {
318318
options: {
319319
...(config.machine ? { machine: config.machine as never } : {}),
320320
...(config.queue ? { queue: { name: config.queue } } : {}),
321+
...(config.concurrency ? { concurrency: config.concurrency } : {}),
322+
...(config.concurrencyKey !== undefined ? { concurrencyKey: config.concurrencyKey } : {}),
321323
...(config.tags ? { tags: config.tags } : {}),
322324
...(config.maxAttempts !== undefined ? { maxAttempts: config.maxAttempts } : {}),
323325
...(config.maxDuration !== undefined ? { maxDuration: config.maxDuration } : {}),

docs/ai-chat/client-protocol.mdx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,8 @@ Pick `"preload"` when the UI has rendered but the user hasn't typed (warms the a
206206
| `expiresAt` | `string` (ISO date) | Retention cap. |
207207
| `triggerConfig.machine` | `string` | Machine preset (`micro`, `small-1x`, …) for every run. |
208208
| `triggerConfig.queue` | `string` | Queue name. |
209+
| `triggerConfig.concurrency` | `string[]` | Up to two [named concurrency limits](/concurrency#sharing-a-limit-between-tasks) every run holds, replacing the task's declared named limits. |
210+
| `triggerConfig.concurrencyKey` | `string` | Scopes every run of this session to its own pool under each `perKey` bound it holds. Never defaulted — pass one (e.g. your chat or tenant ID) to isolate sessions from each other. |
209211
| `triggerConfig.tags` | `string[]` | Tags applied to every run (in addition to session-level `tags`). |
210212
| `triggerConfig.maxAttempts` | `number` | Per-run retry cap (1–10). |
211213
| `triggerConfig.maxDuration` | `number` | Per-run wall-clock cap, seconds. |

packages/core/src/v3/schemas/api.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,6 +1890,10 @@ export const SessionTriggerConfig = z.object({
18901890
basePayload: z.record(z.unknown()),
18911891
machine: MachinePresetName.optional(),
18921892
queue: z.string().max(128).optional(),
1893+
/** Named concurrency limits every run holds, replacing the task's declared named limits. */
1894+
concurrency: z.string().min(1).max(128).array().max(2).optional(),
1895+
/** Scopes every run to its own pool under each `perKey` bound it holds. Never defaulted — a session without one shares the keyless pool. */
1896+
concurrencyKey: ConcurrencyKeySchema.optional(),
18931897
tags: z.array(z.string().max(128)).max(10).optional(),
18941898
maxAttempts: z.number().int().positive().max(10).optional(),
18951899
/** Per-run wall-clock cap (seconds). Forwarded to `TaskRunOptions.maxDuration`. */

packages/trigger-sdk/src/v3/ai.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ import {
118118
sessions,
119119
type SessionSubscribeOptions,
120120
} from "./sessions.js";
121-
import { createTask } from "./shared.js";
121+
import { createTask, triggerConcurrencyBody } from "./shared.js";
122122
import { markChatAgentRunForStreamsWarning } from "./streams.js";
123123
import { tracer } from "./tracer.js";
124124

@@ -11800,6 +11800,9 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
1180011800
params.clientData !== undefined ? { metadata: params.clientData } : {};
1180111801
const maxAttempts = params.triggerConfig?.maxAttempts ?? options?.triggerConfig?.maxAttempts;
1180211802
const maxDuration = params.triggerConfig?.maxDuration ?? options?.triggerConfig?.maxDuration;
11803+
const concurrency = params.triggerConfig?.concurrency ?? options?.triggerConfig?.concurrency;
11804+
const concurrencyKey =
11805+
params.triggerConfig?.concurrencyKey ?? options?.triggerConfig?.concurrencyKey;
1180311806
const idleTimeoutInSeconds =
1180411807
params.triggerConfig?.idleTimeoutInSeconds ?? options?.triggerConfig?.idleTimeoutInSeconds;
1180511808

@@ -11818,6 +11821,8 @@ function createChatStartSessionAction<TChat extends AnyTask = AnyTask>(
1181811821
...(options?.triggerConfig?.queue || params.triggerConfig?.queue
1181911822
? { queue: params.triggerConfig?.queue ?? options?.triggerConfig?.queue }
1182011823
: {}),
11824+
...(concurrency ? triggerConcurrencyBody(concurrency) : {}),
11825+
...(concurrencyKey !== undefined ? { concurrencyKey } : {}),
1182111826
tags,
1182211827
...(maxAttempts !== undefined ? { maxAttempts } : {}),
1182311828
...(maxDuration !== undefined ? { maxDuration } : {}),

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,12 @@ export class AgentChat<TAgent = unknown> {
671671
},
672672
...(this.triggerConfigDefault?.machine ? { machine: this.triggerConfigDefault.machine } : {}),
673673
...(this.triggerConfigDefault?.queue ? { queue: this.triggerConfigDefault.queue } : {}),
674+
...(this.triggerConfigDefault?.concurrency
675+
? { concurrency: this.triggerConfigDefault.concurrency }
676+
: {}),
677+
...(this.triggerConfigDefault?.concurrencyKey !== undefined
678+
? { concurrencyKey: this.triggerConfigDefault.concurrencyKey }
679+
: {}),
674680
tags: chatRunTags(this.chatId, this.triggerConfigDefault?.tags),
675681
...(this.triggerConfigDefault?.maxAttempts !== undefined
676682
? { maxAttempts: this.triggerConfigDefault.maxAttempts }

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
import type { FinishReason, ModelMessage, Tool, UIMessage, UIMessageChunk } from "ai";
7373
import type { ChatInputChunk, ChatTaskWirePayload } from "./ai-shared.js";
7474
import { chatRunTags } from "./ai-shared.js";
75+
import { triggerConcurrencyBody } from "./shared.js";
7576

7677
// `StreamTextResult` is defined locally rather than imported from `ai`: its
7778
// generic arity diverged (v6 `StreamTextResult<TOOLS, OUTPUT>`, v7
@@ -543,6 +544,12 @@ async function openHandoverSession(opts: {
543544
},
544545
...(opts.triggerConfig?.machine ? { machine: opts.triggerConfig.machine } : {}),
545546
...(opts.triggerConfig?.queue ? { queue: opts.triggerConfig.queue } : {}),
547+
...(opts.triggerConfig?.concurrency
548+
? triggerConcurrencyBody(opts.triggerConfig.concurrency)
549+
: {}),
550+
...(opts.triggerConfig?.concurrencyKey !== undefined
551+
? { concurrencyKey: opts.triggerConfig.concurrencyKey }
552+
: {}),
546553
tags,
547554
...(opts.triggerConfig?.maxAttempts !== undefined
548555
? { maxAttempts: opts.triggerConfig.maxAttempts }

packages/trigger-sdk/src/v3/createStartSessionAction.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,47 @@ describe("chat.createStartSessionAction — runtime", () => {
150150
expect(lastStartBody?.triggerConfig.lockToVersion).toBe("20260101.1");
151151
});
152152

153+
it("forwards concurrency and concurrencyKey from triggerConfig, with per-call precedence", async () => {
154+
installStartFixture();
155+
156+
const start = chat.createStartSessionAction("fake-chat", {
157+
triggerConfig: {
158+
concurrency: ["chats"],
159+
concurrencyKey: "tenant-default",
160+
},
161+
});
162+
await start({
163+
chatId: "chat-conc",
164+
triggerConfig: { concurrencyKey: "tenant-42" },
165+
});
166+
167+
expect(lastStartBody?.triggerConfig.concurrency).toEqual(["chats"]);
168+
expect(lastStartBody?.triggerConfig.concurrencyKey).toBe("tenant-42");
169+
});
170+
171+
it("never defaults concurrencyKey from the chatId", async () => {
172+
installStartFixture();
173+
174+
const start = chat.createStartSessionAction("fake-chat");
175+
await start({ chatId: "chat-no-key" });
176+
177+
expect(lastStartBody?.triggerConfig.concurrencyKey).toBeUndefined();
178+
expect(lastStartBody?.triggerConfig.concurrency).toBeUndefined();
179+
});
180+
181+
it("rejects invalid trigger-time limit names before any network call", async () => {
182+
installStartFixture();
183+
184+
const start = chat.createStartSessionAction("fake-chat", {
185+
triggerConfig: { concurrency: ["not a valid name!"] },
186+
});
187+
188+
await expect(start({ chatId: "chat-bad-limit" })).rejects.toThrow(
189+
/letters, numbers, underscores and hyphens/
190+
);
191+
expect(lastStartBody).toBeUndefined();
192+
});
193+
153194
it("server-mints override tokens for additional API keys", async () => {
154195
const requests: Array<{ url: string; body: unknown }> = [];
155196
const start = chat.createStartSessionAction("fake-chat", {

packages/trigger-sdk/src/v3/shared.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,7 +217,7 @@ function triggerQueueBody(
217217
* Trigger-time named limits: strings only, like `queue`. They replace the task's
218218
* declared named limits for this run; the server resolves names to the run's gates.
219219
*/
220-
function triggerConcurrencyBody(concurrency: string | string[] | undefined): {
220+
export function triggerConcurrencyBody(concurrency: string | string[] | undefined): {
221221
concurrency?: string[];
222222
} {
223223
if (!concurrency) {

0 commit comments

Comments
 (0)