@bolt-foundry/gambit-core 0.8.5-rc.9 → 0.8.5

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.
Files changed (105) hide show
  1. package/README.md +6 -5
  2. package/cards/context.card.md +4 -4
  3. package/decks/anthropic/agent-sdk/PROMPT.md +9 -0
  4. package/decks/openai/codex-sdk/PROMPT.md +9 -0
  5. package/decks/openai/codex-sdk/codex_client.ts +109 -0
  6. package/decks/openai/codex-sdk/codex_sdk_bridge.deck.ts +36 -0
  7. package/esm/cards/context.card.md +9 -0
  8. package/esm/cards/end.card.md +10 -0
  9. package/esm/cards/generate-test-input.card.md +12 -0
  10. package/esm/cards/respond.card.md +10 -0
  11. package/esm/decks/anthropic/agent-sdk/PROMPT.md +9 -0
  12. package/esm/decks/openai/codex-sdk/PROMPT.md +9 -0
  13. package/esm/decks/openai/codex-sdk/codex_client.ts +109 -0
  14. package/esm/decks/openai/codex-sdk/codex_sdk_bridge.deck.ts +36 -0
  15. package/esm/mod.d.ts +1 -1
  16. package/esm/mod.d.ts.map +1 -1
  17. package/esm/schemas/graders/contexts/conversation.ts +40 -0
  18. package/esm/schemas/graders/contexts/conversation.zod.ts +1 -0
  19. package/esm/schemas/graders/contexts/conversation_tools.ts +63 -0
  20. package/esm/schemas/graders/contexts/conversation_tools.zod.ts +1 -0
  21. package/esm/schemas/graders/contexts/tools.ts +5 -0
  22. package/esm/schemas/graders/contexts/tools.zod.ts +1 -0
  23. package/esm/schemas/graders/contexts/turn.ts +17 -0
  24. package/esm/schemas/graders/contexts/turn.zod.ts +1 -0
  25. package/esm/schemas/graders/contexts/turn_tools.ts +63 -0
  26. package/esm/schemas/graders/contexts/turn_tools.zod.ts +1 -0
  27. package/esm/schemas/graders/grader_output.ts +15 -0
  28. package/esm/schemas/graders/grader_output.zod.ts +1 -0
  29. package/esm/schemas/graders/respond.ts +19 -0
  30. package/esm/schemas/graders/respond.zod.ts +1 -0
  31. package/esm/schemas/scenarios/plain_chat_input_optional.ts +6 -0
  32. package/esm/schemas/scenarios/plain_chat_input_optional.zod.ts +1 -0
  33. package/esm/schemas/scenarios/plain_chat_output.ts +5 -0
  34. package/esm/schemas/scenarios/plain_chat_output.zod.ts +1 -0
  35. package/esm/snippets/context.md +8 -0
  36. package/esm/snippets/end.md +10 -0
  37. package/esm/snippets/generate-test-input.md +12 -0
  38. package/esm/snippets/init.md +12 -0
  39. package/esm/snippets/respond.md +10 -0
  40. package/esm/snippets/scenario-participant.md +10 -0
  41. package/esm/src/constants.d.ts +0 -1
  42. package/esm/src/constants.d.ts.map +1 -1
  43. package/esm/src/constants.js +0 -4
  44. package/esm/src/loader.d.ts.map +1 -1
  45. package/esm/src/loader.js +101 -0
  46. package/esm/src/markdown.d.ts.map +1 -1
  47. package/esm/src/markdown.js +109 -9
  48. package/esm/src/runtime.d.ts +16 -1
  49. package/esm/src/runtime.d.ts.map +1 -1
  50. package/esm/src/runtime.js +1607 -311
  51. package/esm/src/types.d.ts +25 -1
  52. package/esm/src/types.d.ts.map +1 -1
  53. package/package.json +1 -1
  54. package/script/cards/context.card.md +9 -0
  55. package/script/cards/end.card.md +10 -0
  56. package/script/cards/generate-test-input.card.md +12 -0
  57. package/script/cards/respond.card.md +10 -0
  58. package/script/decks/anthropic/agent-sdk/PROMPT.md +9 -0
  59. package/script/decks/openai/codex-sdk/PROMPT.md +9 -0
  60. package/script/decks/openai/codex-sdk/codex_client.ts +109 -0
  61. package/script/decks/openai/codex-sdk/codex_sdk_bridge.deck.ts +36 -0
  62. package/script/mod.d.ts +1 -1
  63. package/script/mod.d.ts.map +1 -1
  64. package/script/schemas/graders/contexts/conversation.ts +40 -0
  65. package/script/schemas/graders/contexts/conversation.zod.ts +1 -0
  66. package/script/schemas/graders/contexts/conversation_tools.ts +63 -0
  67. package/script/schemas/graders/contexts/conversation_tools.zod.ts +1 -0
  68. package/script/schemas/graders/contexts/tools.ts +5 -0
  69. package/script/schemas/graders/contexts/tools.zod.ts +1 -0
  70. package/script/schemas/graders/contexts/turn.ts +17 -0
  71. package/script/schemas/graders/contexts/turn.zod.ts +1 -0
  72. package/script/schemas/graders/contexts/turn_tools.ts +63 -0
  73. package/script/schemas/graders/contexts/turn_tools.zod.ts +1 -0
  74. package/script/schemas/graders/grader_output.ts +15 -0
  75. package/script/schemas/graders/grader_output.zod.ts +1 -0
  76. package/script/schemas/graders/respond.ts +19 -0
  77. package/script/schemas/graders/respond.zod.ts +1 -0
  78. package/script/schemas/scenarios/plain_chat_input_optional.ts +6 -0
  79. package/script/schemas/scenarios/plain_chat_input_optional.zod.ts +1 -0
  80. package/script/schemas/scenarios/plain_chat_output.ts +5 -0
  81. package/script/schemas/scenarios/plain_chat_output.zod.ts +1 -0
  82. package/script/snippets/context.md +8 -0
  83. package/script/snippets/end.md +10 -0
  84. package/script/snippets/generate-test-input.md +12 -0
  85. package/script/snippets/init.md +12 -0
  86. package/script/snippets/respond.md +10 -0
  87. package/script/snippets/scenario-participant.md +10 -0
  88. package/script/src/constants.d.ts +0 -1
  89. package/script/src/constants.d.ts.map +1 -1
  90. package/script/src/constants.js +1 -5
  91. package/script/src/loader.d.ts.map +1 -1
  92. package/script/src/loader.js +101 -0
  93. package/script/src/markdown.d.ts.map +1 -1
  94. package/script/src/markdown.js +109 -9
  95. package/script/src/runtime.d.ts +16 -1
  96. package/script/src/runtime.d.ts.map +1 -1
  97. package/script/src/runtime.js +1606 -310
  98. package/script/src/types.d.ts +25 -1
  99. package/script/src/types.d.ts.map +1 -1
  100. package/snippets/context.md +8 -0
  101. package/snippets/end.md +10 -0
  102. package/snippets/generate-test-input.md +12 -0
  103. package/snippets/init.md +12 -0
  104. package/snippets/respond.md +10 -0
  105. package/snippets/scenario-participant.md +10 -0
@@ -1,6 +1,6 @@
1
1
  import * as dntShim from "../_dnt.shims.js";
2
2
  import * as path from "../deps/jsr.io/@std/path/1.1.4/mod.js";
3
- import { DEFAULT_GUARDRAILS, DEFAULT_STATUS_DELAY_MS, GAMBIT_TOOL_COMPLETE, GAMBIT_TOOL_CONTEXT, GAMBIT_TOOL_END, GAMBIT_TOOL_INIT, GAMBIT_TOOL_RESPOND, } from "./constants.js";
3
+ import { DEFAULT_GUARDRAILS, DEFAULT_STATUS_DELAY_MS, GAMBIT_TOOL_CONTEXT, GAMBIT_TOOL_END, GAMBIT_TOOL_INIT, GAMBIT_TOOL_RESPOND, } from "./constants.js";
4
4
  import { loadDeck } from "./loader.js";
5
5
  import { canReadPath, canRunCommand, canRunPath, canWritePath, intersectPermissions, resolveEffectivePermissions, } from "./permissions.js";
6
6
  import { ExecToolUnsupportedHostError, executeBuiltinCommand, } from "./runtime_exec_host.js";
@@ -21,6 +21,7 @@ const WORKER_SANDBOX_ENV = "GAMBIT_DECK_WORKER_SANDBOX";
21
21
  const WORKER_TIMEOUT_MESSAGE = "Timeout exceeded";
22
22
  const RUN_CANCELED_MESSAGE = "Run canceled";
23
23
  const WORKER_SANDBOX_SIGNAL_UNSUPPORTED_MESSAGE = "workerSandbox is unsupported when `signal` is provided.";
24
+ const INTERMEDIATE_OUTPUT_DISALLOWED_CODE = "intermediate_output_disallowed";
24
25
  const INSPECT_WORKER_TIMEOUT_MS = 1_500;
25
26
  const INSPECT_WORKER_TIMEOUT_MESSAGE = "Deck inspection timed out";
26
27
  const BUILTIN_TOOL_READ_FILE = "read_file";
@@ -28,12 +29,21 @@ const BUILTIN_TOOL_LIST_DIR = "list_dir";
28
29
  const BUILTIN_TOOL_GREP_FILES = "grep_files";
29
30
  const BUILTIN_TOOL_APPLY_PATCH = "apply_patch";
30
31
  const BUILTIN_TOOL_EXEC = "exec";
32
+ const BUILTIN_TOOL_EMIT_OUTPUT_ITEM = "gambit_emit_output_item";
33
+ const BUILTIN_TOOL_CONSUME_ASYNC_ACTION = "gambit_consume_async_action";
34
+ const ASYNC_ACTION_JOBS_META_KEY = "gambit_async_action_jobs_v1";
35
+ const ASYNC_ACTION_JOBS_META_VERSION = 1;
36
+ const ASYNC_ACTION_JOB_INTERRUPTED_CODE = "async_action_interrupted";
37
+ const ASYNC_ACTION_JOB_MAX_WAIT_MS = 5000;
38
+ const ASYNC_ACTION_JOB_MAX_EVENTS = 512;
31
39
  const BUILTIN_TOOL_NAMES = new Set([
32
40
  BUILTIN_TOOL_READ_FILE,
33
41
  BUILTIN_TOOL_LIST_DIR,
34
42
  BUILTIN_TOOL_GREP_FILES,
35
43
  BUILTIN_TOOL_APPLY_PATCH,
36
44
  BUILTIN_TOOL_EXEC,
45
+ BUILTIN_TOOL_EMIT_OUTPUT_ITEM,
46
+ BUILTIN_TOOL_CONSUME_ASYNC_ACTION,
37
47
  ]);
38
48
  const TRUSTED_SCHEMA_IMPORT_PREFIXES = [
39
49
  "@bolt-foundry/gambit-core/schemas",
@@ -166,7 +176,10 @@ function normalizePermissionBaseDir(set, baseDir) {
166
176
  };
167
177
  }
