@stigmer/runner 3.37.0 → 3.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/hermetic-activity.d.ts +5 -2
- package/dist/__test-utils__/hermetic-activity.js +12 -2
- package/dist/__test-utils__/hermetic-activity.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.js +6 -0
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/harness/run-turn.js +13 -13
- package/dist/harness/run-turn.js.map +1 -1
- package/dist/harness/terminal-table.d.ts +20 -5
- package/dist/harness/terminal-table.js +22 -8
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/shared/model-client.d.ts +10 -2
- package/dist/shared/model-client.js +1 -1
- package/dist/shared/model-client.js.map +1 -1
- package/dist/shared/worker-shutdown.d.ts +10 -5
- package/dist/shared/worker-shutdown.js +3 -3
- package/dist/shared/worker-shutdown.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/harness-contract/runtime-contract.ts +27 -16
- package/src/__test-utils__/hermetic-activity.ts +11 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/pause.status.json +0 -6
- package/src/activities/execute-cursor/__tests__/hermetic/pause-vs-shutdown.test.ts +28 -21
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.after-tool.status.json +0 -6
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json +0 -6
- package/src/activities/execute-deep-agent/__tests__/hermetic/pause-vs-shutdown.test.ts +27 -21
- package/src/activities/execute-deep-agent/turn-setup.ts +6 -0
- package/src/harness/run-turn.ts +13 -13
- package/src/harness/terminal-table.ts +23 -8
- package/src/shared/__tests__/model-client-sampling.test.ts +56 -0
- package/src/shared/__tests__/model-client.test.ts +15 -0
- package/src/shared/__tests__/worker-shutdown.test.ts +2 -2
- package/src/shared/model-client.ts +11 -3
- package/src/shared/worker-shutdown.ts +11 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"worker-shutdown.js","sourceRoot":"","sources":["../../src/shared/worker-shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAAiB;IAC5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACpC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,OAAO,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,8BAA8B,CAAC,SAAiB;IAC9D,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC7B,CAAC;AAaD,wFAAwF;AACxF,MAAM,UAAU,oBAAoB,CAAC,MAAmB;IACtD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,MAAM,GAAY,MAAM,CAAC,MAAM,CAAC;IACtC,IAAI,MAAM,YAAY,KAAK;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC;IACnD,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;AAC3D,CAAC;
|
|
1
|
+
{"version":3,"file":"worker-shutdown.js","sourceRoot":"","sources":["../../src/shared/worker-shutdown.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AAEH,MAAM,QAAQ,GAAG,IAAI,GAAG,EAA2B,CAAC;AAEpD;;;;;GAKG;AACH,MAAM,UAAU,4BAA4B,CAAC,SAAiB;IAC5D,MAAM,UAAU,GAAG,IAAI,eAAe,EAAE,CAAC;IACzC,QAAQ,CAAC,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,CAAC;IACpC,OAAO,UAAU,CAAC;AACpB,CAAC;AAED,kFAAkF;AAClF,MAAM,UAAU,yBAAyB,CAAC,SAAiB;IACzD,OAAO,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;AACzC,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,oBAAoB,CAAC,SAAiB;IACpD,QAAQ,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,KAAK,EAAE,CAAC;AACnC,CAAC;AAED,wEAAwE;AACxE,MAAM,UAAU,8BAA8B,CAAC,SAAiB;IAC9D,QAAQ,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC;AAC7B,CAAC;AAaD,wFAAwF;AACxF,MAAM,UAAU,oBAAoB,CAAC,MAAmB;IACtD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,SAAS,CAAC;IACtC,MAAM,MAAM,GAAY,MAAM,CAAC,MAAM,CAAC;IACtC,IAAI,MAAM,YAAY,KAAK;QAAE,OAAO,MAAM,CAAC,OAAO,CAAC;IACnD,OAAO,OAAO,MAAM,KAAK,QAAQ,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,WAAW,CAAC;AAC3D,CAAC;AA2BD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,MAAM,UAAU,wBAAwB,CAAC,QAAkC;IACzE,MAAM,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,GAAG,QAAQ,CAAC;IAC/D,IAAI,kBAAkB,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO,MAAM,CAAC;IAChB,CAAC;IACD,IAAI,qBAAqB,IAAI,kBAAkB,KAAK,iBAAiB,EAAE,CAAC;QACtE,OAAO,iBAAiB,CAAC;IAC3B,CAAC;IACD,OAAO,kBAAkB,KAAK,WAAW,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,gBAAgB,CAAC;AACrF,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/runner",
|
|
3
|
-
"version": "3.37.
|
|
3
|
+
"version": "3.37.1",
|
|
4
4
|
"description": "Embeddable Temporal worker for the Stigmer AI agent platform — handles agent execution, workflow orchestration, and MCP server management",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -108,10 +108,10 @@
|
|
|
108
108
|
"@opentelemetry/sdk-metrics": "^2.0.0",
|
|
109
109
|
"@opentelemetry/sdk-trace-base": "^2.0.0",
|
|
110
110
|
"@opentelemetry/sdk-trace-node": "^2.0.0",
|
|
111
|
-
"@stigmer/outbound": "3.37.
|
|
112
|
-
"@stigmer/protos": "3.37.
|
|
113
|
-
"@stigmer/temporal-codecs": "3.37.
|
|
114
|
-
"@stigmer/zip-structure": "3.37.
|
|
111
|
+
"@stigmer/outbound": "3.37.1",
|
|
112
|
+
"@stigmer/protos": "3.37.1",
|
|
113
|
+
"@stigmer/temporal-codecs": "3.37.1",
|
|
114
|
+
"@stigmer/zip-structure": "3.37.1",
|
|
115
115
|
"@temporalio/activity": "1.16.2",
|
|
116
116
|
"@temporalio/common": "1.16.2",
|
|
117
117
|
"@temporalio/interceptors-opentelemetry": "1.16.2",
|
|
@@ -594,12 +594,17 @@ function hangingTurn(before: string): TurnScenario {
|
|
|
594
594
|
}
|
|
595
595
|
|
|
596
596
|
/**
|
|
597
|
-
*
|
|
598
|
-
*
|
|
599
|
-
*
|
|
600
|
-
*
|
|
597
|
+
* An orchestrator stop (a user's Pause or Cancel, one cancellation to the
|
|
598
|
+
* turn): Temporal's cancellation delivered while the engine hangs settles
|
|
599
|
+
* the transcript into one write that carries no phase and no row (the
|
|
600
|
+
* workflow writes both, stigmer#980), no error, no `completedAt`, and
|
|
601
|
+
* THROWS the stop's `CancelledFailure` so the workflow sees the activity
|
|
602
|
+
* stop.
|
|
601
603
|
*/
|
|
602
|
-
export async function
|
|
604
|
+
export async function assertOrchestratorStopSettlesAndThrows(
|
|
605
|
+
harness: RuntimeContractHarness,
|
|
606
|
+
label = "rt-orchestrator-stop",
|
|
607
|
+
): Promise<RuntimeArmResult> {
|
|
603
608
|
const { subject } = harness;
|
|
604
609
|
const driver = new RuntimeExecutionDriver(harness, label);
|
|
605
610
|
let controls: InvocationControls | undefined;
|
|
@@ -607,16 +612,22 @@ export async function assertPausePersistsAndThrows(harness: RuntimeContractHarne
|
|
|
607
612
|
|
|
608
613
|
const invocation = await driver.turn(hangingTurn("Starting on the parser."), { onControls: (c) => (controls = c) });
|
|
609
614
|
|
|
610
|
-
expect(threwCancelledFailure(invocation.outcome), `${subject.name}:
|
|
615
|
+
expect(threwCancelledFailure(invocation.outcome), `${subject.name}: an orchestrator stop THROWS a CancelledFailure`).toBe(true);
|
|
611
616
|
if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
|
|
612
|
-
expect(invocation.outcome.error.message).toBe(TERMINAL_COPY.
|
|
613
|
-
expect(
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
617
|
+
expect(invocation.outcome.error.message).toBe(TERMINAL_COPY.orchestratorStop.throwMessage);
|
|
618
|
+
expect(
|
|
619
|
+
driver.record.persistedPhases.at(-1),
|
|
620
|
+
`${subject.name}: the stop writes no phase; the last phase is the turn's own`,
|
|
621
|
+
).toBe(ExecutionPhase.EXECUTION_IN_PROGRESS);
|
|
622
|
+
const settle = driver.record.persisted.at(-1);
|
|
623
|
+
if (!settle) throw new Error(`${subject.name}: the runtime persisted nothing`);
|
|
624
|
+
expect(settle.phase, `${subject.name}: the settle write carries no phase`).toBe(ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED);
|
|
625
|
+
assertTranscriptStreamsNothing(subject.name, settle);
|
|
626
|
+
expect(settle.error, `${subject.name}: a stop is not an error`).toBe("");
|
|
627
|
+
expect(settle.completedAt, `${subject.name}: a stopped turn is not complete`).toBe("");
|
|
628
|
+
expect(systemMessages(settle), `${subject.name}: no stop row; the workflow writes it`).toEqual([]);
|
|
629
|
+
expect(aiMessages(settle)).not.toContain(NEVER_SEEN);
|
|
630
|
+
return { driver, invocations: [invocation], final: settle };
|
|
620
631
|
}
|
|
621
632
|
|
|
622
633
|
/**
|
|
@@ -970,9 +981,9 @@ export function describeHarnessRuntimeContract(harness: RuntimeContractHarness,
|
|
|
970
981
|
await assertNonExecutingDecisionSettlesSkipped(harness, action);
|
|
971
982
|
});
|
|
972
983
|
}
|
|
973
|
-
it("
|
|
984
|
+
it("an orchestrator stop settles the transcript with no phase and no row, and throws the stop's CancelledFailure", async () => {
|
|
974
985
|
clock.reset();
|
|
975
|
-
await
|
|
986
|
+
await assertOrchestratorStopSettlesAndThrows(harness);
|
|
976
987
|
});
|
|
977
988
|
it("a worker shutdown persists FAILED with the shutdown copy once and throws the shutdown CancelledFailure", async () => {
|
|
978
989
|
clock.reset();
|
|
@@ -331,8 +331,11 @@ export class ExecutionRecord {
|
|
|
331
331
|
|
|
332
332
|
/**
|
|
333
333
|
* Apply one `UpdateStatus` write and answer the control signal the server
|
|
334
|
-
* would. A
|
|
335
|
-
* the response there
|
|
334
|
+
* would. A write with no phase never carries a signal (the runner ignores
|
|
335
|
+
* the response there). It is a setup-progress report, or the transcript an
|
|
336
|
+
* orchestrator stop settles (the runtime writes no phase for it; the
|
|
337
|
+
* workflow does, stigmer#980): the server's merge applies the transcript and
|
|
338
|
+
* leaves the phase to its writers, so the double keeps the held phase.
|
|
336
339
|
*/
|
|
337
340
|
applyStatusUpdate(status: AgentExecutionStatus): ExecutionControlSignal {
|
|
338
341
|
const snapshot = clone(AgentExecutionStatusSchema, status);
|
|
@@ -341,6 +344,12 @@ export class ExecutionRecord {
|
|
|
341
344
|
if (snapshot.setupProgress?.currentPhase) {
|
|
342
345
|
this.setupProgress.push(snapshot.setupProgress.currentPhase);
|
|
343
346
|
}
|
|
347
|
+
if (snapshot.messages.length > 0) {
|
|
348
|
+
const heldPhase = this.execution.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
|
|
349
|
+
this.execution.status = clone(AgentExecutionStatusSchema, snapshot);
|
|
350
|
+
this.execution.status.phase = heldPhase;
|
|
351
|
+
for (const listener of [...this.persistListeners]) listener();
|
|
352
|
+
}
|
|
344
353
|
return ExecutionControlSignal.UNSPECIFIED;
|
|
345
354
|
}
|
|
346
355
|
this.execution.status = clone(AgentExecutionStatusSchema, snapshot);
|
|
@@ -4,14 +4,8 @@
|
|
|
4
4
|
"type": "MESSAGE_AI",
|
|
5
5
|
"content": "Starting on the parser.",
|
|
6
6
|
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
7
|
-
},
|
|
8
|
-
{
|
|
9
|
-
"type": "MESSAGE_SYSTEM",
|
|
10
|
-
"content": "Execution paused by user. Use resume to continue.",
|
|
11
|
-
"timestamp": "2026-01-01T00:00:03.000Z"
|
|
12
7
|
}
|
|
13
8
|
],
|
|
14
|
-
"phase": "EXECUTION_PAUSED",
|
|
15
9
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
16
10
|
"fileReviewEventStream": {
|
|
17
11
|
"executionId": "aex_hermetic_0001",
|
|
@@ -1,23 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Hermetic goldens:
|
|
3
|
-
* that THROW, and
|
|
2
|
+
* Hermetic goldens: ORCHESTRATOR STOP versus WORKER SHUTDOWN — the two
|
|
3
|
+
* interruptions that THROW, and what tells them apart.
|
|
4
4
|
*
|
|
5
5
|
* Invariant pinned (the throw-vs-return table the runtime owns,
|
|
6
6
|
* `harness/terminal-table.ts`; parent §5c): both interruptions end the activity with a thrown Temporal
|
|
7
7
|
* `CancelledFailure` (never a return), but they persist DIFFERENT terminal
|
|
8
8
|
* states the control plane keys on:
|
|
9
9
|
*
|
|
10
|
-
* -
|
|
11
|
-
*
|
|
12
|
-
* and
|
|
10
|
+
* - an orchestrator stop (Temporal delivers cancellation, no shutdown
|
|
11
|
+
* signal: a user's Pause or Cancel, which the turn cannot tell apart)
|
|
12
|
+
* settles the transcript in one write with NO phase and NO row, and throws
|
|
13
|
+
* "Activity paused by orchestrator" (bytes kept: wire copy). The invoke
|
|
14
|
+
* workflow, which knows which stop it runs, writes the phase and the stop
|
|
15
|
+
* row, and the server's merge keeps the row last (stigmer#980). Until then
|
|
16
|
+
* this arm wrote PAUSED and the pause row, so a cancelled run ended with an
|
|
17
|
+
* instruction to resume it;
|
|
13
18
|
* - a worker shutdown (the runner's per-queue shutdown signal is aborted AND
|
|
14
19
|
* cancellation is delivered) persists EXECUTION_FAILED with the error copy
|
|
15
20
|
* "Execution interrupted: runner worker was shut down. Retry or resume." and
|
|
16
21
|
* throws "Activity cancelled (worker shutdown, not user pause)".
|
|
17
22
|
*
|
|
18
|
-
* The
|
|
19
|
-
*
|
|
20
|
-
*
|
|
23
|
+
* The goldens (`goldens/pause.status.json`, the stop's settle write, and
|
|
24
|
+
* `goldens/worker-shutdown.status.json`) are the wire contract, character for
|
|
25
|
+
* character. Both interruptions are triggered from
|
|
21
26
|
* a script `effect` step — never a timer — so the instant is deterministic: the
|
|
22
27
|
* stream loop sees `isCancelled()` on its next iteration and never processes
|
|
23
28
|
* the event that follows.
|
|
@@ -31,8 +36,9 @@
|
|
|
31
36
|
* writer of a terminal arm, so a second row has no code path to come from.
|
|
32
37
|
*
|
|
33
38
|
* Runtime phases exercised: the adapter's stop-signal cancel; the runtime's
|
|
34
|
-
* `classifyTurnInterruption`; the terminal table's
|
|
35
|
-
* arms through `settleWith`; the `finally` teardown under a
|
|
39
|
+
* `classifyTurnInterruption`; the terminal table's orchestrator-stop and
|
|
40
|
+
* worker-shutdown arms through `settleWith`; the `finally` teardown under a
|
|
41
|
+
* thrown exit.
|
|
36
42
|
*
|
|
37
43
|
* Regeneration history:
|
|
38
44
|
* - #1070: timestamp-only diff — every terminal stamp
|
|
@@ -41,6 +47,8 @@
|
|
|
41
47
|
* and the clock ticks on, one more step. Transcript unchanged.
|
|
42
48
|
* - 2026-09-12 (stigmer#1054, the PR after #1070): one removed system row per
|
|
43
49
|
* golden, nothing else. The transcript now carries each terminal row once.
|
|
50
|
+
* - stigmer#980: `pause.status.json` loses its phase and its pause row (the
|
|
51
|
+
* workflow writes both now); the transcript is otherwise unchanged.
|
|
44
52
|
*
|
|
45
53
|
* Regenerate ONLY after a deliberate behavior change:
|
|
46
54
|
* npx vitest run src/activities/execute-cursor/__tests__/hermetic -u
|
|
@@ -108,7 +116,7 @@ function interruptedTurn(
|
|
|
108
116
|
});
|
|
109
117
|
}
|
|
110
118
|
|
|
111
|
-
describe("ExecuteCursor hermetic —
|
|
119
|
+
describe("ExecuteCursor hermetic — orchestrator stop vs worker shutdown", () => {
|
|
112
120
|
let env: HermeticEnvironment;
|
|
113
121
|
let registry: ReturnType<typeof stubRegistryFetch>;
|
|
114
122
|
const clock = new ScriptedClock();
|
|
@@ -125,7 +133,7 @@ describe("ExecuteCursor hermetic — user pause vs worker shutdown", () => {
|
|
|
125
133
|
env.dispose();
|
|
126
134
|
});
|
|
127
135
|
|
|
128
|
-
it("
|
|
136
|
+
it("an orchestrator stop settles the transcript with no phase and no row, and throws CancelledFailure", async () => {
|
|
129
137
|
// ── Arrange ──────────────────────────────────────────────────────────────
|
|
130
138
|
clock.reset(); // both goldens read from :00
|
|
131
139
|
let controls: InvocationControls | undefined;
|
|
@@ -143,19 +151,18 @@ describe("ExecuteCursor hermetic — user pause vs worker shutdown", () => {
|
|
|
143
151
|
const invocation = await runCursorTurn(scenario, { onControls: (c) => (controls = c) });
|
|
144
152
|
|
|
145
153
|
// ── Assert: the throw and the persisted state ────────────────────────────
|
|
146
|
-
expect(threwCancelledFailure(invocation.outcome), "
|
|
154
|
+
expect(threwCancelledFailure(invocation.outcome), "an orchestrator stop THROWS CancelledFailure").toBe(true);
|
|
147
155
|
if (!threwCancelledFailure(invocation.outcome)) throw new Error("unreachable");
|
|
148
156
|
expect(invocation.outcome.error.message).toBe("Activity paused by orchestrator");
|
|
149
|
-
expect(record.persistedPhases).toEqual([
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
expect(final.error, "a pause is not an error").toBe("");
|
|
155
|
-
expect(final.completedAt, "a paused turn is not complete").toBe("");
|
|
157
|
+
expect(record.persistedPhases, "the stop writes no phase").toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS]);
|
|
158
|
+
const final = record.persisted.at(-1)!;
|
|
159
|
+
expect(final.phase, "the settle write carries no phase").toBe(ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED);
|
|
160
|
+
expect(final.error, "a stop is not an error").toBe("");
|
|
161
|
+
expect(final.completedAt, "a stopped turn is not complete").toBe("");
|
|
156
162
|
expect(
|
|
157
163
|
final.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
158
|
-
|
|
164
|
+
"no stop row: the workflow writes it",
|
|
165
|
+
).toEqual([]);
|
|
159
166
|
expect(final.messages.some((m) => m.content === NEVER_SEEN), "nothing after the cancel is processed").toBe(false);
|
|
160
167
|
|
|
161
168
|
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, final), null, 2) + "\n";
|
|
@@ -19,14 +19,8 @@
|
|
|
19
19
|
"toolKind": "TOOL_KIND_FILE_READ"
|
|
20
20
|
}
|
|
21
21
|
]
|
|
22
|
-
},
|
|
23
|
-
{
|
|
24
|
-
"type": "MESSAGE_SYSTEM",
|
|
25
|
-
"content": "Execution paused by user. Use resume to continue.",
|
|
26
|
-
"timestamp": "2026-01-01T00:00:02.000Z"
|
|
27
22
|
}
|
|
28
23
|
],
|
|
29
|
-
"phase": "EXECUTION_PAUSED",
|
|
30
24
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
31
25
|
"streamingUsage": {
|
|
32
26
|
"inputTokens": "1200",
|
package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/pause.loop.mid-tool.status.json
CHANGED
|
@@ -17,14 +17,8 @@
|
|
|
17
17
|
"toolKind": "TOOL_KIND_FILE_READ"
|
|
18
18
|
}
|
|
19
19
|
]
|
|
20
|
-
},
|
|
21
|
-
{
|
|
22
|
-
"type": "MESSAGE_SYSTEM",
|
|
23
|
-
"content": "Execution paused by user. Use resume to continue.",
|
|
24
|
-
"timestamp": "2026-01-01T00:00:01.000Z"
|
|
25
20
|
}
|
|
26
21
|
],
|
|
27
|
-
"phase": "EXECUTION_PAUSED",
|
|
28
22
|
"startedAt": "2026-01-01T00:00:00.000Z",
|
|
29
23
|
"streamingUsage": {
|
|
30
24
|
"inputTokens": "1200",
|
|
@@ -3,19 +3,22 @@
|
|
|
3
3
|
* the activity tells them apart — the throw-vs-return table the control
|
|
4
4
|
* plane's workflow keys on. Since #1096 the activity is the turn runtime
|
|
5
5
|
* over the native adapter, and the shapes below are the runtime's terminal
|
|
6
|
-
* table (`harness/terminal-table.ts
|
|
7
|
-
*
|
|
6
|
+
* table (`harness/terminal-table.ts`) and stigmer#1071 ("persist a thrown
|
|
7
|
+
* terminal once").
|
|
8
8
|
*
|
|
9
|
-
* 1.
|
|
9
|
+
* 1. ORCHESTRATOR STOP (a user's Pause or Cancel; the turn cannot tell
|
|
10
|
+
* them apart): the activity's `cancellationSignal` aborts. Whether the
|
|
10
11
|
* abort lands inside a model call (staged from the scripted model's
|
|
11
12
|
* `onTurn`) or while the loop is persisting (staged from the control
|
|
12
13
|
* plane's persist channel, the record's `controlSignal`), the runtime's
|
|
13
14
|
* stop controller aborts the adapter's signal, the adapter cancels the
|
|
14
|
-
* graph run and settles `interrupted`, and the runtime persists
|
|
15
|
-
* ONCE
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
15
|
+
* graph run and settles `interrupted`, and the runtime persists the
|
|
16
|
+
* transcript ONCE with no phase and no row, then throws
|
|
17
|
+
* `CancelledFailure("Activity paused by orchestrator")` (bytes kept:
|
|
18
|
+
* wire copy). The invoke workflow writes the phase and the stop row, and
|
|
19
|
+
* the server's merge keeps the row last (stigmer#980); until then this
|
|
20
|
+
* write carried PAUSED and "Execution paused by user. Use resume to
|
|
21
|
+
* continue.", which a cancelled run showed too. Until #1096 the
|
|
19
22
|
* orchestrator persisted PAUSED TWICE, the second write bare and erasing
|
|
20
23
|
* the transcript (stigmer#1054's double persist);
|
|
21
24
|
* `pause.bare.status.json` recorded that write and is gone with it. The
|
|
@@ -132,30 +135,33 @@ describe("ExecuteDeepAgent hermetic — pause vs worker shutdown", () => {
|
|
|
132
135
|
},
|
|
133
136
|
loopGolden: "./goldens/pause.loop.mid-tool.status.json",
|
|
134
137
|
},
|
|
135
|
-
])("
|
|
136
|
-
it("
|
|
138
|
+
])("orchestrator stop $staging", ({ begin, controlSignal, loopGolden }) => {
|
|
139
|
+
it("settles the transcript once, with no phase and no row, and throws as the stop", async () => {
|
|
137
140
|
clock.reset();
|
|
138
141
|
const record = deepAgentExecutionRecord({ message: "Look at my notes.", controlSignal });
|
|
139
142
|
inFlight = begin(record);
|
|
140
143
|
|
|
141
144
|
const invocation = await runDeepAgentTurn(inFlight);
|
|
142
145
|
|
|
143
|
-
expect(threwCancelledFailure(invocation.outcome), "
|
|
146
|
+
expect(threwCancelledFailure(invocation.outcome), "an orchestrator stop THROWS CancelledFailure").toBe(true);
|
|
144
147
|
expect((invocation.outcome as { error: Error }).error.message).toBe("Activity paused by orchestrator");
|
|
145
|
-
expect(record.persistedPhases).toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
|
|
148
|
+
expect(record.persistedPhases, "the stop writes no phase").toEqual([ExecutionPhase.EXECUTION_IN_PROGRESS]);
|
|
149
|
+
|
|
150
|
+
const settleWrites = record.persisted.filter(
|
|
151
|
+
(s) => s.phase === ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED && s.messages.length > 0,
|
|
152
|
+
);
|
|
153
|
+
expect(settleWrites, "the stop is settled exactly once (stigmer#1071)").toHaveLength(1);
|
|
154
|
+
const [paused] = settleWrites;
|
|
155
|
+
expect(
|
|
156
|
+
paused.messages.filter((m) => m.type === MessageType.MESSAGE_SYSTEM).map((m) => m.content),
|
|
157
|
+
"no stop row: the workflow writes it (stigmer#980)",
|
|
158
|
+
).toEqual([]);
|
|
153
159
|
expect(
|
|
154
160
|
paused.messages.flatMap((m) => m.toolCalls).map((tc) => tc.name),
|
|
155
|
-
"the read that landed before the
|
|
161
|
+
"the read that landed before the stop is in the one write",
|
|
156
162
|
).toEqual(["read_file"]);
|
|
157
163
|
expect(paused.completedAt).toBe("");
|
|
158
|
-
expect(record.
|
|
164
|
+
expect(record.persisted.at(-1), "the wire ends on the write that carries the transcript").toEqual(paused);
|
|
159
165
|
|
|
160
166
|
const json = JSON.stringify(toJson(AgentExecutionStatusSchema, paused), null, 2) + "\n";
|
|
161
167
|
await expect(json).toMatchFileSnapshot(loopGolden);
|
|
@@ -345,6 +345,11 @@ export async function buildEngine(
|
|
|
345
345
|
// native-thinking heuristic, and sub-agent inheritance. The credential is
|
|
346
346
|
// read from the ref at build, once per turn: a LangChain client carries
|
|
347
347
|
// its headers for its life, so this is the freshest a turn's model can be.
|
|
348
|
+
//
|
|
349
|
+
// No temperature: current Anthropic models refuse one other than their
|
|
350
|
+
// default (claude-opus-4.7 and later and claude-fable-5 inside
|
|
351
|
+
// @langchain/anthropic, before any request; claude-sonnet-5 at the
|
|
352
|
+
// provider), and the Cursor harness sends none (stigmer/stigmer#1341).
|
|
348
353
|
const buildModelFor = async (name: string) =>
|
|
349
354
|
(await buildChatModel({
|
|
350
355
|
modelName: name,
|
|
@@ -352,6 +357,7 @@ export async function buildEngine(
|
|
|
352
357
|
stigmerToken: config.stigmerTokenRef.current ?? undefined,
|
|
353
358
|
headerScope: { executionId },
|
|
354
359
|
serviceTier: input.model.serviceTier,
|
|
360
|
+
temperature: null,
|
|
355
361
|
})).model;
|
|
356
362
|
const model = await buildModelFor(modelName);
|
|
357
363
|
sink.setupTiming.mark("build_model");
|
package/src/harness/run-turn.ts
CHANGED
|
@@ -74,7 +74,7 @@ import {
|
|
|
74
74
|
failedArm,
|
|
75
75
|
fileReviewResolvedArm,
|
|
76
76
|
infrastructureCancelArm,
|
|
77
|
-
|
|
77
|
+
orchestratorStopArm,
|
|
78
78
|
platformStopArm,
|
|
79
79
|
stallArm,
|
|
80
80
|
TERMINAL_COPY,
|
|
@@ -463,7 +463,7 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
|
|
|
463
463
|
* The table for an `interrupted` outcome, in the orchestrator's precedence:
|
|
464
464
|
* a stall (FAILED, return) outranks everything; a cost cap (TERMINATED,
|
|
465
465
|
* return); then Temporal's cancellation as the signal's reason reads it —
|
|
466
|
-
* the runner's drain, the orchestrator's
|
|
466
|
+
* the runner's drain, the orchestrator's stop, an infrastructure cancel
|
|
467
467
|
* (all thrown); last a platform stop (COMPLETED, return).
|
|
468
468
|
*/
|
|
469
469
|
async function settleInterrupted(maxCostUsd: number, accumulator: UsageAccumulator): Promise<Settled> {
|
|
@@ -487,9 +487,9 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
|
|
|
487
487
|
case "worker-shutdown":
|
|
488
488
|
console.log(`${activityName} interrupted (worker shutdown): execution=${executionId}`);
|
|
489
489
|
return settleWith(workerShutdownArm());
|
|
490
|
-
case "
|
|
491
|
-
console.log(`${activityName}
|
|
492
|
-
return settleWith(
|
|
490
|
+
case "orchestrator-stop":
|
|
491
|
+
console.log(`${activityName} stopped by the orchestrator (pause or cancel): execution=${executionId}`);
|
|
492
|
+
return settleWith(orchestratorStopArm());
|
|
493
493
|
case "infrastructure":
|
|
494
494
|
console.log(`${activityName} interrupted (infrastructure cancel): execution=${executionId}`);
|
|
495
495
|
return settleWith(infrastructureCancelArm());
|
|
@@ -659,9 +659,9 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
|
|
|
659
659
|
case "worker-shutdown":
|
|
660
660
|
console.log(`${activityName} cancelled (worker shutdown) for execution ${executionId}`);
|
|
661
661
|
return workerShutdownArm();
|
|
662
|
-
case "
|
|
663
|
-
console.log(`${activityName} cancelled (
|
|
664
|
-
return
|
|
662
|
+
case "orchestrator-stop":
|
|
663
|
+
console.log(`${activityName} cancelled (orchestrator stop) for execution ${executionId}`);
|
|
664
|
+
return orchestratorStopArm();
|
|
665
665
|
case "infrastructure":
|
|
666
666
|
console.log(`${activityName} cancelled (infrastructure) for execution ${executionId}`);
|
|
667
667
|
return infrastructureCancelArm();
|
|
@@ -687,11 +687,11 @@ async function runTurn(deps: TurnRuntimeDeps, input: NormalizedActivityInput): P
|
|
|
687
687
|
if (arm !== undefined) {
|
|
688
688
|
// A non-cancellation error while cancelled was most likely caused by the
|
|
689
689
|
// cancellation itself (a teardown mid-flight) and must not overwrite the
|
|
690
|
-
// state the workflow already expects:
|
|
691
|
-
// else reads as the interruption it was.
|
|
692
|
-
if (
|
|
693
|
-
console.log(`${activityName} error during
|
|
694
|
-
return settleWith(arm, new CancelledFailure(TERMINAL_COPY.
|
|
690
|
+
// state the workflow already expects: an orchestrator stop stays the
|
|
691
|
+
// workflow's to write; anything else reads as the interruption it was.
|
|
692
|
+
if (interruption() === "orchestrator-stop") {
|
|
693
|
+
console.log(`${activityName} error during an orchestrator stop (treating as the stop): execution=${executionId}, error=${errDetail}`);
|
|
694
|
+
return settleWith(arm, new CancelledFailure(TERMINAL_COPY.orchestratorStop.throwMessageAfterError));
|
|
695
695
|
}
|
|
696
696
|
console.log(`${activityName} error during infrastructure cancel: execution=${executionId}, error=${errDetail}`);
|
|
697
697
|
return settleWith(
|
|
@@ -21,6 +21,16 @@
|
|
|
21
21
|
* infrastructure cancel it delivered — with the message the control
|
|
22
22
|
* planes recognise.
|
|
23
23
|
*
|
|
24
|
+
* One stop has no copy here: the orchestrator's (a Pause or a Cancel, which
|
|
25
|
+
* reach the activity as the same workflow-requested cancellation). The turn
|
|
26
|
+
* cannot tell which it is, so its arm writes no phase and no row, and the
|
|
27
|
+
* invoke workflow, which knows, writes the phase and the stop row that the
|
|
28
|
+
* server's merge keeps last (stigmer#980;
|
|
29
|
+
* `backend/services/stigmer-server/src/domain/agentexecution/platform-rows.ts`).
|
|
30
|
+
* The pause row lived here until then, and a cancelled run ended with an
|
|
31
|
+
* instruction to resume it. The arm's throw messages keep their bytes: they
|
|
32
|
+
* are the wire copy the control planes see on the activity failure.
|
|
33
|
+
*
|
|
24
34
|
* An arm's rows are appended exactly once, by `applyTerminalArm`, and an arm
|
|
25
35
|
* is persisted exactly once, by `run-turn.ts`'s `settleWith` — the one
|
|
26
36
|
* writer of a terminal. The orchestrator this table replaced appended the
|
|
@@ -47,6 +57,7 @@ export type TerminalDisposition = { readonly kind: "return" } | { readonly kind:
|
|
|
47
57
|
|
|
48
58
|
/** One arm of the table: what to write, and how to end. */
|
|
49
59
|
export interface TerminalArm {
|
|
60
|
+
/** `EXECUTION_PHASE_UNSPECIFIED` writes no phase: the merge applies a phase only when one is sent. */
|
|
50
61
|
readonly phase: ExecutionPhase;
|
|
51
62
|
/** `status.error`; absent leaves it as it is (a pause is not an error). */
|
|
52
63
|
readonly error?: string;
|
|
@@ -61,8 +72,7 @@ const RETURN: TerminalDisposition = { kind: "return" };
|
|
|
61
72
|
|
|
62
73
|
/** The fixed strings of the table, in one place. Every one is pinned by a golden or a hermetic assertion. */
|
|
63
74
|
export const TERMINAL_COPY = {
|
|
64
|
-
|
|
65
|
-
row: "Execution paused by user. Use resume to continue.",
|
|
75
|
+
orchestratorStop: {
|
|
66
76
|
throwMessage: "Activity paused by orchestrator",
|
|
67
77
|
throwMessageAfterError: "Activity paused by orchestrator (error during pause)",
|
|
68
78
|
},
|
|
@@ -138,7 +148,7 @@ export function toolCallLimitArm(): TerminalArm {
|
|
|
138
148
|
};
|
|
139
149
|
}
|
|
140
150
|
|
|
141
|
-
/** The runner is draining. Not a user
|
|
151
|
+
/** The runner is draining. Not a user's stop: FAILED with the shutdown copy, thrown as cancelled. */
|
|
142
152
|
export function workerShutdownArm(): TerminalArm {
|
|
143
153
|
return {
|
|
144
154
|
phase: ExecutionPhase.EXECUTION_FAILED,
|
|
@@ -149,13 +159,18 @@ export function workerShutdownArm(): TerminalArm {
|
|
|
149
159
|
};
|
|
150
160
|
}
|
|
151
161
|
|
|
152
|
-
/**
|
|
153
|
-
|
|
162
|
+
/**
|
|
163
|
+
* The orchestrator stopped the turn: a Pause or a Cancel, indistinguishable
|
|
164
|
+
* here (the module header). The transcript is settled and persisted with no
|
|
165
|
+
* phase and no row, because the workflow writes both; not an error, not
|
|
166
|
+
* complete; thrown as cancelled so the workflow sees the activity stop.
|
|
167
|
+
*/
|
|
168
|
+
export function orchestratorStopArm(): TerminalArm {
|
|
154
169
|
return {
|
|
155
|
-
phase: ExecutionPhase.
|
|
156
|
-
rows: [
|
|
170
|
+
phase: ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED,
|
|
171
|
+
rows: [],
|
|
157
172
|
completes: false,
|
|
158
|
-
disposition: { kind: "throw", message: TERMINAL_COPY.
|
|
173
|
+
disposition: { kind: "throw", message: TERMINAL_COPY.orchestratorStop.throwMessage },
|
|
159
174
|
};
|
|
160
175
|
}
|
|
161
176
|
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The native execution turn's request against the REAL @langchain/anthropic
|
|
3
|
+
* (stigmer/stigmer#1341): the unit suite in model-client.test.ts mocks the
|
|
4
|
+
* wrapper, so it cannot see the wrapper's own parameter validation. That
|
|
5
|
+
* validation is what failed every native turn on claude-opus-4.7,
|
|
6
|
+
* claude-opus-4.8 and claude-fable-5 while the runner pinned temperature 0.
|
|
7
|
+
* These tests build the client the way the execution turn does and read the
|
|
8
|
+
* request parameters offline; no request leaves the process.
|
|
9
|
+
*/
|
|
10
|
+
import { ChatAnthropic } from "@langchain/anthropic";
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
12
|
+
|
|
13
|
+
import { buildChatModel } from "../model-client.js";
|
|
14
|
+
import { _resetRegistryCache } from "../model-registry.js";
|
|
15
|
+
|
|
16
|
+
// Ids the wrapper treats as adaptive-only and refuses a sampling override
|
|
17
|
+
// for, and ids it lets one through for (the provider itself refuses it on
|
|
18
|
+
// claude-sonnet-5); the registry is unreachable in these tests, so each id
|
|
19
|
+
// reaches the wrapper unchanged.
|
|
20
|
+
const ADAPTIVE_ONLY_IDS = ["claude-opus-4-7", "claude-opus-4-8", "claude-fable-5"];
|
|
21
|
+
const SAMPLING_TOLERANT_IDS = ["claude-sonnet-4-6", "claude-opus-4-6", "claude-sonnet-5", "claude-haiku-4-5-20251001"];
|
|
22
|
+
|
|
23
|
+
async function executionTurnParams(modelName: string, temperature: number | null | undefined) {
|
|
24
|
+
const { model } = await buildChatModel({
|
|
25
|
+
modelName,
|
|
26
|
+
proxyEndpoint: "http://127.0.0.1:9",
|
|
27
|
+
stigmerToken: "sampling-test",
|
|
28
|
+
temperature,
|
|
29
|
+
});
|
|
30
|
+
return (model as ChatAnthropic).invocationParams({});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe("the native execution turn's sampling parameters (real @langchain/anthropic)", () => {
|
|
34
|
+
beforeEach(() => {
|
|
35
|
+
_resetRegistryCache();
|
|
36
|
+
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("registry unreachable in this test"));
|
|
37
|
+
vi.spyOn(console, "warn").mockImplementation(() => {});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
_resetRegistryCache();
|
|
42
|
+
vi.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it.each(ADAPTIVE_ONLY_IDS)("%s: the pinned temperature 0 is refused before any request", async (id) => {
|
|
46
|
+
await expect(executionTurnParams(id, undefined)).rejects.toThrow(/temperature is not supported/);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it.each([...ADAPTIVE_ONLY_IDS, ...SAMPLING_TOLERANT_IDS])(
|
|
50
|
+
"%s: with no temperature the request builds and carries no sampling override",
|
|
51
|
+
async (id) => {
|
|
52
|
+
const params = await executionTurnParams(id, null);
|
|
53
|
+
expect(params).not.toHaveProperty("temperature");
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
});
|
|
@@ -156,6 +156,21 @@ describe("buildChatModel", () => {
|
|
|
156
156
|
expect(lastAnthropicArgs()).toMatchObject({ maxTokens: 4096 });
|
|
157
157
|
});
|
|
158
158
|
|
|
159
|
+
it("pins temperature 0 by default, and sends none when the caller passes null", async () => {
|
|
160
|
+
mockRegistryResponse([
|
|
161
|
+
{ id: "claude-opus-4.8", apiModelId: "claude-opus-4-8", provider: "anthropic" },
|
|
162
|
+
]);
|
|
163
|
+
|
|
164
|
+
await buildChatModel({ modelName: "claude-opus-4.8" });
|
|
165
|
+
expect(lastAnthropicArgs()).toMatchObject({ temperature: 0 });
|
|
166
|
+
|
|
167
|
+
await buildChatModel({ modelName: "claude-opus-4.8", temperature: 0.3 });
|
|
168
|
+
expect(lastAnthropicArgs()).toMatchObject({ temperature: 0.3 });
|
|
169
|
+
|
|
170
|
+
await buildChatModel({ modelName: "claude-opus-4.8", temperature: null });
|
|
171
|
+
expect(lastAnthropicArgs()).not.toHaveProperty("temperature");
|
|
172
|
+
});
|
|
173
|
+
|
|
159
174
|
it("passes the model id through unchanged when the registry is unavailable", async () => {
|
|
160
175
|
vi.spyOn(globalThis, "fetch").mockRejectedValueOnce(new Error("network down"));
|
|
161
176
|
|
|
@@ -54,8 +54,8 @@ describe("classifyTurnInterruption", () => {
|
|
|
54
54
|
expect(classifyTurnInterruption({ cancellationReason: undefined, shutdownSignalAborted: true })).toBe("none");
|
|
55
55
|
});
|
|
56
56
|
|
|
57
|
-
it("a workflow-requested cancellation without a shutdown signal is the orchestrator's
|
|
58
|
-
expect(classifyTurnInterruption({ cancellationReason: "CANCELLED", shutdownSignalAborted: false })).toBe("
|
|
57
|
+
it("a workflow-requested cancellation without a shutdown signal is the orchestrator's stop (a Pause or a Cancel, indistinguishable here)", () => {
|
|
58
|
+
expect(classifyTurnInterruption({ cancellationReason: "CANCELLED", shutdownSignalAborted: false })).toBe("orchestrator-stop");
|
|
59
59
|
});
|
|
60
60
|
|
|
61
61
|
it("a delivered cancellation WITH the shutdown signal aborted is a worker shutdown, not a pause (the #776 misclassification)", () => {
|
|
@@ -53,8 +53,16 @@ export interface BuildChatModelOptions {
|
|
|
53
53
|
mcpServerId?: string;
|
|
54
54
|
workflowExecutionId?: string;
|
|
55
55
|
};
|
|
56
|
-
/**
|
|
57
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Defaults to 0. `null` sends no sampling override at all, so the
|
|
58
|
+
* provider's default applies: the native execution turn passes it,
|
|
59
|
+
* because current Anthropic models refuse a temperature other than their
|
|
60
|
+
* default (claude-opus-4.7 and later, claude-sonnet-5, claude-fable-5;
|
|
61
|
+
* @langchain/anthropic throws before the request for some of them) and
|
|
62
|
+
* the Cursor harness sends none either (stigmer/stigmer#1341). Utility
|
|
63
|
+
* calls keep their deterministic 0.
|
|
64
|
+
*/
|
|
65
|
+
readonly temperature?: number | null;
|
|
58
66
|
/**
|
|
59
67
|
* Pass-through only; intentionally no default. Anthropic requires a value,
|
|
60
68
|
* but callers differ (setup omits it, call-llm uses 4096), so the default
|
|
@@ -258,7 +266,7 @@ export async function buildChatModel(opts: BuildChatModelOptions): Promise<Built
|
|
|
258
266
|
? { maxRetries: opts.maxRetries ?? 0 }
|
|
259
267
|
: {};
|
|
260
268
|
const common = {
|
|
261
|
-
temperature: opts.temperature ?? 0,
|
|
269
|
+
...(opts.temperature === null ? {} : { temperature: opts.temperature ?? 0 }),
|
|
262
270
|
apiKey,
|
|
263
271
|
...(maxTokens ? { maxTokens } : {}),
|
|
264
272
|
...maxRetries,
|