168
178
  function deadlineForRun(guardrails, existing) {
169
- const timeoutDeadline = performance.now() + guardrails.timeoutMs;
179
+ const timeoutMs = guardrails.timeoutMs === 0
180
+ ? Number.POSITIVE_INFINITY
181
+ : guardrails.timeoutMs;
182
+ const timeoutDeadline = performance.now() + timeoutMs;
170
183
  if (typeof existing === "number" && Number.isFinite(existing)) {
171
184
  return Math.min(existing, timeoutDeadline);
172
185
  }
@@ -193,6 +206,22 @@ function ensureRunActive(deadlineMs, signal) {
193
206
  throwIfCanceled(signal);
194
207
  ensureNotExpired(deadlineMs);
195
208
  }
209
+ function mergeAbortSignals(signalA, signalB) {
210
+ if (!signalA)
211
+ return signalB;
212
+ if (!signalB)
213
+ return signalA;
214
+ if (signalA.aborted || signalB.aborted) {
215
+ const controller = new AbortController();
216
+ controller.abort();
217
+ return controller.signal;
218
+ }
219
+ const controller = new AbortController();
220
+ const abort = () => controller.abort();
221
+ signalA.addEventListener("abort", abort, { once: true });
222
+ signalB.addEventListener("abort", abort, { once: true });
223
+ return controller.signal;
224
+ }
196
225
  function isTrustedSchemaImportKey(key) {
197
226
  const normalized = key.trim();
198
227
  if (!normalized)
@@ -329,7 +358,7 @@ export async function runDeck(opts) {
329
358
  if (workerSandboxRequested && !isWorkerSandboxHostSupported()) {
330
359
  throw new WorkerSandboxUnsupportedHostError();
331
360
  }
332
- if (workerSandboxRequested && opts.signal) {
361
+ if (workerSandboxRequested && opts.signal && inferredRoot) {
333
362
  throw new WorkerSandboxSignalUnsupportedError();
334
363
  }
335
364
  const workerSandbox = workerSandboxRequested;
@@ -425,6 +454,9 @@ export async function runDeck(opts) {
425
454
  responsesMode: opts.responsesMode,
426
455
  permissions: permissions.effective,
427
456
  permissionsTrace: permissions.trace,
457
+ intermediateOutputAllow: opts.intermediateOutputAllow,
458
+ onIntermediateOutputItem: opts.onIntermediateOutputItem,
459
+ intermediateOutputErrorContext: opts.intermediateOutputErrorContext,
428
460
  workspacePermissions: opts.workspacePermissions,
429
461
  workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
430
462
  sessionPermissions: opts.sessionPermissions,
@@ -456,6 +488,9 @@ export async function runDeck(opts) {
456
488
  responsesMode: opts.responsesMode,
457
489
  permissions: permissions.effective,
458
490
  permissionsTrace: permissions.trace,
491
+ intermediateOutputAllow: opts.intermediateOutputAllow,
492
+ onIntermediateOutputItem: opts.onIntermediateOutputItem,
493
+ intermediateOutputErrorContext: opts.intermediateOutputErrorContext,
459
494
  workspacePermissions: opts.workspacePermissions,
460
495
  workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
461
496
  sessionPermissions: opts.sessionPermissions,
@@ -501,6 +536,10 @@ export async function runDeck(opts) {
501
536
  };
502
537
  const runDeadlineMs = deadlineForRun(effectiveGuardrails, opts.runDeadlineMs);
503
538
  ensureRunActive(runDeadlineMs, opts.signal);
539
+ const hasModelParams = Boolean(deck.modelParams?.model || deck.modelParams?.temperature !== undefined);
540
+ if (hasModelParams) {
541
+ assertLegacySyntheticResponseToolsRemoved(deck);
542
+ }
504
543
  ensureSchemaPresence(deck, isRoot);
505
544
  const resolvedInput = resolveInput({
506
545
  deck,
@@ -514,7 +553,7 @@ export async function runDeck(opts) {
514
553
  !opts.inOrchestrationWorker &&
515
554
  isRoot &&
516
555
  !opts.onTool &&
517
- Boolean(deck.modelParams?.model || deck.modelParams?.temperature !== undefined);
556
+ hasModelParams;
518
557
  if (useOrchestrationWorker) {
519
558
  return await runLlmDeckInWorker({
520
559
  deckPath: deck.path,
@@ -536,6 +575,9 @@ export async function runDeck(opts) {
536
575
  responsesMode: opts.responsesMode,
537
576
  permissions: permissions.effective,
538
577
  permissionsTrace: permissions.trace,
578
+ intermediateOutputAllow: opts.intermediateOutputAllow,
579
+ onIntermediateOutputItem: opts.onIntermediateOutputItem,
580
+ intermediateOutputErrorContext: opts.intermediateOutputErrorContext,
539
581
  workspacePermissions: opts.workspacePermissions,
540
582
  workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
541
583
  sessionPermissions: opts.sessionPermissions,
@@ -558,7 +600,7 @@ export async function runDeck(opts) {
558
600
  permissions: permissions.trace,
559
601
  });
560
602
  }
561
- if (deck.modelParams?.model || deck.modelParams?.temperature !== undefined) {
603
+ if (hasModelParams) {
562
604
  return await runLlmDeck({
563
605
  deck,
564
606
  guardrails: effectiveGuardrails,
@@ -579,6 +621,9 @@ export async function runDeck(opts) {
579
621
  responsesMode: opts.responsesMode,
580
622
  permissions: permissions.effective,
581
623
  permissionsTrace: permissions.trace,
624
+ intermediateOutputAllow: opts.intermediateOutputAllow,
625
+ onIntermediateOutputItem: opts.onIntermediateOutputItem,
626
+ intermediateOutputErrorContext: opts.intermediateOutputErrorContext,
582
627
  workspacePermissions: opts.workspacePermissions,
583
628
  workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
584
629
  sessionPermissions: opts.sessionPermissions,
@@ -611,6 +656,9 @@ export async function runDeck(opts) {
611
656
  responsesMode: opts.responsesMode,
612
657
  permissions: permissions.effective,
613
658
  permissionsTrace: permissions.trace,
659
+ intermediateOutputAllow: opts.intermediateOutputAllow,
660
+ onIntermediateOutputItem: opts.onIntermediateOutputItem,
661
+ intermediateOutputErrorContext: opts.intermediateOutputErrorContext,
614
662
  workspacePermissions: opts.workspacePermissions,
615
663
  workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
616
664
  sessionPermissions: opts.sessionPermissions,
@@ -686,10 +734,75 @@ function resolveContextSchema(deck) {
686
734
  function resolveResponseSchema(deck) {
687
735
  return deck.responseSchema ?? deck.outputSchema;
688
736
  }
737
+ function toResponseTextConfig(deck) {
738
+ const responseSchema = resolveResponseSchema(deck);
739
+ if (!responseSchema)
740
+ return undefined;
741
+ const schema = toJsonSchema(responseSchema);
742
+ if (!isStructuredResponseSchema(schema))
743
+ return undefined;
744
+ return {
745
+ format: {
746
+ type: "json_schema",
747
+ name: responseSchemaName(deck),
748
+ schema,
749
+ strict: true,
750
+ },
751
+ };
752
+ }
753
+ function isStructuredResponseSchema(schema) {
754
+ const type = schema.type;
755
+ if (typeof type === "string") {
756
+ return type !== "string";
757
+ }
758
+ if (Array.isArray(type)) {
759
+ return !type.every((entry) => entry === "string");
760
+ }
761
+ const properties = schema.properties;
762
+ if (properties && typeof properties === "object" && !Array.isArray(properties)) {
763
+ return true;
764
+ }
765
+ return false;
766
+ }
767
+ function responseSchemaName(deck) {
768
+ const raw = deck.label ??
769
+ path.basename(deck.path, path.extname(deck.path)) ??
770
+ "gambit_response";
771
+ const normalized = raw.toLowerCase().replace(/[^a-z0-9_]+/g, "_").replace(/^_+|_+$/g, "").slice(0, 64);
772
+ return normalized || "gambit_response";
773
+ }
774
+ const REMOVED_LEGACY_RESPONSE_TOOL_NAMES = new Set([
775
+ GAMBIT_TOOL_RESPOND,
776
+ GAMBIT_TOOL_END,
777
+ ]);
778
+ const LEGACY_RESPONSE_TOOL_MIGRATION_GUIDANCE = [
779
+ `Legacy synthetic response tools (${Array.from(REMOVED_LEGACY_RESPONSE_TOOL_NAMES).join(", ")}) are removed.`,
780
+ "Remove `respond`/`allowEnd` from deck metadata and remove any `gambit://respond`, `gambit://end`, `gambit://snippets/respond.md`, or `gambit://snippets/end.md` embeds.",
781
+ "Return normal assistant output that matches `responseSchema` instead of synthetic completion tools.",
782
+ ].join(" ");
783
+ function legacyResponseToolMigrationError(details) {
784
+ return new Error(`[gambit] ${details} ${LEGACY_RESPONSE_TOOL_MIGRATION_GUIDANCE}`);
785
+ }
786
+ function syntheticContextToolInvocationError(deckPath) {
787
+ return new Error(`[gambit] Model emitted synthetic context tool for deck ${deckPath}. gambit_context is injected automatically when context input is provided. Do not call gambit_context or gambit_init from the model.`);
788
+ }
789
+ function assertLegacySyntheticResponseToolsRemoved(deck) {
790
+ if (!deck.respond && !deck.allowEnd)
791
+ return;
792
+ const enabled = [];
793
+ if (deck.respond)
794
+ enabled.push("respond");
795
+ if (deck.allowEnd)
796
+ enabled.push("allowEnd");
797
+ throw legacyResponseToolMigrationError(`Deck ${deck.path} enables removed synthetic response controls (${enabled.join(", ")}).`);
798
+ }
689
799
  function isContextToolName(name) {
690
800
  return name === GAMBIT_TOOL_CONTEXT || name === GAMBIT_TOOL_INIT;
691
801
  }
692
802
  function ensureSchemaPresence(deck, isRoot) {
803
+ for (const extension of deck.responseItemExtensions ?? []) {
804
+ assertZodSchema(extension.dataSchema, `responseItemExtensions["${extension.type}"].dataSchema`);
805
+ }
693
806
  if (!isRoot) {
694
807
  const contextSchema = resolveContextSchema(deck);
695
808
  const responseSchema = resolveResponseSchema(deck);
@@ -861,6 +974,132 @@ function responseItemsFromMessages(messages) {
861
974
  }
862
975
  return items;
863
976
  }
977
+ const CORE_RESPONSE_ITEM_TYPES = new Set([
978
+ "message",
979
+ "function_call",
980
+ "function_call_output",
981
+ "reasoning",
982
+ ]);
983
+ function isCoreResponseItemType(type) {
984
+ return CORE_RESPONSE_ITEM_TYPES.has(type);
985
+ }
986
+ function isResponseExtensionItem(item) {
987
+ return item.type.includes(":");
988
+ }
989
+ function canonicalizeJsonValue(value) {
990
+ if (value === null)
991
+ return null;
992
+ if (typeof value === "string" || typeof value === "boolean")
993
+ return value;
994
+ if (typeof value === "number") {
995
+ return Number.isFinite(value) ? value : String(value);
996
+ }
997
+ if (Array.isArray(value)) {
998
+ return value.map((entry) => canonicalizeJsonValue(entry));
999
+ }
1000
+ if (value && typeof value === "object") {
1001
+ const record = value;
1002
+ const sortedKeys = Object.keys(record).sort((a, b) => a.localeCompare(b));
1003
+ const out = {};
1004
+ for (const key of sortedKeys) {
1005
+ const next = record[key];
1006
+ if (next === undefined)
1007
+ continue;
1008
+ out[key] = canonicalizeJsonValue(next);
1009
+ }
1010
+ return out;
1011
+ }
1012
+ return String(value);
1013
+ }
1014
+ function createResponseItemEmissionValidator(deck) {
1015
+ const schemaByType = new Map();
1016
+ for (const extension of deck.responseItemExtensions ?? []) {
1017
+ schemaByType.set(extension.type, extension.dataSchema);
1018
+ }
1019
+ return (item, source) => {
1020
+ const type = item.type;
1021
+ if (isCoreResponseItemType(type))
1022
+ return item;
1023
+ if (!type.includes(":")) {
1024
+ throw new Error(`[gambit] Deck ${deck.path} emitted undeclared non-namespaced response extension item type "${type}" (${source}).`);
1025
+ }
1026
+ const schema = schemaByType.get(type);
1027
+ if (!schema) {
1028
+ throw new Error(`[gambit] Deck ${deck.path} emitted undeclared response extension item type "${type}" (${source}). Declare responseItemExtensions with dataSchema.`);
1029
+ }
1030
+ const asRecord = item;
1031
+ if (!Object.hasOwn(asRecord, "data")) {
1032
+ throw new Error(`[gambit] Deck ${deck.path} emitted extension item "${type}" without data payload (${source}).`);
1033
+ }
1034
+ const data = validateWithSchema(schema, asRecord.data);
1035
+ return {
1036
+ type: type,
1037
+ id: typeof asRecord.id === "string" ? asRecord.id : undefined,
1038
+ data: canonicalizeJsonValue(data),
1039
+ };
1040
+ };
1041
+ }
1042
+ function validateResponseOutputItems(items, validateItem, source) {
1043
+ return items.map((item, index) => validateItem(item, `${source}[${index}]`));
1044
+ }
1045
+ function validateResponseEventItems(event, validateItem) {
1046
+ if (event.type === "response.output_item.added" ||
1047
+ event.type === "response.output_item.done") {
1048
+ return {
1049
+ ...event,
1050
+ item: validateItem(event.item, `${event.type}[${event.output_index}]`),
1051
+ };
1052
+ }
1053
+ if (event.type === "response.completed" || event.type === "response.created") {
1054
+ return {
1055
+ ...event,
1056
+ response: {
1057
+ ...event.response,
1058
+ output: validateResponseOutputItems(event.response.output ?? [], validateItem, `${event.type}.response.output`),
1059
+ },
1060
+ };
1061
+ }
1062
+ return event;
1063
+ }
1064
+ function isSupplementalResponseItem(item) {
1065
+ return item.type === "reasoning" || !isCoreResponseItemType(item.type);
1066
+ }
1067
+ function supplementalResponseItemSignature(item) {
1068
+ if (item.type === "reasoning") {
1069
+ const content = (item.content ?? []).map((part) => `${part.type}:${part.text}`).join("|");
1070
+ const summary = item.summary.map((part) => `${part.type}:${part.text}`)
1071
+ .join("|");
1072
+ return [
1073
+ "reasoning",
1074
+ item.id ?? "",
1075
+ content,
1076
+ summary,
1077
+ item.encrypted_content ?? "",
1078
+ ].join(":");
1079
+ }
1080
+ if (isResponseExtensionItem(item)) {
1081
+ return [
1082
+ item.type,
1083
+ item.id ?? "",
1084
+ JSON.stringify(item.data),
1085
+ ].join(":");
1086
+ }
1087
+ return "";
1088
+ }
1089
+ function mergeSupplementalResponseItems(prior, latest) {
1090
+ const out = [];
1091
+ const seen = new Set();
1092
+ for (const item of [...prior, ...latest]) {
1093
+ if (!isSupplementalResponseItem(item))
1094
+ continue;
1095
+ const signature = supplementalResponseItemSignature(item);
1096
+ if (!signature || seen.has(signature))
1097
+ continue;
1098
+ seen.add(signature);
1099
+ out.push(item);
1100
+ }
1101
+ return out;
1102
+ }
864
1103
  function safeJsonArgs(value) {
865
1104
  if (!value)
866
1105
  return {};
@@ -882,13 +1121,62 @@ function asToolKind(value, fallback) {
882
1121
  }
883
1122
  return fallback;
884
1123
  }
1124
+ function normalizeStreamToolEventType(type) {
1125
+ if (type === "tool.call" || type === "gambit:tool.call")
1126
+ return "tool.call";
1127
+ if (type === "tool.result" || type === "gambit:tool.result") {
1128
+ return "tool.result";
1129
+ }
1130
+ return undefined;
1131
+ }
1132
+ function isTerminalResponseEventType(type) {
1133
+ return type === "response.completed" || type === "response.failed";
1134
+ }
1135
+ function normalizeSequenceNumber(value) {
1136
+ if (typeof value !== "number")
1137
+ return undefined;
1138
+ if (!Number.isInteger(value) || value < 0)
1139
+ return undefined;
1140
+ return value;
1141
+ }
1142
+ function createCanonicalStreamEventController() {
1143
+ let nextSequenceNumber = 0;
1144
+ let terminalStateReached = false;
1145
+ return (streamEvent) => {
1146
+ const type = typeof streamEvent.type === "string" ? streamEvent.type : "";
1147
+ if (!type)
1148
+ return streamEvent;
1149
+ const isResponseEvent = type.startsWith("response.");
1150
+ const isToolEvent = normalizeStreamToolEventType(type) !== undefined;
1151
+ if (!isResponseEvent && !isToolEvent)
1152
+ return streamEvent;
1153
+ if (terminalStateReached)
1154
+ return null;
1155
+ if (!isResponseEvent)
1156
+ return streamEvent;
1157
+ const nextEvent = { ...streamEvent, type };
1158
+ const existingSequenceNumber = normalizeSequenceNumber(streamEvent.sequence_number);
1159
+ if (existingSequenceNumber === undefined) {
1160
+ nextSequenceNumber += 1;
1161
+ nextEvent.sequence_number = nextSequenceNumber;
1162
+ }
1163
+ else {
1164
+ nextSequenceNumber = Math.max(nextSequenceNumber, existingSequenceNumber);
1165
+ }
1166
+ if (isTerminalResponseEventType(type)) {
1167
+ terminalStateReached = true;
1168
+ }
1169
+ return nextEvent;
1170
+ };
1171
+ }
885
1172
  function projectStreamToolTraceEvents(input) {
886
1173
  if (!input.trace)
887
1174
  return;
888
- const type = typeof input.streamEvent.type === "string"
1175
+ const rawType = typeof input.streamEvent.type === "string"
889
1176
  ? input.streamEvent.type
890
1177
  : "";
891
- if (type !== "tool.call" && type !== "tool.result")
1178
+ const type = normalizeStreamToolEventType(rawType);
1179
+ if (!type)
892
1180
  return;
893
1181
  const actionCallId = typeof input.streamEvent.actionCallId === "string"
894
1182
  ? input.streamEvent.actionCallId
@@ -962,6 +1250,682 @@ function traceOpenResponsesStreamEvent(input) {
962
1250
  });
963
1251
  return true;
964
1252
  }
1253
+ function createIntermediateOutputDisallowedError(input) {
1254
+ const action = typeof input.context?.actionName === "string" &&
1255
+ input.context.actionName.trim().length > 0
1256
+ ? `action "${input.context.actionName.trim()}"`
1257
+ : "action";
1258
+ const deckSuffix = typeof input.context?.parentDeckPath === "string" &&
1259
+ input.context.parentDeckPath.trim().length > 0
1260
+ ? ` in ${input.context.parentDeckPath.trim()}`
1261
+ : "";
1262
+ const error = new Error(`[gambit] ${action}${deckSuffix} disallows intermediate output emission (${input.source}). Set action.intermediateOutput.emit = "allow" to opt in.`);
1263
+ error.code = INTERMEDIATE_OUTPUT_DISALLOWED_CODE;
1264
+ return error;
1265
+ }
1266
+ function createIntermediateChildResponseEmitter(input) {
1267
+ const responseId = `${input.actionCallId}:intermediate`;
1268
+ const canonicalizeStreamEvent = createCanonicalStreamEventController();
1269
+ const emittedItems = [];
1270
+ let nextOutputIndex = 0;
1271
+ let created = false;
1272
+ let terminal = false;
1273
+ const emitStreamEvent = (event) => {
1274
+ const canonical = canonicalizeStreamEvent(event);
1275
+ if (!canonical)
1276
+ return;
1277
+ traceOpenResponsesStreamEvent({
1278
+ streamEvent: canonical,
1279
+ runId: input.runId,
1280
+ actionCallId: input.actionCallId,
1281
+ deckPath: input.deckPath,
1282
+ parentActionCallId: input.parentActionCallId,
1283
+ trace: input.trace,
1284
+ });
1285
+ };
1286
+ const ensureCreated = () => {
1287
+ if (created)
1288
+ return;
1289
+ created = true;
1290
+ emitStreamEvent({
1291
+ type: "response.created",
1292
+ response: {
1293
+ id: responseId,
1294
+ object: "response",
1295
+ status: "in_progress",
1296
+ output: [],
1297
+ },
1298
+ });
1299
+ };
1300
+ return {
1301
+ emitOutputItem: (item, source) => {
1302
+ input.ensureActive();
1303
+ if (input.allowEmission === false) {
1304
+ throw createIntermediateOutputDisallowedError({
1305
+ source,
1306
+ context: input.errorContext,
1307
+ });
1308
+ }
1309
+ if (terminal)
1310
+ return;
1311
+ const validated = input.validateItem(item, source);
1312
+ ensureCreated();
1313
+ const outputIndex = nextOutputIndex++;
1314
+ emittedItems.push(validated);
1315
+ emitStreamEvent({
1316
+ type: "response.output_item.added",
1317
+ response_id: responseId,
1318
+ output_index: outputIndex,
1319
+ item: validated,
1320
+ });
1321
+ emitStreamEvent({
1322
+ type: "response.output_item.done",
1323
+ response_id: responseId,
1324
+ output_index: outputIndex,
1325
+ item: validated,
1326
+ });
1327
+ input.onEmit?.({
1328
+ responseId,
1329
+ actionCallId: input.actionCallId,
1330
+ parentActionCallId: input.parentActionCallId,
1331
+ deckPath: input.deckPath,
1332
+ outputIndex,
1333
+ item: validated,
1334
+ });
1335
+ },
1336
+ complete: () => {
1337
+ input.ensureActive();
1338
+ if (terminal || !created) {
1339
+ terminal = true;
1340
+ return;
1341
+ }
1342
+ emitStreamEvent({
1343
+ type: "response.completed",
1344
+ response: {
1345
+ id: responseId,
1346
+ object: "response",
1347
+ status: "completed",
1348
+ output: emittedItems,
1349
+ },
1350
+ });
1351
+ terminal = true;
1352
+ },
1353
+ fail: (err) => {
1354
+ if (terminal || !created) {
1355
+ terminal = true;
1356
+ return;
1357
+ }
1358
+ const code = typeof err?.code === "string"
1359
+ ? err.code
1360
+ : undefined;
1361
+ const message = err instanceof Error ? err.message : String(err);
1362
+ emitStreamEvent({
1363
+ type: "response.failed",
1364
+ response_id: responseId,
1365
+ error: {
1366
+ ...(code ? { code } : {}),
1367
+ message,
1368
+ },
1369
+ });
1370
+ terminal = true;
1371
+ },
1372
+ };
1373
+ }
1374
+ function parseIntermediateOutputEmissionFromTraceEvent(input) {
1375
+ if (input.event.type !== "response.output_item.done")
1376
+ return undefined;
1377
+ const eventRecord = input.event;
1378
+ const responseId = typeof eventRecord.response_id === "string"
1379
+ ? eventRecord.response_id
1380
+ : undefined;
1381
+ if (!responseId || !responseId.endsWith(":intermediate"))
1382
+ return undefined;
1383
+ const outputIndex = eventRecord.output_index;
1384
+ if (!Number.isInteger(outputIndex) || Number(outputIndex) < 0) {
1385
+ return undefined;
1386
+ }
1387
+ const item = eventRecord.item;
1388
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1389
+ return undefined;
1390
+ }
1391
+ const gambitMeta = eventRecord._gambit && typeof eventRecord._gambit ===
1392
+ "object" &&
1393
+ !Array.isArray(eventRecord._gambit)
1394
+ ? eventRecord._gambit
1395
+ : {};
1396
+ const actionCallId = typeof gambitMeta.action_call_id === "string"
1397
+ ? gambitMeta.action_call_id
1398
+ : undefined;
1399
+ if (!actionCallId)
1400
+ return undefined;
1401
+ const parentActionCallId = typeof gambitMeta.parent_action_call_id ===
1402
+ "string"
1403
+ ? gambitMeta.parent_action_call_id
1404
+ : undefined;
1405
+ if (input.expectedParentActionCallId !== undefined &&
1406
+ parentActionCallId !== input.expectedParentActionCallId) {
1407
+ return undefined;
1408
+ }
1409
+ const deckPath = typeof gambitMeta.deck_path === "string"
1410
+ ? gambitMeta.deck_path
1411
+ : "";
1412
+ if (!deckPath)
1413
+ return undefined;
1414
+ return {
1415
+ responseId,
1416
+ actionCallId,
1417
+ parentActionCallId,
1418
+ deckPath,
1419
+ outputIndex: Number(outputIndex),
1420
+ item: item,
1421
+ };
1422
+ }
1423
+ function cloneAsyncActionTerminalResult(input) {
1424
+ return {
1425
+ state: input.state,
1426
+ status: input.status,
1427
+ payload: input.payload,
1428
+ message: input.message,
1429
+ code: input.code,
1430
+ meta: input.meta ? { ...input.meta } : undefined,
1431
+ };
1432
+ }
1433
+ function cloneIntermediateEmission(emission) {
1434
+ return {
1435
+ responseId: emission.responseId,
1436
+ actionCallId: emission.actionCallId,
1437
+ parentActionCallId: emission.parentActionCallId,
1438
+ deckPath: emission.deckPath,
1439
+ outputIndex: emission.outputIndex,
1440
+ item: emission.item,
1441
+ };
1442
+ }
1443
+ function cloneAsyncActionJobEvent(event) {
1444
+ if (event.type === "intermediate_output") {
1445
+ return {
1446
+ cursor: event.cursor,
1447
+ type: "intermediate_output",
1448
+ emission: cloneIntermediateEmission(event.emission),
1449
+ };
1450
+ }
1451
+ return {
1452
+ cursor: event.cursor,
1453
+ type: "terminal",
1454
+ terminal: cloneAsyncActionTerminalResult(event.terminal),
1455
+ };
1456
+ }
1457
+ function parsePersistedAsyncActionJobs(state) {
1458
+ const meta = state?.meta;
1459
+ const raw = meta?.[ASYNC_ACTION_JOBS_META_KEY];
1460
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
1461
+ return undefined;
1462
+ const asRecord = raw;
1463
+ const version = Number.isInteger(asRecord.version)
1464
+ ? Number(asRecord.version)
1465
+ : undefined;
1466
+ if (version !== ASYNC_ACTION_JOBS_META_VERSION)
1467
+ return undefined;
1468
+ const rawJobs = asRecord.jobs;
1469
+ if (!Array.isArray(rawJobs))
1470
+ return undefined;
1471
+ const jobs = [];
1472
+ for (const rawJob of rawJobs) {
1473
+ if (!rawJob || typeof rawJob !== "object" || Array.isArray(rawJob)) {
1474
+ continue;
1475
+ }
1476
+ const rec = rawJob;
1477
+ const jobId = typeof rec.jobId === "string" ? rec.jobId : "";
1478
+ const actionName = typeof rec.actionName === "string" ? rec.actionName : "";
1479
+ const actionPath = typeof rec.actionPath === "string" ? rec.actionPath : "";
1480
+ const stateValue = typeof rec.state === "string" ? rec.state : "";
1481
+ if (!jobId || !actionName || !actionPath)
1482
+ continue;
1483
+ if (stateValue !== "running" && stateValue !== "completed" &&
1484
+ stateValue !== "failed" && stateValue !== "canceled") {
1485
+ continue;
1486
+ }
1487
+ const parsedState = stateValue;
1488
+ const cursorBase = Number.isInteger(rec.cursorBase)
1489
+ ? Math.max(0, Number(rec.cursorBase))
1490
+ : 0;
1491
+ const nextCursor = Number.isInteger(rec.nextCursor)
1492
+ ? Math.max(cursorBase, Number(rec.nextCursor))
1493
+ : cursorBase;
1494
+ const expectedCursor = Number.isInteger(rec.expectedCursor)
1495
+ ? Math.max(cursorBase, Math.min(nextCursor, Number(rec.expectedCursor)))
1496
+ : cursorBase;
1497
+ const rawEvents = Array.isArray(rec.events) ? rec.events : [];
1498
+ const events = [];
1499
+ for (const rawEvent of rawEvents) {
1500
+ if (!rawEvent || typeof rawEvent !== "object" || Array.isArray(rawEvent)) {
1501
+ continue;
1502
+ }
1503
+ const eventRec = rawEvent;
1504
+ const cursor = Number.isInteger(eventRec.cursor)
1505
+ ? Number(eventRec.cursor)
1506
+ : NaN;
1507
+ if (!Number.isInteger(cursor) || cursor < cursorBase || cursor >= nextCursor) {
1508
+ continue;
1509
+ }
1510
+ const eventType = typeof eventRec.type === "string" ? eventRec.type : "";
1511
+ if (eventType === "intermediate_output") {
1512
+ const emission = eventRec.emission;
1513
+ if (!emission || typeof emission !== "object" || Array.isArray(emission)) {
1514
+ continue;
1515
+ }
1516
+ const emissionRec = emission;
1517
+ const outputIndex = Number.isInteger(emissionRec.outputIndex)
1518
+ ? Number(emissionRec.outputIndex)
1519
+ : NaN;
1520
+ if (!Number.isInteger(outputIndex) || outputIndex < 0)
1521
+ continue;
1522
+ const responseId = typeof emissionRec.responseId === "string"
1523
+ ? emissionRec.responseId
1524
+ : "";
1525
+ const actionCallId = typeof emissionRec.actionCallId === "string"
1526
+ ? emissionRec.actionCallId
1527
+ : "";
1528
+ const deckPath = typeof emissionRec.deckPath === "string"
1529
+ ? emissionRec.deckPath
1530
+ : "";
1531
+ const item = emissionRec.item;
1532
+ if (!responseId || !actionCallId || !deckPath || !item)
1533
+ continue;
1534
+ events.push({
1535
+ cursor,
1536
+ type: "intermediate_output",
1537
+ emission: {
1538
+ responseId,
1539
+ actionCallId,
1540
+ parentActionCallId: typeof emissionRec.parentActionCallId === "string"
1541
+ ? emissionRec.parentActionCallId
1542
+ : undefined,
1543
+ deckPath,
1544
+ outputIndex,
1545
+ item: item,
1546
+ },
1547
+ });
1548
+ continue;
1549
+ }
1550
+ if (eventType === "terminal") {
1551
+ const terminal = eventRec.terminal;
1552
+ if (!terminal || typeof terminal !== "object" || Array.isArray(terminal)) {
1553
+ continue;
1554
+ }
1555
+ const terminalRec = terminal;
1556
+ const terminalState = terminalRec.state;
1557
+ if (terminalState !== "completed" && terminalState !== "failed" &&
1558
+ terminalState !== "canceled") {
1559
+ continue;
1560
+ }
1561
+ events.push({
1562
+ cursor,
1563
+ type: "terminal",
1564
+ terminal: {
1565
+ state: terminalState,
1566
+ status: typeof terminalRec.status === "number"
1567
+ ? terminalRec.status
1568
+ : undefined,
1569
+ payload: terminalRec.payload,
1570
+ message: typeof terminalRec.message === "string"
1571
+ ? terminalRec.message
1572
+ : undefined,
1573
+ code: typeof terminalRec.code === "string"
1574
+ ? terminalRec.code
1575
+ : undefined,
1576
+ meta: terminalRec.meta && typeof terminalRec.meta === "object" &&
1577
+ !Array.isArray(terminalRec.meta)
1578
+ ? terminalRec.meta
1579
+ : undefined,
1580
+ },
1581
+ });
1582
+ }
1583
+ }
1584
+ events.sort((a, b) => a.cursor - b.cursor);
1585
+ const terminal = rec.terminal && typeof rec.terminal === "object" &&
1586
+ !Array.isArray(rec.terminal)
1587
+ ? (() => {
1588
+ const terminalRec = rec.terminal;
1589
+ const terminalState = terminalRec.state;
1590
+ if (terminalState !== "completed" && terminalState !== "failed" &&
1591
+ terminalState !== "canceled") {
1592
+ return undefined;
1593
+ }
1594
+ return {
1595
+ state: terminalState,
1596
+ status: typeof terminalRec.status === "number"
1597
+ ? terminalRec.status
1598
+ : undefined,
1599
+ payload: terminalRec.payload,
1600
+ message: typeof terminalRec.message === "string"
1601
+ ? terminalRec.message
1602
+ : undefined,
1603
+ code: typeof terminalRec.code === "string"
1604
+ ? terminalRec.code
1605
+ : undefined,
1606
+ meta: terminalRec.meta && typeof terminalRec.meta === "object" &&
1607
+ !Array.isArray(terminalRec.meta)
1608
+ ? terminalRec.meta
1609
+ : undefined,
1610
+ };
1611
+ })()
1612
+ : undefined;
1613
+ jobs.push({
1614
+ jobId,
1615
+ actionName,
1616
+ actionPath,
1617
+ state: parsedState,
1618
+ cursorBase,
1619
+ nextCursor,
1620
+ expectedCursor,
1621
+ events,
1622
+ terminal,
1623
+ });
1624
+ }
1625
+ return { version, jobs };
1626
+ }
1627
+ function createAsyncActionJobController(input) {
1628
+ const jobs = new Map();
1629
+ const notifyChange = () => input.onChange?.();
1630
+ const notifyWaiters = (record) => {
1631
+ if (record.waiters.size === 0)
1632
+ return;
1633
+ for (const waiter of record.waiters) {
1634
+ waiter();
1635
+ }
1636
+ record.waiters.clear();
1637
+ };
1638
+ const appendEvent = (record, event) => {
1639
+ const withCursor = {
1640
+ ...event,
1641
+ cursor: record.nextCursor,
1642
+ };
1643
+ record.events.push(withCursor);
1644
+ record.nextCursor += 1;
1645
+ while (record.events.length > ASYNC_ACTION_JOB_MAX_EVENTS) {
1646
+ record.events.shift();
1647
+ record.cursorBase += 1;
1648
+ }
1649
+ };
1650
+ const completeRecord = (record, terminal) => {
1651
+ if (record.state !== "running")
1652
+ return;
1653
+ record.state = terminal.state;
1654
+ record.terminal = cloneAsyncActionTerminalResult(terminal);
1655
+ appendEvent(record, {
1656
+ type: "terminal",
1657
+ terminal: cloneAsyncActionTerminalResult(terminal),
1658
+ });
1659
+ notifyWaiters(record);
1660
+ notifyChange();
1661
+ };
1662
+ const persisted = parsePersistedAsyncActionJobs(input.state);
1663
+ if (persisted) {
1664
+ let normalizedPersisted = false;
1665
+ for (const entry of persisted.jobs) {
1666
+ const record = {
1667
+ ...entry,
1668
+ events: entry.events.map((event) => cloneAsyncActionJobEvent(event)),
1669
+ terminal: entry.terminal
1670
+ ? cloneAsyncActionTerminalResult(entry.terminal)
1671
+ : undefined,
1672
+ waiters: new Set(),
1673
+ };
1674
+ if (record.expectedCursor < record.cursorBase) {
1675
+ record.expectedCursor = record.cursorBase;
1676
+ normalizedPersisted = true;
1677
+ }
1678
+ if (record.expectedCursor > record.nextCursor) {
1679
+ record.expectedCursor = record.nextCursor;
1680
+ normalizedPersisted = true;
1681
+ }
1682
+ if (record.state === "running") {
1683
+ normalizedPersisted = true;
1684
+ completeRecord(record, {
1685
+ state: "canceled",
1686
+ status: 499,
1687
+ code: ASYNC_ACTION_JOB_INTERRUPTED_CODE,
1688
+ message: "Async action job was interrupted before the run resumed.",
1689
+ });
1690
+ }
1691
+ else if (!record.terminal) {
1692
+ const terminalEvent = [...record.events].reverse().find((event) => event.type === "terminal");
1693
+ if (terminalEvent?.type === "terminal") {
1694
+ record.terminal = cloneAsyncActionTerminalResult(terminalEvent.terminal);
1695
+ normalizedPersisted = true;
1696
+ }
1697
+ }
1698
+ jobs.set(record.jobId, record);
1699
+ }
1700
+ if (normalizedPersisted) {
1701
+ notifyChange();
1702
+ }
1703
+ }
1704
+ const waitForJobChange = async (record, waitMs, signal) => {
1705
+ if (waitMs <= 0)
1706
+ return;
1707
+ await new Promise((resolve) => {
1708
+ const timeoutId = setTimeout(() => {
1709
+ cleanup();
1710
+ resolve();
1711
+ }, waitMs);
1712
+ const onAbort = () => {
1713
+ cleanup();
1714
+ resolve();
1715
+ };
1716
+ const waiter = () => {
1717
+ cleanup();
1718
+ resolve();
1719
+ };
1720
+ const cleanup = () => {
1721
+ clearTimeout(timeoutId);
1722
+ record.waiters.delete(waiter);
1723
+ if (signal)
1724
+ signal.removeEventListener("abort", onAbort);
1725
+ };
1726
+ record.waiters.add(waiter);
1727
+ if (signal)
1728
+ signal.addEventListener("abort", onAbort, { once: true });
1729
+ if (signal?.aborted) {
1730
+ cleanup();
1731
+ resolve();
1732
+ }
1733
+ });
1734
+ };
1735
+ return {
1736
+ hasConfiguredAsyncActions: input.hasConfiguredAsyncActions,
1737
+ hasLiveJobs: () => Array.from(jobs.values()).some((job) => job.state === "running"),
1738
+ startJob: ({ actionName, actionPath }) => {
1739
+ const jobId = randomId("job");
1740
+ const record = {
1741
+ jobId,
1742
+ actionName,
1743
+ actionPath,
1744
+ state: "running",
1745
+ cursorBase: 0,
1746
+ nextCursor: 0,
1747
+ expectedCursor: 0,
1748
+ events: [],
1749
+ waiters: new Set(),
1750
+ };
1751
+ jobs.set(jobId, record);
1752
+ notifyChange();
1753
+ return {
1754
+ handle: {
1755
+ jobId,
1756
+ actionName,
1757
+ state: "running",
1758
+ cursor: {
1759
+ min: record.cursorBase,
1760
+ next: record.nextCursor,
1761
+ },
1762
+ },
1763
+ appendIntermediateOutput: (emission) => {
1764
+ if (record.state !== "running")
1765
+ return;
1766
+ appendEvent(record, {
1767
+ type: "intermediate_output",
1768
+ emission: cloneIntermediateEmission(emission),
1769
+ });
1770
+ notifyWaiters(record);
1771
+ notifyChange();
1772
+ },
1773
+ complete: (terminalResult) => {
1774
+ completeRecord(record, {
1775
+ state: "completed",
1776
+ status: terminalResult.status,
1777
+ payload: terminalResult.payload,
1778
+ message: terminalResult.message,
1779
+ code: terminalResult.code,
1780
+ meta: terminalResult.meta,
1781
+ });
1782
+ },
1783
+ fail: (terminalResult) => {
1784
+ completeRecord(record, {
1785
+ state: terminalResult.state,
1786
+ status: terminalResult.status,
1787
+ payload: terminalResult.payload,
1788
+ message: terminalResult.message,
1789
+ code: terminalResult.code,
1790
+ meta: terminalResult.meta,
1791
+ });
1792
+ },
1793
+ bindLiveRun: (runPromise, controller) => {
1794
+ record.controller = controller;
1795
+ record.runPromise = runPromise.finally(() => {
1796
+ record.runPromise = undefined;
1797
+ record.controller = undefined;
1798
+ });
1799
+ },
1800
+ };
1801
+ },
1802
+ consume: async (consumeInput) => {
1803
+ ensureRunActive(consumeInput.runDeadlineMs, consumeInput.signal);
1804
+ const record = jobs.get(consumeInput.jobId);
1805
+ if (!record) {
1806
+ return {
1807
+ ok: false,
1808
+ status: 404,
1809
+ code: "async_job_not_found",
1810
+ message: `Unknown async action job: ${consumeInput.jobId}`,
1811
+ };
1812
+ }
1813
+ let cursor = consumeInput.cursor;
1814
+ if (!Number.isInteger(cursor) || cursor < 0) {
1815
+ return {
1816
+ ok: false,
1817
+ status: 400,
1818
+ code: "invalid_cursor",
1819
+ message: "cursor must be a non-negative integer",
1820
+ };
1821
+ }
1822
+ if (cursor < record.cursorBase) {
1823
+ return {
1824
+ ok: false,
1825
+ status: 409,
1826
+ code: "cursor_expired",
1827
+ message: `cursor ${cursor} is behind retained minimum cursor ${record.cursorBase}`,
1828
+ payload: { minCursor: record.cursorBase },
1829
+ };
1830
+ }
1831
+ if (cursor > record.nextCursor) {
1832
+ return {
1833
+ ok: false,
1834
+ status: 400,
1835
+ code: "invalid_cursor",
1836
+ message: `cursor ${cursor} is beyond current cursor ${record.nextCursor}`,
1837
+ payload: { maxCursor: record.nextCursor },
1838
+ };
1839
+ }
1840
+ if (cursor !== record.expectedCursor) {
1841
+ return {
1842
+ ok: false,
1843
+ status: 409,
1844
+ code: "cursor_mismatch",
1845
+ message: `cursor ${cursor} does not match expected cursor ${record.expectedCursor}`,
1846
+ payload: { expectedCursor: record.expectedCursor },
1847
+ };
1848
+ }
1849
+ const boundedWaitMs = Math.min(ASYNC_ACTION_JOB_MAX_WAIT_MS, Math.max(0, consumeInput.waitMs));
1850
+ const remainingMs = Math.max(0, Math.floor(consumeInput.runDeadlineMs - performance.now()));
1851
+ const waitMs = Math.min(boundedWaitMs, remainingMs);
1852
+ if (waitMs > 0 &&
1853
+ record.state === "running" &&
1854
+ cursor >= record.nextCursor) {
1855
+ await waitForJobChange(record, waitMs, consumeInput.signal);
1856
+ }
1857
+ ensureRunActive(consumeInput.runDeadlineMs, consumeInput.signal);
1858
+ cursor = consumeInput.cursor;
1859
+ if (cursor < record.cursorBase) {
1860
+ return {
1861
+ ok: false,
1862
+ status: 409,
1863
+ code: "cursor_expired",
1864
+ message: `cursor ${cursor} is behind retained minimum cursor ${record.cursorBase}`,
1865
+ payload: { minCursor: record.cursorBase },
1866
+ };
1867
+ }
1868
+ const startIndex = Math.max(0, cursor - record.cursorBase);
1869
+ const events = record.events.slice(startIndex, startIndex + consumeInput.limit).map((event) => cloneAsyncActionJobEvent(event));
1870
+ const nextCursor = cursor + events.length;
1871
+ if (record.expectedCursor !== nextCursor) {
1872
+ record.expectedCursor = nextCursor;
1873
+ notifyChange();
1874
+ }
1875
+ return {
1876
+ ok: true,
1877
+ payload: {
1878
+ jobId: record.jobId,
1879
+ actionName: record.actionName,
1880
+ state: record.state,
1881
+ cursor: {
1882
+ requested: consumeInput.cursor,
1883
+ min: record.cursorBase,
1884
+ next: nextCursor,
1885
+ expected: record.expectedCursor,
1886
+ },
1887
+ events,
1888
+ terminal: record.terminal
1889
+ ? cloneAsyncActionTerminalResult(record.terminal)
1890
+ : undefined,
1891
+ },
1892
+ };
1893
+ },
1894
+ snapshot: () => {
1895
+ if (jobs.size === 0)
1896
+ return undefined;
1897
+ return {
1898
+ version: ASYNC_ACTION_JOBS_META_VERSION,
1899
+ jobs: Array.from(jobs.values()).map((job) => ({
1900
+ jobId: job.jobId,
1901
+ actionName: job.actionName,
1902
+ actionPath: job.actionPath,
1903
+ state: job.state,
1904
+ cursorBase: job.cursorBase,
1905
+ nextCursor: job.nextCursor,
1906
+ expectedCursor: job.expectedCursor,
1907
+ events: job.events.map((event) => cloneAsyncActionJobEvent(event)),
1908
+ terminal: job.terminal
1909
+ ? cloneAsyncActionTerminalResult(job.terminal)
1910
+ : undefined,
1911
+ })),
1912
+ };
1913
+ },
1914
+ cancelLiveJobs: ({ message, code }) => {
1915
+ for (const record of jobs.values()) {
1916
+ if (record.state !== "running")
1917
+ continue;
1918
+ completeRecord(record, {
1919
+ state: "canceled",
1920
+ status: 499,
1921
+ code,
1922
+ message,
1923
+ });
1924
+ record.controller?.abort();
1925
+ }
1926
+ },
1927
+ };
1928
+ }
965
1929
  function mapResponseOutput(output) {
966
1930
  const toolCalls = [];
967
1931
  const textParts = [];
@@ -1015,7 +1979,22 @@ function validateInput(deck, input, isRoot, allowRootStringInput) {
1015
1979
  function validateOutput(deck, output, isRoot) {
1016
1980
  const responseSchema = resolveResponseSchema(deck);
1017
1981
  if (responseSchema) {
1018
- return validateWithSchema(responseSchema, output);
1982
+ try {
1983
+ return validateWithSchema(responseSchema, output);
1984
+ }
1985
+ catch (directErr) {
1986
+ if (typeof output !== "string")
1987
+ throw directErr;
1988
+ try {
1989
+ const parsed = JSON.parse(output);
1990
+ return validateWithSchema(responseSchema, parsed);
1991
+ }
1992
+ catch (parsedErr) {
1993
+ if (parsedErr instanceof SyntaxError)
1994
+ throw directErr;
1995
+ throw parsedErr;
1996
+ }
1997
+ }
1019
1998
  }
1020
1999
  if (isRoot) {
1021
2000
  if (typeof output === "string")
@@ -1045,6 +2024,9 @@ async function runComputeDeck(ctx) {
1045
2024
  responsesMode: ctx.responsesMode,
1046
2025
  permissions: ctx.permissions,
1047
2026
  permissionsTrace: ctx.permissionsTrace,
2027
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
2028
+ onIntermediateOutputItem: ctx.onIntermediateOutputItem,
2029
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
1048
2030
  workspacePermissions: ctx.workspacePermissions,
1049
2031
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
1050
2032
  sessionPermissions: ctx.sessionPermissions,
@@ -1417,14 +2399,30 @@ function collectLocalImportGraph(entryPath) {
1417
2399
  }
1418
2400
  return visited;
1419
2401
  }
1420
- const WORKER_ENTRY_PATHS = [
1421
- "./runtime_worker.ts",
1422
- "./runtime_orchestration_worker.ts",
1423
- ].map((relative) => path.fromFileUrl(new URL(relative, globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)));
1424
- const BUILTIN_SCHEMAS_DIR = path.resolve(path.dirname(path.fromFileUrl(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)), "../schemas");
1425
- const BUILTIN_SNIPPETS_DIR = path.resolve(path.dirname(path.fromFileUrl(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)), "../snippets");
2402
+ const moduleBaseFilePath = (() => {
2403
+ try {
2404
+ return path.fromFileUrl(globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url);
2405
+ }
2406
+ catch {
2407
+ return undefined;
2408
+ }
2409
+ })();
2410
+ const WORKER_ENTRY_PATHS = moduleBaseFilePath
2411
+ ? [
2412
+ "./runtime_worker.ts",
2413
+ "./runtime_orchestration_worker.ts",
2414
+ ].map((relative) => path.fromFileUrl(new URL(relative, globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url)))
2415
+ : [];
2416
+ const BUILTIN_SCHEMAS_DIR = moduleBaseFilePath
2417
+ ? path.resolve(path.dirname(moduleBaseFilePath), "../schemas")
2418
+ : undefined;
2419
+ const BUILTIN_SNIPPETS_DIR = moduleBaseFilePath
2420
+ ? path.resolve(path.dirname(moduleBaseFilePath), "../snippets")
2421
+ : undefined;
1426
2422
  let builtinSchemaBootstrapCache;
1427
2423
  function builtinSchemaBootstrapReads() {
2424
+ if (!BUILTIN_SCHEMAS_DIR)
2425
+ return [];
1428
2426
  if (builtinSchemaBootstrapCache)
1429
2427
  return builtinSchemaBootstrapCache;
1430
2428
  const schemaModules = [];
@@ -1457,6 +2455,8 @@ function builtinSchemaBootstrapReads() {
1457
2455
  }
1458
2456
  let builtinSnippetBootstrapCache;
1459
2457
  function builtinSnippetBootstrapReads() {
2458
+ if (!BUILTIN_SNIPPETS_DIR)
2459
+ return [];
1460
2460
  if (builtinSnippetBootstrapCache)
1461
2461
  return builtinSnippetBootstrapCache;
1462
2462
  const snippetFiles = [];
@@ -1499,6 +2499,10 @@ let trustedWorkerBootstrapCache;
1499
2499
  function trustedWorkerBootstrapReads() {
1500
2500
  if (trustedWorkerBootstrapCache)
1501
2501
  return trustedWorkerBootstrapCache;
2502
+ if (!moduleBaseFilePath) {
2503
+ trustedWorkerBootstrapCache = [];
2504
+ return trustedWorkerBootstrapCache;
2505
+ }
1502
2506
  const definitionsPath = path.fromFileUrl(new URL("./definitions.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
1503
2507
  const modPath = path.fromFileUrl(new URL("../mod.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
1504
2508
  trustedWorkerBootstrapCache = Array.from(new Set([
@@ -1688,6 +2692,7 @@ async function runLlmDeckInWorker(ctx) {
1688
2692
  const bridgeSession = randomId("bridge");
1689
2693
  const completionNonce = randomId("done");
1690
2694
  const worker = createWorkerSandboxBridge(new URL("./runtime_orchestration_worker.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).href, buildWorkerPermissions(ctx.permissions, ctx.deckPath));
2695
+ const modelRequestControllers = new Map();
1691
2696
  let settled = false;
1692
2697
  const clearAndTerminate = () => {
1693
2698
  try {
@@ -1739,6 +2744,15 @@ async function runLlmDeckInWorker(ctx) {
1739
2744
  return;
1740
2745
  }
1741
2746
  if (msg.type === "trace.event") {
2747
+ const emission = ctx.onIntermediateOutputItem
2748
+ ? parseIntermediateOutputEmissionFromTraceEvent({
2749
+ event: msg.event,
2750
+ expectedParentActionCallId: ctx.parentActionCallId,
2751
+ })
2752
+ : undefined;
2753
+ if (emission) {
2754
+ ctx.onIntermediateOutputItem?.(emission);
2755
+ }
1742
2756
  ctx.trace?.(msg.event);
1743
2757
  return;
1744
2758
  }
@@ -1752,10 +2766,12 @@ async function runLlmDeckInWorker(ctx) {
1752
2766
  }
1753
2767
  if (msg.type === "model.chat.request") {
1754
2768
  (async () => {
2769
+ const controller = new AbortController();
2770
+ modelRequestControllers.set(msg.requestId, controller);
1755
2771
  try {
1756
2772
  const result = await ctx.modelProvider.chat({
1757
2773
  ...msg.input,
1758
- signal: ctx.signal,
2774
+ signal: mergeAbortSignals(ctx.signal, controller.signal),
1759
2775
  onStreamText: (chunk) => {
1760
2776
  worker.postMessage({
1761
2777
  type: "model.chat.stream",
@@ -1782,18 +2798,23 @@ async function runLlmDeckInWorker(ctx) {
1782
2798
  },
1783
2799
  });
1784
2800
  }
2801
+ finally {
2802
+ modelRequestControllers.delete(msg.requestId);
2803
+ }
1785
2804
  })();
1786
2805
  return;
1787
2806
  }
1788
2807
  if (msg.type === "model.responses.request") {
1789
2808
  (async () => {
2809
+ const controller = new AbortController();
2810
+ modelRequestControllers.set(msg.requestId, controller);
1790
2811
  try {
1791
2812
  if (!ctx.modelProvider.responses) {
1792
2813
  throw new Error("Responses API unavailable for current model provider");
1793
2814
  }
1794
2815
  const result = await ctx.modelProvider.responses({
1795
2816
  ...msg.input,
1796
- signal: ctx.signal,
2817
+ signal: mergeAbortSignals(ctx.signal, controller.signal),
1797
2818
  onStreamEvent: (streamEvent) => {
1798
2819
  worker.postMessage({
1799
2820
  type: "model.responses.event",
@@ -1820,9 +2841,16 @@ async function runLlmDeckInWorker(ctx) {
1820
2841
  },
1821
2842
  });
1822
2843
  }
2844
+ finally {
2845
+ modelRequestControllers.delete(msg.requestId);
2846
+ }
1823
2847
  })();
1824
2848
  return;
1825
2849
  }
2850
+ if (msg.type === "model.request.cancel") {
2851
+ modelRequestControllers.get(msg.requestId)?.abort();
2852
+ return;
2853
+ }
1826
2854
  if (msg.type === "model.resolveModel.request") {
1827
2855
  (async () => {
1828
2856
  try {
@@ -1893,6 +2921,8 @@ async function runLlmDeckInWorker(ctx) {
1893
2921
  state: ctx.state,
1894
2922
  responsesMode: ctx.responsesMode,
1895
2923
  allowRootStringInput: ctx.allowRootStringInput,
2924
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
2925
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
1896
2926
  runDeadlineMs: ctx.runDeadlineMs,
1897
2927
  },
1898
2928
  permissionCeiling: toWirePermissionSet(ctx.permissions),
@@ -1901,6 +2931,9 @@ async function runLlmDeckInWorker(ctx) {
1901
2931
  return await outcome;
1902
2932
  }
1903
2933
  finally {
2934
+ for (const controller of modelRequestControllers.values()) {
2935
+ controller.abort();
2936
+ }
1904
2937
  if (timeoutId !== undefined)
1905
2938
  clearTimeout(timeoutId);
1906
2939
  clearAndTerminate();
@@ -1925,6 +2958,18 @@ async function runComputeDeckInWorker(ctx) {
1925
2958
  let timeoutId;
1926
2959
  const activeSpawnRequests = new Set();
1927
2960
  let currentState = ctx.state;
2961
+ const intermediateEmitter = createIntermediateChildResponseEmitter({
2962
+ runId,
2963
+ actionCallId,
2964
+ deckPath: ctx.deckPath,
2965
+ parentActionCallId: ctx.parentActionCallId,
2966
+ trace: ctx.trace,
2967
+ validateItem: (item) => item,
2968
+ ensureActive: () => ensureRunActive(ctx.runDeadlineMs, ctx.signal),
2969
+ allowEmission: ctx.intermediateOutputAllow,
2970
+ errorContext: ctx.intermediateOutputErrorContext,
2971
+ onEmit: ctx.onIntermediateOutputItem,
2972
+ });
1928
2973
  const outcome = new Promise((resolve, reject) => {
1929
2974
  const finishResolve = (value) => {
1930
2975
  if (settled)
@@ -2038,6 +3083,9 @@ async function runComputeDeckInWorker(ctx) {
2038
3083
  responsesMode: ctx.responsesMode,
2039
3084
  initialUserMessage: req.payload.initialUserMessage,
2040
3085
  inputProvided: true,
3086
+ intermediateOutputAllow: req.payload.intermediateOutputAllow,
3087
+ onIntermediateOutputItem: ctx.onIntermediateOutputItem,
3088
+ intermediateOutputErrorContext: req.payload.intermediateOutputErrorContext,
2041
3089
  parentPermissions: bridgedParent,
2042
3090
  workspacePermissions: req.payload.workspacePermissions,
2043
3091
  workspacePermissionsBaseDir: req.payload.workspacePermissionsBaseDir,
@@ -2082,12 +3130,34 @@ async function runComputeDeckInWorker(ctx) {
2082
3130
  ctx.onStateUpdate?.(nextState);
2083
3131
  return;
2084
3132
  }
3133
+ if (type === "response.item") {
3134
+ const item = msg.item;
3135
+ if (!item || typeof item !== "object")
3136
+ return;
3137
+ try {
3138
+ intermediateEmitter.emitOutputItem(item, "worker.emitOutputItem");
3139
+ }
3140
+ catch (err) {
3141
+ intermediateEmitter.fail(err);
3142
+ finishReject(err);
3143
+ clearAndTerminate();
3144
+ }
3145
+ return;
3146
+ }
2085
3147
  if (type === "run.result") {
2086
3148
  if (msg.completionNonce !==
2087
3149
  completionNonce) {
2088
3150
  logger.warn(`[gambit] rejected compute-worker run.result with invalid completion nonce`);
2089
3151
  return;
2090
3152
  }
3153
+ try {
3154
+ intermediateEmitter.complete();
3155
+ }
3156
+ catch (err) {
3157
+ intermediateEmitter.fail(err);
3158
+ finishReject(err);
3159
+ return;
3160
+ }
2091
3161
  finishResolve(msg.result);
2092
3162
  return;
2093
3163
  }
@@ -2097,7 +3167,9 @@ async function runComputeDeckInWorker(ctx) {
2097
3167
  logger.warn(`[gambit] rejected compute-worker run.error with invalid completion nonce`);
2098
3168
  return;
2099
3169
  }
2100
- finishReject(normalizeWorkerError(msg.error));
3170
+ const normalizedError = normalizeWorkerError(msg.error);
3171
+ intermediateEmitter.fail(normalizedError);
3172
+ finishReject(normalizedError);
2101
3173
  }
2102
3174
  });
2103
3175
  });
@@ -2114,6 +3186,8 @@ async function runComputeDeckInWorker(ctx) {
2114
3186
  initialUserMessage: ctx.initialUserMessage,
2115
3187
  depth: ctx.depth,
2116
3188
  parentActionCallId: ctx.parentActionCallId,
3189
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
3190
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
2117
3191
  permissions: toWirePermissionSet(ctx.permissions),
2118
3192
  workspacePermissions: ctx.workspacePermissions,
2119
3193
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
@@ -2136,6 +3210,7 @@ async function runComputeDeckInWorker(ctx) {
2136
3210
  async function runComputeDeckInProcess(ctx) {
2137
3211
  const { deck, runId } = ctx;
2138
3212
  const actionCallId = randomId("action");
3213
+ const validateResponseItemEmission = createResponseItemEmissionValidator(deck);
2139
3214
  let computeState = ctx.state
2140
3215
  ? {
2141
3216
  ...ctx.state,
@@ -2171,6 +3246,18 @@ async function runComputeDeckInProcess(ctx) {
2171
3246
  : undefined,
2172
3247
  });
2173
3248
  };
3249
+ const intermediateEmitter = createIntermediateChildResponseEmitter({
3250
+ runId,
3251
+ actionCallId,
3252
+ deckPath: deck.path,
3253
+ parentActionCallId: ctx.parentActionCallId,
3254
+ trace: ctx.trace,
3255
+ validateItem: validateResponseItemEmission,
3256
+ ensureActive: () => ensureRunActive(ctx.runDeadlineMs, ctx.signal),
3257
+ allowEmission: ctx.intermediateOutputAllow,
3258
+ errorContext: ctx.intermediateOutputErrorContext,
3259
+ onEmit: ctx.onIntermediateOutputItem,
3260
+ });
2174
3261
  const execContext = {
2175
3262
  runId,
2176
3263
  actionCallId,
@@ -2213,6 +3300,10 @@ async function runComputeDeckInProcess(ctx) {
2213
3300
  state.messageRefs = refs;
2214
3301
  publishComputeState();
2215
3302
  },
3303
+ emitOutputItem: (item) => {
3304
+ intermediateEmitter.emitOutputItem(item, "executionContext.emitOutputItem");
3305
+ return Promise.resolve();
3306
+ },
2216
3307
  label: deck.label,
2217
3308
  log: (entry) => {
2218
3309
  if (!ctx.trace)
@@ -2281,6 +3372,9 @@ async function runComputeDeckInProcess(ctx) {
2281
3372
  responsesMode: ctx.responsesMode,
2282
3373
  initialUserMessage: childInitialUserMessage,
2283
3374
  inputProvided: true,
3375
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
3376
+ onIntermediateOutputItem: ctx.onIntermediateOutputItem,
3377
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
2284
3378
  parentPermissions: ctx.permissions,
2285
3379
  workspacePermissions: ctx.workspacePermissions,
2286
3380
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
@@ -2297,18 +3391,40 @@ async function runComputeDeckInProcess(ctx) {
2297
3391
  },
2298
3392
  return: (payload) => Promise.resolve(payload),
2299
3393
  };
2300
- ensureRunActive(ctx.runDeadlineMs, ctx.signal);
2301
- const raw = await deck.executor(execContext);
2302
- ensureRunActive(ctx.runDeadlineMs, ctx.signal);
2303
- return validateOutput(deck, raw, ctx.depth === 0);
3394
+ try {
3395
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
3396
+ const raw = await deck.executor(execContext);
3397
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
3398
+ const validated = validateOutput(deck, raw, ctx.depth === 0);
3399
+ intermediateEmitter.complete();
3400
+ return validated;
3401
+ }
3402
+ catch (err) {
3403
+ intermediateEmitter.fail(err);
3404
+ throw err;
3405
+ }
2304
3406
  }
2305
3407
  async function runLlmDeck(ctx) {
2306
3408
  const { deck, guardrails, depth, modelProvider, input, runId, inputProvided, initialUserMessage, } = ctx;
2307
3409
  const actionCallId = randomId("action");
2308
3410
  const start = performance.now();
2309
- const respondEnabled = Boolean(deck.respond);
2310
3411
  const useResponses = Boolean(ctx.responsesMode) ||
2311
3412
  ctx.state?.format === "responses";
3413
+ const validateResponseItemEmission = createResponseItemEmissionValidator(deck);
3414
+ const intermediateEmitter = ctx.parentActionCallId !== undefined
3415
+ ? createIntermediateChildResponseEmitter({
3416
+ runId,
3417
+ actionCallId,
3418
+ deckPath: deck.path,
3419
+ parentActionCallId: ctx.parentActionCallId,
3420
+ trace: ctx.trace,
3421
+ validateItem: validateResponseItemEmission,
3422
+ ensureActive: () => ensureRunActive(ctx.runDeadlineMs, ctx.signal),
3423
+ allowEmission: ctx.intermediateOutputAllow,
3424
+ errorContext: ctx.intermediateOutputErrorContext,
3425
+ onEmit: ctx.onIntermediateOutputItem,
3426
+ })
3427
+ : undefined;
2312
3428
  const systemPrompt = buildSystemPrompt(deck);
2313
3429
  const refToolCallId = randomId("call");
2314
3430
  const messages = ctx.state?.messages?.length
@@ -2333,6 +3449,9 @@ async function runLlmDeck(ctx) {
2333
3449
  onStreamText: ctx.onStreamText,
2334
3450
  pushMessages: (msgs) => messages.push(...msgs.map(sanitizeMessage)),
2335
3451
  responsesMode: ctx.responsesMode,
3452
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
3453
+ onIntermediateOutputItem: ctx.onIntermediateOutputItem,
3454
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
2336
3455
  permissions: ctx.permissions,
2337
3456
  workspacePermissions: ctx.workspacePermissions,
2338
3457
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
@@ -2343,6 +3462,10 @@ async function runLlmDeck(ctx) {
2343
3462
  signal: ctx.signal,
2344
3463
  onTool: ctx.onTool,
2345
3464
  });
3465
+ const asyncActionJobs = createAsyncActionJobController({
3466
+ state: ctx.state,
3467
+ hasConfiguredAsyncActions: deck.actionDecks.some((action) => action.asyncStart?.mode === "allow"),
3468
+ });
2346
3469
  let streamingBuffer = "";
2347
3470
  let streamingCommitted = false;
2348
3471
  const wrappedOnStreamText = (chunk) => {
@@ -2408,7 +3531,14 @@ async function runLlmDeck(ctx) {
2408
3531
  });
2409
3532
  }
2410
3533
  idleController.touch();
2411
- const tools = await buildToolDefs(deck, ctx.permissions);
3534
+ const tools = await buildToolDefs(deck, ctx.permissions, {
3535
+ parentActionCallId: ctx.parentActionCallId,
3536
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
3537
+ asyncActionJobs,
3538
+ });
3539
+ const responseTextConfig = useResponses
3540
+ ? toResponseTextConfig(deck)
3541
+ : undefined;
2412
3542
  ctx.trace?.({
2413
3543
  type: "deck.start",
2414
3544
  runId,
@@ -2462,6 +3592,7 @@ async function runLlmDeck(ctx) {
2462
3592
  const projectedToolCalls = new Set();
2463
3593
  const projectedToolResults = new Set();
2464
3594
  const projectedToolNames = new Map();
3595
+ const canonicalizeStreamEvent = createCanonicalStreamEventController();
2465
3596
  const result = (useResponses && responses)
2466
3597
  ? await (async () => {
2467
3598
  const responseItems = responseItemsFromMessages(messages);
@@ -2471,6 +3602,7 @@ async function runLlmDeck(ctx) {
2471
3602
  model,
2472
3603
  input: responseItems,
2473
3604
  tools: tools,
3605
+ text: responseTextConfig,
2474
3606
  stream: ctx.stream,
2475
3607
  params: providerParams,
2476
3608
  },
@@ -2479,8 +3611,11 @@ async function runLlmDeck(ctx) {
2479
3611
  signal: ctx.signal,
2480
3612
  onStreamEvent: (ctx.trace || ctx.onStreamText || deck.handlers?.onIdle)
2481
3613
  ? (event) => {
3614
+ const normalizedEvent = validateResponseEventItems(event, validateResponseItemEmission);
3615
+ const streamEvent = canonicalizeStreamEvent(normalizedEvent);
3616
+ if (!streamEvent)
3617
+ return;
2482
3618
  if (ctx.trace) {
2483
- const streamEvent = event;
2484
3619
  const handledAsResponse = traceOpenResponsesStreamEvent({
2485
3620
  streamEvent,
2486
3621
  runId,
@@ -2511,17 +3646,28 @@ async function runLlmDeck(ctx) {
2511
3646
  toolNames: projectedToolNames,
2512
3647
  });
2513
3648
  }
2514
- if (event.type === "response.output_text.delta") {
3649
+ const eventType = typeof streamEvent.type === "string"
3650
+ ? streamEvent.type
3651
+ : "";
3652
+ if (eventType === "response.output_text.delta") {
2515
3653
  sawDelta = true;
2516
- wrappedOnStreamText(event.delta);
3654
+ const delta = typeof streamEvent.delta === "string"
3655
+ ? streamEvent.delta
3656
+ : "";
3657
+ if (delta)
3658
+ wrappedOnStreamText(delta);
2517
3659
  }
2518
- else if (event.type === "response.output_text.done" && !sawDelta) {
2519
- wrappedOnStreamText(event.text);
3660
+ else if (eventType === "response.output_text.done" && !sawDelta) {
3661
+ const text = typeof streamEvent.text === "string"
3662
+ ? streamEvent.text
3663
+ : "";
3664
+ if (text)
3665
+ wrappedOnStreamText(text);
2520
3666
  }
2521
3667
  }
2522
3668
  : undefined,
2523
3669
  });
2524
- responseOutputItems = response.output ?? [];
3670
+ responseOutputItems = validateResponseOutputItems(response.output ?? [], validateResponseItemEmission, "response.output");
2525
3671
  const mapped = mapResponseOutput(responseOutputItems);
2526
3672
  return {
2527
3673
  message: mapped.message,
@@ -2545,8 +3691,11 @@ async function runLlmDeck(ctx) {
2545
3691
  : undefined,
2546
3692
  onStreamEvent: ctx.trace
2547
3693
  ? (event) => {
3694
+ const streamEvent = canonicalizeStreamEvent(event);
3695
+ if (!streamEvent)
3696
+ return;
2548
3697
  const handledAsResponse = traceOpenResponsesStreamEvent({
2549
- streamEvent: event,
3698
+ streamEvent,
2550
3699
  runId,
2551
3700
  actionCallId,
2552
3701
  deckPath: deck.path,
@@ -2561,12 +3710,12 @@ async function runLlmDeck(ctx) {
2561
3710
  actionCallId,
2562
3711
  deckPath: deck.path,
2563
3712
  model,
2564
- event,
3713
+ event: streamEvent,
2565
3714
  parentActionCallId: ctx.parentActionCallId,
2566
3715
  });
2567
3716
  }
2568
3717
  projectStreamToolTraceEvents({
2569
- streamEvent: event,
3718
+ streamEvent,
2570
3719
  runId,
2571
3720
  parentActionCallId: actionCallId,
2572
3721
  trace: ctx.trace,
@@ -2600,14 +3749,36 @@ async function runLlmDeck(ctx) {
2600
3749
  const mergedMessages = base.messages && base.messages.length > 0
2601
3750
  ? base.messages.map(sanitizeMessage)
2602
3751
  : messages.map(sanitizeMessage);
2603
- const responseItems = useResponses
2604
- ? responseItemsFromMessages(mergedMessages)
2605
- : updated?.items ?? ctx.state?.items;
3752
+ let responseItems;
3753
+ if (useResponses) {
3754
+ const conversationalItems = responseItemsFromMessages(mergedMessages);
3755
+ const priorSupplemental = (updated?.items ?? ctx.state?.items ?? [])
3756
+ .filter((item) => isSupplementalResponseItem(item));
3757
+ const latestSupplemental = (responseOutputItems ?? [])
3758
+ .filter((item) => isSupplementalResponseItem(item));
3759
+ const supplementalItems = mergeSupplementalResponseItems(priorSupplemental, latestSupplemental);
3760
+ responseItems = [
3761
+ ...conversationalItems,
3762
+ ...supplementalItems,
3763
+ ];
3764
+ }
3765
+ else {
3766
+ responseItems = updated?.items ?? ctx.state?.items;
3767
+ }
2606
3768
  const priorRefs = updated?.messageRefs ?? ctx.state?.messageRefs ?? [];
2607
3769
  const messageRefs = mergedMessages.map((m, idx) => priorRefs[idx] ?? { id: randomId("msg"), role: m.role });
2608
3770
  const feedback = updated?.feedback ?? ctx.state?.feedback;
2609
3771
  const traces = updated?.traces ?? ctx.state?.traces;
2610
- const meta = updated?.meta ?? ctx.state?.meta;
3772
+ const sourceMeta = updated?.meta ?? ctx.state?.meta;
3773
+ const nextMeta = sourceMeta ? { ...sourceMeta } : {};
3774
+ const asyncActionSnapshot = asyncActionJobs.snapshot();
3775
+ if (asyncActionSnapshot) {
3776
+ nextMeta[ASYNC_ACTION_JOBS_META_KEY] = asyncActionSnapshot;
3777
+ }
3778
+ else {
3779
+ delete nextMeta[ASYNC_ACTION_JOBS_META_KEY];
3780
+ }
3781
+ const meta = Object.keys(nextMeta).length > 0 ? nextMeta : undefined;
2611
3782
  const notes = updated?.notes ?? ctx.state?.notes;
2612
3783
  const conversationScore = updated?.conversationScore ??
2613
3784
  ctx.state?.conversationScore;
@@ -2628,9 +3799,6 @@ async function runLlmDeck(ctx) {
2628
3799
  };
2629
3800
  };
2630
3801
  if (result.toolCalls && result.toolCalls.length > 0) {
2631
- let responded = false;
2632
- let respondValue;
2633
- let endSignal;
2634
3802
  const appendedMessages = [];
2635
3803
  const toolCallText = streamingBuffer ||
2636
3804
  (typeof message.content === "string" ? message.content : "");
@@ -2639,141 +3807,11 @@ async function runLlmDeck(ctx) {
2639
3807
  streamingCommitted = true;
2640
3808
  }
2641
3809
  for (const call of result.toolCalls) {
2642
- if (respondEnabled && call.name === GAMBIT_TOOL_RESPOND) {
2643
- const status = typeof call.args?.status === "number"
2644
- ? call.args.status
2645
- : undefined;
2646
- const message = typeof call.args?.message === "string"
2647
- ? call.args.message
2648
- : undefined;
2649
- const code = typeof call.args?.code === "string"
2650
- ? call.args.code
2651
- : undefined;
2652
- const meta = (call.args?.meta &&
2653
- typeof call.args.meta === "object" &&
2654
- call.args.meta !== null)
2655
- ? call.args.meta
2656
- : undefined;
2657
- const rawPayload = call.args?.payload ?? call.args;
2658
- const validatedPayload = validateOutput(deck, rawPayload, depth === 0);
2659
- const respondEnvelope = {
2660
- payload: validatedPayload,
2661
- };
2662
- if (status !== undefined)
2663
- respondEnvelope.status = status;
2664
- if (message !== undefined)
2665
- respondEnvelope.message = message;
2666
- if (code !== undefined)
2667
- respondEnvelope.code = code;
2668
- if (meta !== undefined)
2669
- respondEnvelope.meta = meta;
2670
- ctx.trace?.({
2671
- type: "tool.call",
2672
- runId,
2673
- actionCallId: call.id,
2674
- name: call.name,
2675
- args: call.args,
2676
- toolKind: "internal",
2677
- parentActionCallId: actionCallId,
2678
- });
2679
- const toolContent = JSON.stringify(call.args ?? {});
2680
- appendedMessages.push({
2681
- role: "assistant",
2682
- content: null,
2683
- tool_calls: [{
2684
- id: call.id,
2685
- type: "function",
2686
- function: {
2687
- name: call.name,
2688
- arguments: JSON.stringify(call.args ?? {}),
2689
- },
2690
- }],
2691
- });
2692
- appendedMessages.push({
2693
- role: "tool",
2694
- tool_call_id: call.id,
2695
- name: call.name,
2696
- content: toolContent,
2697
- });
2698
- respondValue = respondEnvelope;
2699
- responded = true;
2700
- ctx.trace?.({
2701
- type: "tool.result",
2702
- runId,
2703
- actionCallId: call.id,
2704
- name: call.name,
2705
- result: respondEnvelope,
2706
- toolKind: "internal",
2707
- parentActionCallId: actionCallId,
2708
- });
2709
- continue;
3810
+ if (REMOVED_LEGACY_RESPONSE_TOOL_NAMES.has(call.name)) {
3811
+ throw legacyResponseToolMigrationError(`Model emitted removed synthetic tool "${call.name}" for deck ${deck.path}.`);
2710
3812
  }
2711
- if (deck.allowEnd && call.name === GAMBIT_TOOL_END) {
2712
- const status = typeof call.args?.status === "number"
2713
- ? call.args.status
2714
- : undefined;
2715
- const messageText = typeof call.args?.message === "string"
2716
- ? call.args.message
2717
- : undefined;
2718
- const code = typeof call.args?.code === "string"
2719
- ? call.args.code
2720
- : undefined;
2721
- const meta = (call.args?.meta &&
2722
- typeof call.args.meta === "object" &&
2723
- call.args.meta !== null)
2724
- ? call.args.meta
2725
- : undefined;
2726
- const payload = call.args?.payload;
2727
- ctx.trace?.({
2728
- type: "tool.call",
2729
- runId,
2730
- actionCallId: call.id,
2731
- name: call.name,
2732
- args: call.args,
2733
- toolKind: "internal",
2734
- parentActionCallId: actionCallId,
2735
- });
2736
- const toolContent = JSON.stringify(call.args ?? {});
2737
- appendedMessages.push({
2738
- role: "assistant",
2739
- content: null,
2740
- tool_calls: [{
2741
- id: call.id,
2742
- type: "function",
2743
- function: {
2744
- name: call.name,
2745
- arguments: JSON.stringify(call.args ?? {}),
2746
- },
2747
- }],
2748
- });
2749
- appendedMessages.push({
2750
- role: "tool",
2751
- tool_call_id: call.id,
2752
- name: call.name,
2753
- content: toolContent,
2754
- });
2755
- const signal = { __gambitEnd: true };
2756
- if (status !== undefined)
2757
- signal.status = status;
2758
- if (messageText !== undefined)
2759
- signal.message = messageText;
2760
- if (code !== undefined)
2761
- signal.code = code;
2762
- if (meta !== undefined)
2763
- signal.meta = meta;
2764
- if (payload !== undefined)
2765
- signal.payload = payload;
2766
- endSignal = signal;
2767
- ctx.trace?.({
2768
- type: "tool.result",
2769
- runId,
2770
- actionCallId: call.id,
2771
- name: call.name,
2772
- result: signal,
2773
- toolKind: "internal",
2774
- parentActionCallId: actionCallId,
2775
- });
2776
- continue;
3813
+ if (isContextToolName(call.name)) {
3814
+ throw syntheticContextToolInvocationError(deck.path);
2777
3815
  }
2778
3816
  const actionRef = deck.actionDecks.find((a) => a.name === call.name);
2779
3817
  const toolKind = actionRef ? "action" : "external";
@@ -2815,6 +3853,7 @@ async function runLlmDeck(ctx) {
2815
3853
  defaultModel: ctx.defaultModel,
2816
3854
  modelOverride: ctx.modelOverride,
2817
3855
  trace: ctx.trace,
3856
+ stream: ctx.stream,
2818
3857
  onStreamText: (ctx.onStreamText || deck.handlers?.onIdle)
2819
3858
  ? wrappedOnStreamText
2820
3859
  : undefined,
@@ -2830,7 +3869,14 @@ async function runLlmDeck(ctx) {
2830
3869
  runDeadlineMs: ctx.runDeadlineMs,
2831
3870
  workerSandbox: ctx.workerSandbox,
2832
3871
  signal: ctx.signal,
3872
+ intermediateOutputAllow: ctx.intermediateOutputAllow,
3873
+ onIntermediateOutputItem: ctx.onIntermediateOutputItem,
3874
+ intermediateOutputErrorContext: ctx.intermediateOutputErrorContext,
2833
3875
  onTool: ctx.onTool,
3876
+ emitIntermediateOutputItem: intermediateEmitter
3877
+ ? (item, source) => intermediateEmitter.emitOutputItem(item, source)
3878
+ : undefined,
3879
+ asyncActionJobs,
2834
3880
  });
2835
3881
  ctx.trace?.({
2836
3882
  type: "tool.result",
@@ -2880,30 +3926,9 @@ async function runLlmDeck(ctx) {
2880
3926
  const state = computeState(result.updatedState);
2881
3927
  ctx.onStateUpdate(state);
2882
3928
  }
2883
- if (endSignal) {
2884
- ctx.trace?.({
2885
- type: "deck.end",
2886
- runId,
2887
- deckPath: deck.path,
2888
- actionCallId,
2889
- parentActionCallId: ctx.parentActionCallId,
2890
- });
2891
- return endSignal;
2892
- }
2893
- if (responded) {
2894
- ctx.trace?.({
2895
- type: "deck.end",
2896
- runId,
2897
- deckPath: deck.path,
2898
- actionCallId,
2899
- parentActionCallId: ctx.parentActionCallId,
2900
- });
2901
- return respondValue;
2902
- }
2903
3929
  continue;
2904
3930
  }
2905
- if (!respondEnabled &&
2906
- result.finishReason === "stop" &&
3931
+ if (result.finishReason === "stop" &&
2907
3932
  (message.content === null || message.content === undefined) &&
2908
3933
  (!result.toolCalls || result.toolCalls.length === 0)) {
2909
3934
  message = { ...message, content: "" };
@@ -2934,30 +3959,34 @@ async function runLlmDeck(ctx) {
2934
3959
  .content,
2935
3960
  });
2936
3961
  }
2937
- if (!respondEnabled) {
2938
- const validated = validateOutput(deck, message.content, depth === 0);
2939
- ctx.trace?.({
2940
- type: "deck.end",
2941
- runId,
2942
- deckPath: deck.path,
2943
- actionCallId,
2944
- parentActionCallId: ctx.parentActionCallId,
2945
- });
2946
- return validated;
2947
- }
2948
- }
2949
- if (respondEnabled && result.finishReason === "stop") {
2950
- continue;
3962
+ const validated = validateOutput(deck, message.content, depth === 0);
3963
+ intermediateEmitter?.complete();
3964
+ ctx.trace?.({
3965
+ type: "deck.end",
3966
+ runId,
3967
+ deckPath: deck.path,
3968
+ actionCallId,
3969
+ parentActionCallId: ctx.parentActionCallId,
3970
+ });
3971
+ return validated;
2951
3972
  }
2952
3973
  if (passes >= guardrails.maxPasses) {
2953
3974
  throw new Error("Max passes exceeded without completing");
2954
3975
  }
2955
3976
  }
3977
+ throw new Error("Model did not complete within guardrails");
3978
+ }
3979
+ catch (err) {
3980
+ intermediateEmitter?.fail(err);
3981
+ throw err;
2956
3982
  }
2957
3983
  finally {
3984
+ asyncActionJobs.cancelLiveJobs({
3985
+ code: "async_job_parent_ended",
3986
+ message: "Parent run ended before async action job reached terminal state.",
3987
+ });
2958
3988
  idleController.stop();
2959
3989
  }
2960
- throw new Error("Model did not complete within guardrails");
2961
3990
  }
2962
3991
  async function handleToolCall(call, ctx) {
2963
3992
  ensureRunActive(ctx.runDeadlineMs, ctx.signal);
@@ -2965,6 +3994,7 @@ async function handleToolCall(call, ctx) {
2965
3994
  deckPath: ctx.parentDeck.path,
2966
3995
  actionName: call.name,
2967
3996
  };
3997
+ let intermediateOutputView;
2968
3998
  const baseComplete = (payload) => JSON.stringify({
2969
3999
  runId: ctx.runId,
2970
4000
  actionCallId: call.id,
@@ -2975,6 +4005,9 @@ async function handleToolCall(call, ctx) {
2975
4005
  message: payload.message,
2976
4006
  code: payload.code,
2977
4007
  meta: payload.meta,
4008
+ ...(intermediateOutputView
4009
+ ? { intermediateOutput: intermediateOutputView }
4010
+ : {}),
2978
4011
  });
2979
4012
  const extraMessages = [];
2980
4013
  const started = performance.now();
@@ -2988,6 +4021,98 @@ async function handleToolCall(call, ctx) {
2988
4021
  message,
2989
4022
  }),
2990
4023
  });
4024
+ if (call.name === BUILTIN_TOOL_EMIT_OUTPUT_ITEM) {
4025
+ if (!ctx.emitIntermediateOutputItem) {
4026
+ return {
4027
+ toolContent: baseComplete({
4028
+ status: 400,
4029
+ code: "invalid_tool_context",
4030
+ message: `${BUILTIN_TOOL_EMIT_OUTPUT_ITEM} is only available inside child LLM action decks`,
4031
+ }),
4032
+ };
4033
+ }
4034
+ let item;
4035
+ try {
4036
+ item = parseEmitOutputItemArgs(call.args);
4037
+ }
4038
+ catch (err) {
4039
+ return {
4040
+ toolContent: baseComplete({
4041
+ status: 400,
4042
+ code: "invalid_input",
4043
+ message: err instanceof Error ? err.message : String(err),
4044
+ }),
4045
+ };
4046
+ }
4047
+ try {
4048
+ ctx.emitIntermediateOutputItem(item, `tool.${BUILTIN_TOOL_EMIT_OUTPUT_ITEM}`);
4049
+ }
4050
+ catch (err) {
4051
+ const errorCode = typeof err?.code === "string"
4052
+ ? err.code
4053
+ : "invalid_input";
4054
+ return {
4055
+ toolContent: baseComplete({
4056
+ status: errorCode === INTERMEDIATE_OUTPUT_DISALLOWED_CODE
4057
+ ? 403
4058
+ : 400,
4059
+ code: errorCode,
4060
+ message: err instanceof Error ? err.message : String(err),
4061
+ }),
4062
+ };
4063
+ }
4064
+ return {
4065
+ toolContent: baseComplete({
4066
+ status: 200,
4067
+ payload: { emitted: true, type: item.type },
4068
+ }),
4069
+ };
4070
+ }
4071
+ if (call.name === BUILTIN_TOOL_CONSUME_ASYNC_ACTION) {
4072
+ if (!ctx.asyncActionJobs) {
4073
+ return {
4074
+ toolContent: baseComplete({
4075
+ status: 400,
4076
+ code: "invalid_tool_context",
4077
+ message: `${BUILTIN_TOOL_CONSUME_ASYNC_ACTION} is unavailable for this deck`,
4078
+ }),
4079
+ };
4080
+ }
4081
+ let parsedArgs;
4082
+ try {
4083
+ parsedArgs = parseConsumeAsyncActionArgs(call.args);
4084
+ }
4085
+ catch (err) {
4086
+ return {
4087
+ toolContent: baseComplete({
4088
+ status: 400,
4089
+ code: "invalid_input",
4090
+ message: err instanceof Error ? err.message : String(err),
4091
+ }),
4092
+ };
4093
+ }
4094
+ const consumed = await ctx.asyncActionJobs.consume({
4095
+ ...parsedArgs,
4096
+ runDeadlineMs: ctx.runDeadlineMs,
4097
+ signal: ctx.signal,
4098
+ });
4099
+ if (!consumed.ok) {
4100
+ return {
4101
+ toolContent: baseComplete({
4102
+ status: consumed.status,
4103
+ code: consumed.code,
4104
+ message: consumed.message,
4105
+ payload: consumed.payload,
4106
+ }),
4107
+ };
4108
+ }
4109
+ return {
4110
+ toolContent: baseComplete({
4111
+ status: 200,
4112
+ payload: consumed.payload,
4113
+ }),
4114
+ };
4115
+ }
2991
4116
  if (call.name === BUILTIN_TOOL_READ_FILE) {
2992
4117
  let targetPath;
2993
4118
  try {
@@ -3452,6 +4577,139 @@ async function handleToolCall(call, ctx) {
3452
4577
  };
3453
4578
  }
3454
4579
  }
4580
+ const inheritedIntermediateOutputAllow = ctx.intermediateOutputAllow ?? true;
4581
+ const actionDeclaresIntermediateOutput = action.intermediateOutput?.emit === "allow";
4582
+ const actionIntermediateOutputAllow = inheritedIntermediateOutputAllow &&
4583
+ actionDeclaresIntermediateOutput;
4584
+ const actionIntermediateOutputItems = [];
4585
+ const actionIntermediateOutputErrorContext = inheritedIntermediateOutputAllow
4586
+ ? {
4587
+ actionName: action.name,
4588
+ parentDeckPath: ctx.parentDeck.path,
4589
+ }
4590
+ : (ctx.intermediateOutputErrorContext ?? {
4591
+ actionName: action.name,
4592
+ parentDeckPath: ctx.parentDeck.path,
4593
+ });
4594
+ const actionIntermediateOutputCapture = actionIntermediateOutputAllow
4595
+ ? (emission) => {
4596
+ actionIntermediateOutputItems.push(emission);
4597
+ ctx.onIntermediateOutputItem?.(emission);
4598
+ }
4599
+ : undefined;
4600
+ const actionAsyncStartAllowed = action.asyncStart?.mode === "allow";
4601
+ if (actionAsyncStartAllowed) {
4602
+ if (!ctx.asyncActionJobs) {
4603
+ return {
4604
+ toolContent: baseComplete({
4605
+ status: 500,
4606
+ code: "async_job_unavailable",
4607
+ message: "Async action start is configured but async job controller is unavailable",
4608
+ }),
4609
+ };
4610
+ }
4611
+ const job = ctx.asyncActionJobs.startJob({
4612
+ actionName: action.name,
4613
+ actionPath: action.path,
4614
+ });
4615
+ const controller = new AbortController();
4616
+ const childSignal = mergeAbortSignals(ctx.signal, controller.signal);
4617
+ const asyncIntermediateOutputCapture = actionIntermediateOutputAllow
4618
+ ? (emission) => {
4619
+ job.appendIntermediateOutput(emission);
4620
+ ctx.onIntermediateOutputItem?.(emission);
4621
+ }
4622
+ : undefined;
4623
+ const runPromise = (async () => {
4624
+ try {
4625
+ const result = await runDeck({
4626
+ path: action.path,
4627
+ input: actionInput,
4628
+ modelProvider: ctx.modelProvider,
4629
+ isRoot: false,
4630
+ guardrails: ctx.guardrails,
4631
+ depth: ctx.depth + 1,
4632
+ parentActionCallId: call.id,
4633
+ runId: ctx.runId,
4634
+ defaultModel: ctx.defaultModel,
4635
+ modelOverride: ctx.modelOverride,
4636
+ trace: ctx.trace,
4637
+ stream: ctx.stream,
4638
+ onStreamText: ctx.onStreamText,
4639
+ responsesMode: ctx.responsesMode,
4640
+ initialUserMessage: undefined,
4641
+ intermediateOutputAllow: actionIntermediateOutputAllow,
4642
+ onIntermediateOutputItem: asyncIntermediateOutputCapture,
4643
+ intermediateOutputErrorContext: actionIntermediateOutputErrorContext,
4644
+ parentPermissions: ctx.permissions,
4645
+ referencePermissions: action.permissions,
4646
+ referencePermissionsBaseDir: path.dirname(ctx.parentDeck.path),
4647
+ workspacePermissions: ctx.workspacePermissions,
4648
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
4649
+ sessionPermissions: ctx.sessionPermissions,
4650
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
4651
+ runDeadlineMs: ctx.runDeadlineMs,
4652
+ workerSandbox: ctx.workerSandbox,
4653
+ signal: childSignal,
4654
+ onTool: ctx.onTool,
4655
+ });
4656
+ const normalized = normalizeChildResult(result);
4657
+ if (action.responseSchema) {
4658
+ normalized.payload = validateWithSchema(action.responseSchema, normalized.payload);
4659
+ }
4660
+ job.complete(normalized);
4661
+ }
4662
+ catch (err) {
4663
+ const handled = await maybeHandleError({
4664
+ err,
4665
+ call,
4666
+ ctx,
4667
+ action,
4668
+ });
4669
+ if (handled) {
4670
+ const handledEnvelope = parseToolContentEnvelope(handled.toolContent);
4671
+ job.fail({
4672
+ state: "failed",
4673
+ status: handledEnvelope?.status,
4674
+ payload: handledEnvelope?.payload,
4675
+ message: handledEnvelope?.message ??
4676
+ (err instanceof Error ? err.message : String(err)),
4677
+ code: handledEnvelope?.code,
4678
+ meta: handledEnvelope?.meta,
4679
+ });
4680
+ return;
4681
+ }
4682
+ if (isRunCanceledError(err)) {
4683
+ job.fail({
4684
+ state: "canceled",
4685
+ status: 499,
4686
+ code: typeof err?.code === "string"
4687
+ ? err.code
4688
+ : "run_canceled",
4689
+ message: err instanceof Error ? err.message : String(err),
4690
+ });
4691
+ return;
4692
+ }
4693
+ job.fail({
4694
+ state: "failed",
4695
+ code: typeof err?.code === "string"
4696
+ ? err.code
4697
+ : undefined,
4698
+ message: err instanceof Error ? err.message : String(err),
4699
+ });
4700
+ }
4701
+ })();
4702
+ job.bindLiveRun(runPromise, controller);
4703
+ return {
4704
+ toolContent: baseComplete({
4705
+ status: 202,
4706
+ code: "async_action_started",
4707
+ payload: {
4708
+ job: job.handle,
4709
+ },
4710
+ }),
4711
+ };
4712
+ }
3455
4713
  const busyCfg = ctx.parentDeck.handlers?.onBusy ??
3456
4714
  ctx.parentDeck.handlers?.onInterval;
3457
4715
  const busyDelay = busyCfg?.delayMs ?? DEFAULT_STATUS_DELAY_MS;
@@ -3479,6 +4737,9 @@ async function handleToolCall(call, ctx) {
3479
4737
  onStreamText: ctx.onStreamText,
3480
4738
  responsesMode: ctx.responsesMode,
3481
4739
  initialUserMessage: undefined,
4740
+ intermediateOutputAllow: actionIntermediateOutputAllow,
4741
+ onIntermediateOutputItem: actionIntermediateOutputCapture,
4742
+ intermediateOutputErrorContext: actionIntermediateOutputErrorContext,
3482
4743
  parentPermissions: ctx.permissions,
3483
4744
  referencePermissions: action.permissions,
3484
4745
  referencePermissionsBaseDir: path.dirname(ctx.parentDeck.path),
@@ -3523,6 +4784,9 @@ async function handleToolCall(call, ctx) {
3523
4784
  onStreamText: ctx.onStreamText,
3524
4785
  responsesMode: ctx.responsesMode,
3525
4786
  initialUserMessage: undefined,
4787
+ intermediateOutputAllow: actionIntermediateOutputAllow,
4788
+ onIntermediateOutputItem: actionIntermediateOutputCapture,
4789
+ intermediateOutputErrorContext: actionIntermediateOutputErrorContext,
3526
4790
  permissions: ctx.permissions,
3527
4791
  workspacePermissions: ctx.workspacePermissions,
3528
4792
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
@@ -3592,6 +4856,12 @@ async function handleToolCall(call, ctx) {
3592
4856
  if (action.responseSchema) {
3593
4857
  normalized.payload = validateWithSchema(action.responseSchema, normalized.payload);
3594
4858
  }
4859
+ if (actionIntermediateOutputAllow) {
4860
+ intermediateOutputView = {
4861
+ policy: "allow",
4862
+ items: actionIntermediateOutputItems,
4863
+ };
4864
+ }
3595
4865
  const toolContent = baseComplete(normalized);
3596
4866
  if (busyCfg?.path) {
3597
4867
  const elapsedFromAction = performance.now() - started;
@@ -3615,6 +4885,9 @@ async function handleToolCall(call, ctx) {
3615
4885
  onStreamText: ctx.onStreamText,
3616
4886
  responsesMode: ctx.responsesMode,
3617
4887
  initialUserMessage: undefined,
4888
+ intermediateOutputAllow: actionIntermediateOutputAllow,
4889
+ onIntermediateOutputItem: actionIntermediateOutputCapture,
4890
+ intermediateOutputErrorContext: actionIntermediateOutputErrorContext,
3618
4891
  permissions: ctx.permissions,
3619
4892
  workspacePermissions: ctx.workspacePermissions,
3620
4893
  workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
@@ -3635,24 +4908,6 @@ async function handleToolCall(call, ctx) {
3635
4908
  }
3636
4909
  }
3637
4910
  }
3638
- const completeEventId = randomId("event");
3639
- extraMessages.push({
3640
- role: "assistant",
3641
- content: null,
3642
- tool_calls: [{
3643
- id: completeEventId,
3644
- type: "function",
3645
- function: {
3646
- name: GAMBIT_TOOL_COMPLETE,
3647
- arguments: toolContent,
3648
- },
3649
- }],
3650
- }, {
3651
- role: "tool",
3652
- tool_call_id: completeEventId,
3653
- name: GAMBIT_TOOL_COMPLETE,
3654
- content: toolContent,
3655
- });
3656
4911
  stopBusy();
3657
4912
  ctx.idle?.touch();
3658
4913
  return { toolContent, extraMessages };
@@ -3671,6 +4926,27 @@ function normalizeChildResult(result) {
3671
4926
  }
3672
4927
  return { payload: result };
3673
4928
  }
4929
+ function parseToolContentEnvelope(content) {
4930
+ try {
4931
+ const parsed = JSON.parse(content);
4932
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
4933
+ return undefined;
4934
+ }
4935
+ const rec = parsed;
4936
+ return {
4937
+ status: typeof rec.status === "number" ? rec.status : undefined,
4938
+ payload: rec.payload,
4939
+ message: typeof rec.message === "string" ? rec.message : undefined,
4940
+ code: typeof rec.code === "string" ? rec.code : undefined,
4941
+ meta: rec.meta && typeof rec.meta === "object" && !Array.isArray(rec.meta)
4942
+ ? rec.meta
4943
+ : undefined,
4944
+ };
4945
+ }
4946
+ catch {
4947
+ return undefined;
4948
+ }
4949
+ }
3674
4950
  async function runBusyHandler(args) {
3675
4951
  try {
3676
4952
  ensureRunActive(args.runDeadlineMs, args.signal);
@@ -3701,6 +4977,9 @@ async function runBusyHandler(args) {
3701
4977
  responsesMode: args.responsesMode,
3702
4978
  initialUserMessage: args.initialUserMessage,
3703
4979
  inputProvided: true,
4980
+ intermediateOutputAllow: args.intermediateOutputAllow,
4981
+ onIntermediateOutputItem: args.onIntermediateOutputItem,
4982
+ intermediateOutputErrorContext: args.intermediateOutputErrorContext,
3704
4983
  parentPermissions: args.permissions,
3705
4984
  workspacePermissions: args.workspacePermissions,
3706
4985
  workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
@@ -3787,6 +5066,9 @@ function createIdleController(args) {
3787
5066
  stream: args.stream,
3788
5067
  onStreamText: args.onStreamText,
3789
5068
  responsesMode: args.responsesMode,
5069
+ intermediateOutputAllow: args.intermediateOutputAllow,
5070
+ onIntermediateOutputItem: args.onIntermediateOutputItem,
5071
+ intermediateOutputErrorContext: args.intermediateOutputErrorContext,
3790
5072
  permissions: args.permissions,
3791
5073
  workspacePermissions: args.workspacePermissions,
3792
5074
  workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
@@ -3862,6 +5144,9 @@ async function runIdleHandler(args) {
3862
5144
  responsesMode: args.responsesMode,
3863
5145
  initialUserMessage: undefined,
3864
5146
  inputProvided: true,
5147
+ intermediateOutputAllow: args.intermediateOutputAllow,
5148
+ onIntermediateOutputItem: args.onIntermediateOutputItem,
5149
+ intermediateOutputErrorContext: args.intermediateOutputErrorContext,
3865
5150
  parentPermissions: args.permissions,
3866
5151
  workspacePermissions: args.workspacePermissions,
3867
5152
  workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
@@ -3938,6 +5223,9 @@ async function maybeHandleError(args) {
3938
5223
  responsesMode: args.ctx.responsesMode,
3939
5224
  initialUserMessage: undefined,
3940
5225
  inputProvided: true,
5226
+ intermediateOutputAllow: args.ctx.intermediateOutputAllow,
5227
+ onIntermediateOutputItem: args.ctx.onIntermediateOutputItem,
5228
+ intermediateOutputErrorContext: args.ctx.intermediateOutputErrorContext,
3941
5229
  parentPermissions: args.ctx.permissions,
3942
5230
  workspacePermissions: args.ctx.workspacePermissions,
3943
5231
  workspacePermissionsBaseDir: args.ctx.workspacePermissionsBaseDir,
@@ -3974,28 +5262,7 @@ async function maybeHandleError(args) {
3974
5262
  code,
3975
5263
  meta,
3976
5264
  });
3977
- const callId = randomId("event");
3978
- const extraMessages = [
3979
- {
3980
- role: "assistant",
3981
- content: null,
3982
- tool_calls: [{
3983
- id: callId,
3984
- type: "function",
3985
- function: {
3986
- name: GAMBIT_TOOL_COMPLETE,
3987
- arguments: content,
3988
- },
3989
- }],
3990
- },
3991
- {
3992
- role: "tool",
3993
- tool_call_id: callId,
3994
- name: GAMBIT_TOOL_COMPLETE,
3995
- content,
3996
- },
3997
- ];
3998
- return { toolContent: content, extraMessages };
5265
+ return { toolContent: content };
3999
5266
  }
4000
5267
  catch {
4001
5268
  // Fallback when the handler itself fails: still return a structured error envelope
@@ -4017,28 +5284,7 @@ async function maybeHandleError(args) {
4017
5284
  code,
4018
5285
  meta: { handlerFailed: true },
4019
5286
  });
4020
- const callId = randomId("event");
4021
- const extraMessages = [
4022
- {
4023
- role: "assistant",
4024
- content: null,
4025
- tool_calls: [{
4026
- id: callId,
4027
- type: "function",
4028
- function: {
4029
- name: GAMBIT_TOOL_COMPLETE,
4030
- arguments: content,
4031
- },
4032
- }],
4033
- },
4034
- {
4035
- role: "tool",
4036
- tool_call_id: callId,
4037
- name: GAMBIT_TOOL_COMPLETE,
4038
- content,
4039
- },
4040
- ];
4041
- return { toolContent: content, extraMessages };
5287
+ return { toolContent: content };
4042
5288
  }
4043
5289
  }
4044
5290
  function buildSystemPrompt(deck) {
@@ -4090,6 +5336,53 @@ function parseToolLimit(value, fallback, max) {
4090
5336
  return fallback;
4091
5337
  return Math.min(max, Math.max(1, Number(value)));
4092
5338
  }
5339
+ function parseAsyncActionWaitMs(value) {
5340
+ if (value === undefined || value === null)
5341
+ return 0;
5342
+ if (!Number.isInteger(value)) {
5343
+ throw new Error("wait_ms must be a non-negative integer");
5344
+ }
5345
+ return Math.min(ASYNC_ACTION_JOB_MAX_WAIT_MS, Math.max(0, Number(value)));
5346
+ }
5347
+ function parseConsumeAsyncActionArgs(args) {
5348
+ const rawJobId = typeof args.job_id === "string"
5349
+ ? args.job_id
5350
+ : typeof args.jobId === "string"
5351
+ ? args.jobId
5352
+ : "";
5353
+ const jobId = rawJobId.trim();
5354
+ if (!jobId) {
5355
+ throw new Error("job_id is required");
5356
+ }
5357
+ const cursor = args.cursor === undefined ? 0 : args.cursor;
5358
+ if (!Number.isInteger(cursor) || Number(cursor) < 0) {
5359
+ throw new Error("cursor must be a non-negative integer");
5360
+ }
5361
+ const limit = parseToolLimit(args.limit, 32, 256);
5362
+ const waitMs = parseAsyncActionWaitMs(Object.hasOwn(args, "wait_ms") ? args.wait_ms : args.waitMs);
5363
+ return {
5364
+ jobId,
5365
+ cursor: Number(cursor),
5366
+ limit,
5367
+ waitMs,
5368
+ };
5369
+ }
5370
+ function parseEmitOutputItemArgs(args) {
5371
+ const rawItem = Object.hasOwn(args, "item") ? args.item : args;
5372
+ if (!rawItem || typeof rawItem !== "object" || Array.isArray(rawItem)) {
5373
+ throw new Error("item must be an object matching the OpenResponses response item shape");
5374
+ }
5375
+ const canonicalItem = canonicalizeJsonValue(rawItem);
5376
+ if (!canonicalItem || typeof canonicalItem !== "object" ||
5377
+ Array.isArray(canonicalItem)) {
5378
+ throw new Error("item must be an object matching the OpenResponses response item shape");
5379
+ }
5380
+ const type = canonicalItem.type;
5381
+ if (typeof type !== "string" || !type.trim()) {
5382
+ throw new Error("item.type must be a non-empty string");
5383
+ }
5384
+ return canonicalItem;
5385
+ }
4093
5386
  function toStringArray(value) {
4094
5387
  if (!Array.isArray(value))
4095
5388
  return [];
@@ -4127,7 +5420,7 @@ function applySimplePatch(content, edits) {
4127
5420
  }
4128
5421
  return { next, applied };
4129
5422
  }
4130
- async function buildToolDefs(deck, permissions) {
5423
+ async function buildToolDefs(deck, permissions, options) {
4131
5424
  const defs = [];
4132
5425
  const addBuiltinTools = () => {
4133
5426
  if (hasAnyScope(permissions.read)) {
@@ -4235,48 +5528,51 @@ async function buildToolDefs(deck, permissions) {
4235
5528
  },
4236
5529
  });
4237
5530
  }
4238
- };
4239
- addBuiltinTools();
4240
- if (deck.allowEnd) {
4241
- defs.push({
4242
- type: "function",
4243
- function: {
4244
- name: GAMBIT_TOOL_END,
4245
- description: "End the current run once all goals are complete.",
4246
- parameters: {
4247
- type: "object",
4248
- properties: {
4249
- status: { type: "number" },
4250
- payload: {},
4251
- message: { type: "string" },
4252
- code: { type: "string" },
4253
- meta: { type: "object" },
5531
+ if (options?.parentActionCallId !== undefined &&
5532
+ (options.intermediateOutputAllow ?? true)) {
5533
+ defs.push({
5534
+ type: "function",
5535
+ function: {
5536
+ name: BUILTIN_TOOL_EMIT_OUTPUT_ITEM,
5537
+ description: "Emit a supplemental output item to the parent before action completion.",
5538
+ parameters: {
5539
+ type: "object",
5540
+ properties: {
5541
+ item: {
5542
+ type: "object",
5543
+ additionalProperties: true,
5544
+ },
5545
+ },
5546
+ required: ["item"],
5547
+ additionalProperties: false,
4254
5548
  },
4255
- additionalProperties: true,
4256
5549
  },
4257
- },
4258
- });
4259
- }
4260
- if (deck.respond) {
4261
- defs.push({
4262
- type: "function",
4263
- function: {
4264
- name: GAMBIT_TOOL_RESPOND,
4265
- description: "Finish the current deck with a structured response.",
4266
- parameters: {
4267
- type: "object",
4268
- properties: {
4269
- status: { type: "number" },
4270
- payload: {},
4271
- message: { type: "string" },
4272
- code: { type: "string" },
4273
- meta: { type: "object" },
5550
+ });
5551
+ }
5552
+ if (options?.asyncActionJobs &&
5553
+ (options.asyncActionJobs.hasConfiguredAsyncActions ||
5554
+ options.asyncActionJobs.hasLiveJobs())) {
5555
+ defs.push({
5556
+ type: "function",
5557
+ function: {
5558
+ name: BUILTIN_TOOL_CONSUME_ASYNC_ACTION,
5559
+ description: "Consume ordered async action job events/results by cursor.",
5560
+ parameters: {
5561
+ type: "object",
5562
+ properties: {
5563
+ job_id: { type: "string" },
5564
+ cursor: { type: "number" },
5565
+ limit: { type: "number" },
5566
+ wait_ms: { type: "number" },
5567
+ },
5568
+ required: ["job_id"],
5569
+ additionalProperties: false,
4274
5570
  },
4275
- additionalProperties: true,
4276
5571
  },
4277
- },
4278
- });
4279
- }
5572
+ });
5573
+ }
5574
+ };
5575
+ addBuiltinTools();
4280
5576
  for (const action of deck.actionDecks) {
4281
5577
  if (isBuiltinTool(action.name)) {
4282
5578
  throw new Error(`Action name ${action.name} conflicts with a built-in tool name`);