@bolt-foundry/gambit-core 0.8.1 → 0.8.5-rc.3

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 (262) hide show
  1. package/README.md +60 -34
  2. package/cards/context.card.md +5 -5
  3. package/cards/generate-test-input.card.md +12 -0
  4. package/{script/deps/jsr.io/@std/collections/1.1.4 → esm/deps/jsr.io/@std/collections/1.1.5}/deep_merge.d.ts +2 -2
  5. package/esm/deps/jsr.io/@std/collections/{1.1.4 → 1.1.5}/deep_merge.d.ts.map +1 -1
  6. package/esm/deps/jsr.io/@std/collections/{1.1.4 → 1.1.5}/deep_merge.js +29 -19
  7. package/esm/deps/jsr.io/@std/toml/1.0.11/_parser.js +1 -1
  8. package/esm/mod.d.ts +20 -10
  9. package/esm/mod.d.ts.map +1 -1
  10. package/esm/mod.js +11 -5
  11. package/esm/schemas/graders/contexts/conversation.d.ts +22 -0
  12. package/esm/schemas/graders/contexts/conversation.d.ts.map +1 -0
  13. package/esm/schemas/graders/contexts/conversation.js +17 -0
  14. package/esm/schemas/graders/contexts/conversation.zod.d.ts +3 -0
  15. package/esm/schemas/graders/contexts/conversation.zod.d.ts.map +1 -0
  16. package/esm/schemas/graders/contexts/conversation.zod.js +2 -0
  17. package/esm/schemas/graders/contexts/conversation_tools.d.ts +31 -0
  18. package/esm/schemas/graders/contexts/conversation_tools.d.ts.map +1 -0
  19. package/esm/schemas/graders/contexts/conversation_tools.js +25 -0
  20. package/esm/schemas/graders/contexts/conversation_tools.zod.d.ts +3 -0
  21. package/esm/schemas/graders/contexts/conversation_tools.zod.d.ts.map +1 -0
  22. package/esm/schemas/graders/contexts/conversation_tools.zod.js +2 -0
  23. package/esm/schemas/graders/contexts/tools.d.ts +4 -0
  24. package/esm/schemas/graders/contexts/tools.d.ts.map +1 -0
  25. package/esm/schemas/graders/contexts/tools.js +3 -0
  26. package/esm/schemas/graders/contexts/tools.zod.d.ts +3 -0
  27. package/esm/schemas/graders/contexts/tools.zod.d.ts.map +1 -0
  28. package/esm/schemas/graders/contexts/tools.zod.js +2 -0
  29. package/esm/schemas/graders/contexts/turn.d.ts +10 -0
  30. package/esm/schemas/graders/contexts/turn.d.ts.map +1 -0
  31. package/esm/schemas/graders/contexts/turn.js +8 -0
  32. package/esm/schemas/graders/contexts/turn.zod.d.ts +3 -0
  33. package/esm/schemas/graders/contexts/turn.zod.d.ts.map +1 -0
  34. package/esm/schemas/graders/contexts/turn.zod.js +2 -0
  35. package/esm/schemas/graders/contexts/turn_tools.d.ts +32 -0
  36. package/esm/schemas/graders/contexts/turn_tools.d.ts.map +1 -0
  37. package/esm/schemas/graders/contexts/turn_tools.js +28 -0
  38. package/esm/schemas/graders/contexts/turn_tools.zod.d.ts +3 -0
  39. package/esm/schemas/graders/contexts/turn_tools.zod.d.ts.map +1 -0
  40. package/esm/schemas/graders/contexts/turn_tools.zod.js +2 -0
  41. package/esm/schemas/graders/grader_output.d.ts +10 -0
  42. package/esm/schemas/graders/grader_output.d.ts.map +1 -0
  43. package/esm/schemas/graders/grader_output.js +8 -0
  44. package/esm/schemas/graders/grader_output.zod.d.ts +3 -0
  45. package/esm/schemas/graders/grader_output.zod.d.ts.map +1 -0
  46. package/esm/schemas/graders/grader_output.zod.js +2 -0
  47. package/esm/schemas/graders/respond.d.ts +12 -0
  48. package/esm/schemas/graders/respond.d.ts.map +1 -0
  49. package/esm/schemas/graders/respond.js +10 -0
  50. package/esm/schemas/graders/respond.zod.d.ts +3 -0
  51. package/esm/schemas/graders/respond.zod.d.ts.map +1 -0
  52. package/esm/schemas/graders/respond.zod.js +2 -0
  53. package/esm/schemas/scenarios/plain_chat_input_optional.d.ts +5 -0
  54. package/esm/schemas/scenarios/plain_chat_input_optional.d.ts.map +1 -0
  55. package/esm/schemas/scenarios/plain_chat_input_optional.js +5 -0
  56. package/esm/schemas/scenarios/plain_chat_input_optional.zod.d.ts +3 -0
  57. package/esm/schemas/scenarios/plain_chat_input_optional.zod.d.ts.map +1 -0
  58. package/esm/schemas/scenarios/plain_chat_input_optional.zod.js +2 -0
  59. package/esm/schemas/scenarios/plain_chat_output.d.ts +5 -0
  60. package/esm/schemas/scenarios/plain_chat_output.d.ts.map +1 -0
  61. package/esm/schemas/scenarios/plain_chat_output.js +4 -0
  62. package/esm/schemas/scenarios/plain_chat_output.zod.d.ts +3 -0
  63. package/esm/schemas/scenarios/plain_chat_output.zod.d.ts.map +1 -0
  64. package/esm/schemas/scenarios/plain_chat_output.zod.js +2 -0
  65. package/esm/src/builtins.d.ts +2 -0
  66. package/esm/src/builtins.d.ts.map +1 -1
  67. package/esm/src/builtins.js +45 -1
  68. package/esm/src/constants.d.ts +4 -0
  69. package/esm/src/constants.d.ts.map +1 -1
  70. package/esm/src/constants.js +5 -0
  71. package/esm/src/definitions.d.ts +5 -1
  72. package/esm/src/definitions.d.ts.map +1 -1
  73. package/esm/src/loader.d.ts.map +1 -1
  74. package/esm/src/loader.js +119 -13
  75. package/esm/src/markdown.d.ts.map +1 -1
  76. package/esm/src/markdown.js +222 -53
  77. package/esm/src/permissions.d.ts +143 -0
  78. package/esm/src/permissions.d.ts.map +1 -0
  79. package/esm/src/permissions.js +406 -0
  80. package/esm/src/render.d.ts.map +1 -1
  81. package/esm/src/render.js +22 -8
  82. package/esm/src/runtime.d.ts +28 -2
  83. package/esm/src/runtime.d.ts.map +1 -1
  84. package/esm/src/runtime.js +3051 -100
  85. package/esm/src/runtime_exec_host.d.ts +6 -0
  86. package/esm/src/runtime_exec_host.d.ts.map +1 -0
  87. package/esm/src/runtime_exec_host.js +17 -0
  88. package/esm/src/runtime_exec_host_contract.d.ts +23 -0
  89. package/esm/src/runtime_exec_host_contract.d.ts.map +1 -0
  90. package/esm/src/runtime_exec_host_contract.js +14 -0
  91. package/esm/src/runtime_exec_host_deno.d.ts +3 -0
  92. package/esm/src/runtime_exec_host_deno.d.ts.map +1 -0
  93. package/esm/src/runtime_exec_host_deno.js +35 -0
  94. package/esm/src/runtime_exec_host_unsupported.d.ts +3 -0
  95. package/esm/src/runtime_exec_host_unsupported.d.ts.map +1 -0
  96. package/esm/src/runtime_exec_host_unsupported.js +8 -0
  97. package/esm/src/runtime_worker_host.d.ts +6 -0
  98. package/esm/src/runtime_worker_host.d.ts.map +1 -0
  99. package/esm/src/runtime_worker_host.js +17 -0
  100. package/esm/src/runtime_worker_host_contract.d.ts +33 -0
  101. package/esm/src/runtime_worker_host_contract.d.ts.map +1 -0
  102. package/esm/src/runtime_worker_host_contract.js +14 -0
  103. package/esm/src/runtime_worker_host_deno.d.ts +3 -0
  104. package/esm/src/runtime_worker_host_deno.d.ts.map +1 -0
  105. package/esm/src/runtime_worker_host_deno.js +26 -0
  106. package/esm/src/runtime_worker_host_unsupported.d.ts +3 -0
  107. package/esm/src/runtime_worker_host_unsupported.d.ts.map +1 -0
  108. package/esm/src/runtime_worker_host_unsupported.js +8 -0
  109. package/esm/src/state.d.ts +4 -1
  110. package/esm/src/state.d.ts.map +1 -1
  111. package/esm/src/state.js +48 -2
  112. package/esm/src/types.d.ts +381 -1
  113. package/esm/src/types.d.ts.map +1 -1
  114. package/esm/src/types.js +102 -1
  115. package/package.json +73 -2
  116. package/schemas/graders/contexts/conversation.ts +32 -9
  117. package/schemas/graders/contexts/conversation.zod.ts +1 -0
  118. package/schemas/graders/contexts/conversation_tools.ts +63 -0
  119. package/schemas/graders/contexts/conversation_tools.zod.ts +1 -0
  120. package/schemas/graders/contexts/tools.ts +5 -0
  121. package/schemas/graders/contexts/tools.zod.ts +1 -0
  122. package/schemas/graders/contexts/turn.ts +8 -1
  123. package/schemas/graders/contexts/turn.zod.ts +1 -0
  124. package/schemas/graders/contexts/turn_tools.ts +63 -0
  125. package/schemas/graders/contexts/turn_tools.zod.ts +1 -0
  126. package/schemas/graders/grader_output.ts +15 -0
  127. package/schemas/graders/grader_output.zod.ts +1 -0
  128. package/schemas/graders/respond.ts +13 -3
  129. package/schemas/graders/respond.zod.ts +1 -0
  130. package/schemas/scenarios/plain_chat_input_optional.ts +6 -0
  131. package/schemas/scenarios/plain_chat_input_optional.zod.ts +1 -0
  132. package/schemas/scenarios/plain_chat_output.ts +5 -0
  133. package/schemas/scenarios/plain_chat_output.zod.ts +1 -0
  134. package/{esm/deps/jsr.io/@std/collections/1.1.4 → script/deps/jsr.io/@std/collections/1.1.5}/deep_merge.d.ts +2 -2
  135. package/script/deps/jsr.io/@std/collections/{1.1.4 → 1.1.5}/deep_merge.d.ts.map +1 -1
  136. package/script/deps/jsr.io/@std/collections/{1.1.4 → 1.1.5}/deep_merge.js +29 -19
  137. package/script/deps/jsr.io/@std/toml/1.0.11/_parser.js +1 -1
  138. package/script/mod.d.ts +20 -10
  139. package/script/mod.d.ts.map +1 -1
  140. package/script/mod.js +25 -9
  141. package/script/schemas/graders/contexts/conversation.d.ts +22 -0
  142. package/script/schemas/graders/contexts/conversation.d.ts.map +1 -0
  143. package/script/schemas/graders/contexts/conversation.js +20 -0
  144. package/script/schemas/graders/contexts/conversation.zod.d.ts +3 -0
  145. package/script/schemas/graders/contexts/conversation.zod.d.ts.map +1 -0
  146. package/script/schemas/graders/contexts/conversation.zod.js +9 -0
  147. package/script/schemas/graders/contexts/conversation_tools.d.ts +31 -0
  148. package/script/schemas/graders/contexts/conversation_tools.d.ts.map +1 -0
  149. package/script/schemas/graders/contexts/conversation_tools.js +28 -0
  150. package/script/schemas/graders/contexts/conversation_tools.zod.d.ts +3 -0
  151. package/script/schemas/graders/contexts/conversation_tools.zod.d.ts.map +1 -0
  152. package/script/schemas/graders/contexts/conversation_tools.zod.js +9 -0
  153. package/script/schemas/graders/contexts/tools.d.ts +4 -0
  154. package/script/schemas/graders/contexts/tools.d.ts.map +1 -0
  155. package/script/schemas/graders/contexts/tools.js +12 -0
  156. package/script/schemas/graders/contexts/tools.zod.d.ts +3 -0
  157. package/script/schemas/graders/contexts/tools.zod.d.ts.map +1 -0
  158. package/script/schemas/graders/contexts/tools.zod.js +9 -0
  159. package/script/schemas/graders/contexts/turn.d.ts +10 -0
  160. package/script/schemas/graders/contexts/turn.d.ts.map +1 -0
  161. package/script/schemas/graders/contexts/turn.js +10 -0
  162. package/script/schemas/graders/contexts/turn.zod.d.ts +3 -0
  163. package/script/schemas/graders/contexts/turn.zod.d.ts.map +1 -0
  164. package/script/schemas/graders/contexts/turn.zod.js +9 -0
  165. package/script/schemas/graders/contexts/turn_tools.d.ts +32 -0
  166. package/script/schemas/graders/contexts/turn_tools.d.ts.map +1 -0
  167. package/script/schemas/graders/contexts/turn_tools.js +31 -0
  168. package/script/schemas/graders/contexts/turn_tools.zod.d.ts +3 -0
  169. package/script/schemas/graders/contexts/turn_tools.zod.d.ts.map +1 -0
  170. package/script/schemas/graders/contexts/turn_tools.zod.js +9 -0
  171. package/script/schemas/graders/grader_output.d.ts +10 -0
  172. package/script/schemas/graders/grader_output.d.ts.map +1 -0
  173. package/script/schemas/graders/grader_output.js +10 -0
  174. package/script/schemas/graders/grader_output.zod.d.ts +3 -0
  175. package/script/schemas/graders/grader_output.zod.d.ts.map +1 -0
  176. package/script/schemas/graders/grader_output.zod.js +9 -0
  177. package/script/schemas/graders/respond.d.ts +12 -0
  178. package/script/schemas/graders/respond.d.ts.map +1 -0
  179. package/script/schemas/graders/respond.js +12 -0
  180. package/script/schemas/graders/respond.zod.d.ts +3 -0
  181. package/script/schemas/graders/respond.zod.d.ts.map +1 -0
  182. package/script/schemas/graders/respond.zod.js +9 -0
  183. package/script/schemas/scenarios/plain_chat_input_optional.d.ts +5 -0
  184. package/script/schemas/scenarios/plain_chat_input_optional.d.ts.map +1 -0
  185. package/script/schemas/scenarios/plain_chat_input_optional.js +7 -0
  186. package/script/schemas/scenarios/plain_chat_input_optional.zod.d.ts +3 -0
  187. package/script/schemas/scenarios/plain_chat_input_optional.zod.d.ts.map +1 -0
  188. package/script/schemas/scenarios/plain_chat_input_optional.zod.js +9 -0
  189. package/script/schemas/scenarios/plain_chat_output.d.ts +5 -0
  190. package/script/schemas/scenarios/plain_chat_output.d.ts.map +1 -0
  191. package/script/schemas/scenarios/plain_chat_output.js +6 -0
  192. package/script/schemas/scenarios/plain_chat_output.zod.d.ts +3 -0
  193. package/script/schemas/scenarios/plain_chat_output.zod.d.ts.map +1 -0
  194. package/script/schemas/scenarios/plain_chat_output.zod.js +9 -0
  195. package/script/src/builtins.d.ts +2 -0
  196. package/script/src/builtins.d.ts.map +1 -1
  197. package/script/src/builtins.js +47 -1
  198. package/script/src/constants.d.ts +4 -0
  199. package/script/src/constants.d.ts.map +1 -1
  200. package/script/src/constants.js +6 -1
  201. package/script/src/definitions.d.ts +5 -1
  202. package/script/src/definitions.d.ts.map +1 -1
  203. package/script/src/loader.d.ts.map +1 -1
  204. package/script/src/loader.js +118 -12
  205. package/script/src/markdown.d.ts.map +1 -1
  206. package/script/src/markdown.js +221 -52
  207. package/script/src/permissions.d.ts +143 -0
  208. package/script/src/permissions.d.ts.map +1 -0
  209. package/script/src/permissions.js +453 -0
  210. package/script/src/render.d.ts.map +1 -1
  211. package/script/src/render.js +22 -8
  212. package/script/src/runtime.d.ts +28 -2
  213. package/script/src/runtime.d.ts.map +1 -1
  214. package/script/src/runtime.js +3053 -99
  215. package/script/src/runtime_exec_host.d.ts +6 -0
  216. package/script/src/runtime_exec_host.d.ts.map +1 -0
  217. package/script/src/runtime_exec_host.js +56 -0
  218. package/script/src/runtime_exec_host_contract.d.ts +23 -0
  219. package/script/src/runtime_exec_host_contract.d.ts.map +1 -0
  220. package/script/src/runtime_exec_host_contract.js +18 -0
  221. package/script/src/runtime_exec_host_deno.d.ts +3 -0
  222. package/script/src/runtime_exec_host_deno.d.ts.map +1 -0
  223. package/script/src/runtime_exec_host_deno.js +71 -0
  224. package/script/src/runtime_exec_host_unsupported.d.ts +3 -0
  225. package/script/src/runtime_exec_host_unsupported.d.ts.map +1 -0
  226. package/script/src/runtime_exec_host_unsupported.js +11 -0
  227. package/script/src/runtime_worker_host.d.ts +6 -0
  228. package/script/src/runtime_worker_host.d.ts.map +1 -0
  229. package/script/src/runtime_worker_host.js +56 -0
  230. package/script/src/runtime_worker_host_contract.d.ts +33 -0
  231. package/script/src/runtime_worker_host_contract.d.ts.map +1 -0
  232. package/script/src/runtime_worker_host_contract.js +18 -0
  233. package/script/src/runtime_worker_host_deno.d.ts +3 -0
  234. package/script/src/runtime_worker_host_deno.d.ts.map +1 -0
  235. package/script/src/runtime_worker_host_deno.js +62 -0
  236. package/script/src/runtime_worker_host_unsupported.d.ts +3 -0
  237. package/script/src/runtime_worker_host_unsupported.d.ts.map +1 -0
  238. package/script/src/runtime_worker_host_unsupported.js +11 -0
  239. package/script/src/state.d.ts +4 -1
  240. package/script/src/state.d.ts.map +1 -1
  241. package/script/src/state.js +48 -2
  242. package/script/src/types.d.ts +381 -1
  243. package/script/src/types.d.ts.map +1 -1
  244. package/script/src/types.js +103 -0
  245. package/esm/deps/jsr.io/@std/collections/1.1.4/_utils.d.ts +0 -6
  246. package/esm/deps/jsr.io/@std/collections/1.1.4/_utils.d.ts.map +0 -1
  247. package/esm/deps/jsr.io/@std/collections/1.1.4/_utils.js +0 -18
  248. package/esm/src/openai_compat.d.ts +0 -63
  249. package/esm/src/openai_compat.d.ts.map +0 -1
  250. package/esm/src/openai_compat.js +0 -272
  251. package/esm/src/providers/openrouter.d.ts +0 -8
  252. package/esm/src/providers/openrouter.d.ts.map +0 -1
  253. package/esm/src/providers/openrouter.js +0 -168
  254. package/script/deps/jsr.io/@std/collections/1.1.4/_utils.d.ts +0 -6
  255. package/script/deps/jsr.io/@std/collections/1.1.4/_utils.d.ts.map +0 -1
  256. package/script/deps/jsr.io/@std/collections/1.1.4/_utils.js +0 -21
  257. package/script/src/openai_compat.d.ts +0 -63
  258. package/script/src/openai_compat.d.ts.map +0 -1
  259. package/script/src/openai_compat.js +0 -276
  260. package/script/src/providers/openrouter.d.ts +0 -8
  261. package/script/src/providers/openrouter.d.ts.map +0 -1
  262. package/script/src/providers/openrouter.js +0 -207
@@ -1,6 +1,10 @@
1
+ import * as dntShim from "../_dnt.shims.js";
1
2
  import * as path from "../deps/jsr.io/@std/path/1.1.4/mod.js";
2
- import { DEFAULT_GUARDRAILS, DEFAULT_STATUS_DELAY_MS, GAMBIT_TOOL_COMPLETE, GAMBIT_TOOL_END, GAMBIT_TOOL_INIT, GAMBIT_TOOL_RESPOND, } from "./constants.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
4
  import { loadDeck } from "./loader.js";
5
+ import { canReadPath, canRunCommand, canRunPath, canWritePath, intersectPermissions, resolveEffectivePermissions, } from "./permissions.js";
6
+ import { ExecToolUnsupportedHostError, executeBuiltinCommand, } from "./runtime_exec_host.js";
7
+ import { createWorkerSandboxBridge, isWorkerSandboxHostSupported, WorkerSandboxUnsupportedHostError, } from "./runtime_worker_host.js";
4
8
  import { assertZodSchema, toJsonSchema, validateWithSchema } from "./schema.js";
5
9
  export function isGambitEndSignal(value) {
6
10
  return Boolean(value &&
@@ -13,6 +17,300 @@ function randomId(prefix) {
13
17
  // Keep IDs short enough for OpenAI/OpenRouter tool_call id limits (~40 chars).
14
18
  return `${prefix}-${suffix}`;
15
19
  }
20
+ const WORKER_SANDBOX_ENV = "GAMBIT_DECK_WORKER_SANDBOX";
21
+ const WORKER_TIMEOUT_MESSAGE = "Timeout exceeded";
22
+ const RUN_CANCELED_MESSAGE = "Run canceled";
23
+ const WORKER_SANDBOX_SIGNAL_UNSUPPORTED_MESSAGE = "workerSandbox is unsupported when `signal` is provided.";
24
+ const INSPECT_WORKER_TIMEOUT_MS = 1_500;
25
+ const INSPECT_WORKER_TIMEOUT_MESSAGE = "Deck inspection timed out";
26
+ const BUILTIN_TOOL_READ_FILE = "read_file";
27
+ const BUILTIN_TOOL_LIST_DIR = "list_dir";
28
+ const BUILTIN_TOOL_GREP_FILES = "grep_files";
29
+ const BUILTIN_TOOL_APPLY_PATCH = "apply_patch";
30
+ const BUILTIN_TOOL_EXEC = "exec";
31
+ const BUILTIN_TOOL_NAMES = new Set([
32
+ BUILTIN_TOOL_READ_FILE,
33
+ BUILTIN_TOOL_LIST_DIR,
34
+ BUILTIN_TOOL_GREP_FILES,
35
+ BUILTIN_TOOL_APPLY_PATCH,
36
+ BUILTIN_TOOL_EXEC,
37
+ ]);
38
+ const TRUSTED_SCHEMA_IMPORT_PREFIXES = [
39
+ "@bolt-foundry/gambit-core/schemas",
40
+ "gambit://schemas",
41
+ ];
42
+ export class RunCanceledError extends Error {
43
+ constructor(message = RUN_CANCELED_MESSAGE) {
44
+ super(message);
45
+ Object.defineProperty(this, "code", {
46
+ enumerable: true,
47
+ configurable: true,
48
+ writable: true,
49
+ value: "run_canceled"
50
+ });
51
+ this.name = "RunCanceledError";
52
+ }
53
+ }
54
+ class WorkerSandboxSignalUnsupportedError extends Error {
55
+ constructor(message = WORKER_SANDBOX_SIGNAL_UNSUPPORTED_MESSAGE) {
56
+ super(message);
57
+ Object.defineProperty(this, "code", {
58
+ enumerable: true,
59
+ configurable: true,
60
+ writable: true,
61
+ value: "worker_sandbox_signal_unsupported"
62
+ });
63
+ this.name = "WorkerSandboxSignalUnsupportedError";
64
+ }
65
+ }
66
+ export function isRunCanceledError(err) {
67
+ if (!err || typeof err !== "object")
68
+ return false;
69
+ const name = err.name;
70
+ const code = err.code;
71
+ if (name === "RunCanceledError" || code === "run_canceled")
72
+ return true;
73
+ if (name === "AbortError")
74
+ return true;
75
+ return false;
76
+ }
77
+ function shouldUseWorkerSandbox() {
78
+ let raw;
79
+ try {
80
+ raw = dntShim.Deno.env.get(WORKER_SANDBOX_ENV);
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ raw = raw?.trim().toLowerCase();
86
+ return raw === "1" || raw === "true" || raw === "yes";
87
+ }
88
+ function normalizedScopeToWire(scope) {
89
+ if (scope.all)
90
+ return true;
91
+ if (scope.values.size === 0)
92
+ return false;
93
+ return Array.from(scope.values).sort();
94
+ }
95
+ function normalizedRunToWire(scope) {
96
+ if (scope.all)
97
+ return true;
98
+ if (scope.paths.size === 0 && scope.commands.size === 0)
99
+ return false;
100
+ return {
101
+ paths: Array.from(scope.paths).sort(),
102
+ commands: Array.from(scope.commands).sort(),
103
+ };
104
+ }
105
+ function toWirePermissionSet(set) {
106
+ return {
107
+ baseDir: set.baseDir,
108
+ read: normalizedScopeToWire(set.read),
109
+ write: normalizedScopeToWire(set.write),
110
+ run: normalizedRunToWire(set.run),
111
+ net: normalizedScopeToWire(set.net),
112
+ env: normalizedScopeToWire(set.env),
113
+ };
114
+ }
115
+ function wireScopeToNormalized(scope) {
116
+ if (scope === true)
117
+ return { all: true, values: new Set() };
118
+ if (scope === false)
119
+ return { all: false, values: new Set() };
120
+ return { all: false, values: new Set(scope) };
121
+ }
122
+ function wireRunToNormalized(scope) {
123
+ if (scope === true) {
124
+ return {
125
+ all: true,
126
+ paths: new Set(),
127
+ commands: new Set(),
128
+ };
129
+ }
130
+ if (scope === false) {
131
+ return {
132
+ all: false,
133
+ paths: new Set(),
134
+ commands: new Set(),
135
+ };
136
+ }
137
+ return {
138
+ all: false,
139
+ paths: new Set(scope.paths),
140
+ commands: new Set(scope.commands),
141
+ };
142
+ }
143
+ function fromWirePermissionSet(set) {
144
+ return {
145
+ baseDir: set.baseDir,
146
+ read: wireScopeToNormalized(set.read),
147
+ write: wireScopeToNormalized(set.write),
148
+ run: wireRunToNormalized(set.run),
149
+ net: wireScopeToNormalized(set.net),
150
+ env: wireScopeToNormalized(set.env),
151
+ };
152
+ }
153
+ function normalizePermissionBaseDir(set, baseDir) {
154
+ return {
155
+ ...set,
156
+ baseDir,
157
+ read: { all: set.read.all, values: new Set(set.read.values) },
158
+ write: { all: set.write.all, values: new Set(set.write.values) },
159
+ run: {
160
+ all: set.run.all,
161
+ paths: new Set(set.run.paths),
162
+ commands: new Set(set.run.commands),
163
+ },
164
+ net: { all: set.net.all, values: new Set(set.net.values) },
165
+ env: { all: set.env.all, values: new Set(set.env.values) },
166
+ };
167
+ }
168
+ function deadlineForRun(guardrails, existing) {
169
+ const timeoutDeadline = performance.now() + guardrails.timeoutMs;
170
+ if (typeof existing === "number" && Number.isFinite(existing)) {
171
+ return Math.min(existing, timeoutDeadline);
172
+ }
173
+ return timeoutDeadline;
174
+ }
175
+ function ensureNotExpired(deadlineMs) {
176
+ if (performance.now() > deadlineMs) {
177
+ throw new Error(WORKER_TIMEOUT_MESSAGE);
178
+ }
179
+ }
180
+ function throwIfCanceled(signal) {
181
+ if (!signal?.aborted)
182
+ return;
183
+ const reason = signal.reason;
184
+ if (typeof reason === "string" && reason.trim().length > 0) {
185
+ throw new RunCanceledError(reason);
186
+ }
187
+ if (reason instanceof Error && reason.message.trim().length > 0) {
188
+ throw new RunCanceledError(reason.message);
189
+ }
190
+ throw new RunCanceledError();
191
+ }
192
+ function ensureRunActive(deadlineMs, signal) {
193
+ throwIfCanceled(signal);
194
+ ensureNotExpired(deadlineMs);
195
+ }
196
+ function isTrustedSchemaImportKey(key) {
197
+ const normalized = key.trim();
198
+ if (!normalized)
199
+ return false;
200
+ return TRUSTED_SCHEMA_IMPORT_PREFIXES.some((prefix) => normalized === prefix || normalized.startsWith(`${prefix}/`));
201
+ }
202
+ function tryReadWorkspaceConfigPath(deckPath) {
203
+ const startDir = path.dirname(path.resolve(deckPath));
204
+ let current = startDir;
205
+ while (true) {
206
+ const denoJson = path.join(current, "deno.json");
207
+ const denoJsonc = path.join(current, "deno.jsonc");
208
+ try {
209
+ if (dntShim.Deno.statSync(denoJson).isFile)
210
+ return denoJson;
211
+ }
212
+ catch {
213
+ // continue search
214
+ }
215
+ try {
216
+ if (dntShim.Deno.statSync(denoJsonc).isFile)
217
+ return denoJsonc;
218
+ }
219
+ catch {
220
+ // continue search
221
+ }
222
+ const parent = path.dirname(current);
223
+ if (parent === current)
224
+ break;
225
+ current = parent;
226
+ }
227
+ return undefined;
228
+ }
229
+ function readWorkspaceImportMapKeys(configPath) {
230
+ const text = dntShim.Deno.readTextFileSync(configPath);
231
+ const parsed = parseWorkspaceConfig(text);
232
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) ||
233
+ !parsed.imports || typeof parsed.imports !== "object" ||
234
+ Array.isArray(parsed.imports)) {
235
+ return [];
236
+ }
237
+ return Object.keys(parsed.imports);
238
+ }
239
+ function parseWorkspaceConfig(text) {
240
+ try {
241
+ return JSON.parse(text);
242
+ }
243
+ catch {
244
+ const stripped = stripJsonComments(text);
245
+ return JSON.parse(stripped);
246
+ }
247
+ }
248
+ function stripJsonComments(text) {
249
+ let out = "";
250
+ let inString = false;
251
+ let escapeNext = false;
252
+ let inLineComment = false;
253
+ let inBlockComment = false;
254
+ for (let i = 0; i < text.length; i++) {
255
+ const ch = text[i];
256
+ const next = text[i + 1];
257
+ if (inLineComment) {
258
+ if (ch === "\n") {
259
+ inLineComment = false;
260
+ out += ch;
261
+ }
262
+ continue;
263
+ }
264
+ if (inBlockComment) {
265
+ if (ch === "*" && next === "/") {
266
+ inBlockComment = false;
267
+ i++;
268
+ }
269
+ continue;
270
+ }
271
+ if (inString) {
272
+ out += ch;
273
+ if (escapeNext) {
274
+ escapeNext = false;
275
+ }
276
+ else if (ch === "\\") {
277
+ escapeNext = true;
278
+ }
279
+ else if (ch === '"') {
280
+ inString = false;
281
+ }
282
+ continue;
283
+ }
284
+ if (ch === '"') {
285
+ inString = true;
286
+ out += ch;
287
+ continue;
288
+ }
289
+ if (ch === "/" && next === "/") {
290
+ inLineComment = true;
291
+ i++;
292
+ continue;
293
+ }
294
+ if (ch === "/" && next === "*") {
295
+ inBlockComment = true;
296
+ i++;
297
+ continue;
298
+ }
299
+ out += ch;
300
+ }
301
+ return out;
302
+ }
303
+ function enforceTrustedSchemaImportMapPolicy(deckPath) {
304
+ if (deckPath.startsWith("gambit://"))
305
+ return;
306
+ const configPath = tryReadWorkspaceConfigPath(deckPath);
307
+ if (!configPath)
308
+ return;
309
+ const violations = readWorkspaceImportMapKeys(configPath).filter((key) => isTrustedSchemaImportKey(key));
310
+ if (violations.length === 0)
311
+ return;
312
+ throw new Error(`[gambit] trust-boundary violation: workspace import map at ${configPath} remaps trusted schema namespace (${violations.join(", ")})`);
313
+ }
16
314
  export async function runDeck(opts) {
17
315
  const guardrails = {
18
316
  ...DEFAULT_GUARDRAILS,
@@ -25,34 +323,241 @@ export async function runDeck(opts) {
25
323
  throw new Error(`Max depth ${guardrails.maxDepth} exceeded`);
26
324
  }
27
325
  const runId = opts.runId ?? opts.state?.runId ?? randomId("run");
28
- const deck = await loadDeck(opts.path);
29
- const deckGuardrails = deck.guardrails ?? {};
30
- const effectiveGuardrails = {
31
- ...guardrails,
32
- ...deckGuardrails,
33
- };
326
+ enforceTrustedSchemaImportMapPolicy(opts.path);
327
+ const workerSandboxRequested = opts.workerSandbox ??
328
+ shouldUseWorkerSandbox();
329
+ if (workerSandboxRequested && !isWorkerSandboxHostSupported()) {
330
+ throw new WorkerSandboxUnsupportedHostError();
331
+ }
332
+ if (workerSandboxRequested && opts.signal) {
333
+ throw new WorkerSandboxSignalUnsupportedError();
334
+ }
335
+ const workerSandbox = workerSandboxRequested;
34
336
  const isRoot = Boolean(inferredRoot);
35
- ensureSchemaPresence(deck, isRoot);
36
- const resolvedInput = resolveInput({
37
- deck,
38
- input: opts.input,
39
- state: opts.state,
40
- isRoot,
41
- initialUserMessage: opts.initialUserMessage,
42
- });
43
- const validatedInput = validateInput(deck, resolvedInput, isRoot, opts.allowRootStringInput ?? false);
44
337
  const shouldEmitRun = opts.depth === undefined || opts.depth === 0;
45
- if (shouldEmitRun) {
46
- opts.trace?.({
47
- type: "run.start",
48
- runId,
49
- deckPath: deck.path,
50
- input: validatedInput,
51
- initialUserMessage: opts
52
- .initialUserMessage,
53
- });
54
- }
338
+ let canceled = false;
339
+ let cancelHandled = false;
340
+ const handleCancel = async () => {
341
+ if (cancelHandled)
342
+ return;
343
+ cancelHandled = true;
344
+ if (!opts.onCancel)
345
+ return;
346
+ try {
347
+ await opts.onCancel();
348
+ }
349
+ catch (err) {
350
+ logger.warn(`[gambit] runDeck onCancel callback failed: ${err instanceof Error ? err.message : String(err)}`);
351
+ }
352
+ };
55
353
  try {
354
+ throwIfCanceled(opts.signal);
355
+ if (workerSandbox) {
356
+ const preInspectRunDeadlineMs = deadlineForRun(guardrails, opts.runDeadlineMs);
357
+ ensureRunActive(preInspectRunDeadlineMs, opts.signal);
358
+ const inspectedDeck = await inspectDeckInWorker(opts.path, preInspectRunDeadlineMs);
359
+ const deckDir = path.dirname(inspectedDeck.deckPath);
360
+ const permissions = resolveEffectivePermissions({
361
+ baseDir: deckDir,
362
+ parent: opts.parentPermissions,
363
+ workspace: opts.workspacePermissions
364
+ ? {
365
+ baseDir: opts.workspacePermissionsBaseDir ?? deckDir,
366
+ permissions: opts.workspacePermissions,
367
+ }
368
+ : undefined,
369
+ declaration: inspectedDeck.permissions
370
+ ? { baseDir: deckDir, permissions: inspectedDeck.permissions }
371
+ : undefined,
372
+ reference: opts.referencePermissions
373
+ ? {
374
+ baseDir: opts.referencePermissionsBaseDir ?? deckDir,
375
+ permissions: opts.referencePermissions,
376
+ }
377
+ : undefined,
378
+ session: opts.sessionPermissions
379
+ ? {
380
+ baseDir: opts.sessionPermissionsBaseDir ?? dntShim.Deno.cwd(),
381
+ permissions: opts.sessionPermissions,
382
+ }
383
+ : undefined,
384
+ });
385
+ const effectiveGuardrails = {
386
+ ...guardrails,
387
+ ...(inspectedDeck.guardrails ?? {}),
388
+ };
389
+ const runDeadlineMs = deadlineForRun(effectiveGuardrails, opts.runDeadlineMs);
390
+ ensureRunActive(runDeadlineMs, opts.signal);
391
+ const resolvedInput = resolveInputWithoutDeck({
392
+ input: opts.input,
393
+ state: opts.state,
394
+ isRoot,
395
+ initialUserMessage: opts.initialUserMessage,
396
+ });
397
+ if (!inspectedDeck.hasModelParams) {
398
+ if (shouldEmitRun) {
399
+ opts.trace?.({
400
+ type: "run.start",
401
+ runId,
402
+ deckPath: inspectedDeck.deckPath,
403
+ input: resolvedInput,
404
+ initialUserMessage: opts
405
+ .initialUserMessage,
406
+ permissions: permissions.trace,
407
+ });
408
+ }
409
+ return await runComputeDeckInWorker({
410
+ deckPath: inspectedDeck.deckPath,
411
+ guardrails: effectiveGuardrails,
412
+ depth,
413
+ runId,
414
+ initialUserMessage: opts.initialUserMessage,
415
+ parentActionCallId: opts.parentActionCallId,
416
+ modelProvider: opts.modelProvider,
417
+ input: resolvedInput,
418
+ defaultModel: opts.defaultModel,
419
+ modelOverride: opts.modelOverride,
420
+ trace: opts.trace,
421
+ stream: opts.stream,
422
+ state: opts.state,
423
+ onStateUpdate: opts.onStateUpdate,
424
+ onStreamText: opts.onStreamText,
425
+ responsesMode: opts.responsesMode,
426
+ permissions: permissions.effective,
427
+ permissionsTrace: permissions.trace,
428
+ workspacePermissions: opts.workspacePermissions,
429
+ workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
430
+ sessionPermissions: opts.sessionPermissions,
431
+ sessionPermissionsBaseDir: opts.sessionPermissionsBaseDir,
432
+ runDeadlineMs,
433
+ isRoot,
434
+ allowRootStringInput: opts.allowRootStringInput ?? false,
435
+ signal: opts.signal,
436
+ });
437
+ }
438
+ if (!opts.inOrchestrationWorker) {
439
+ return await runLlmDeckInWorker({
440
+ deckPath: inspectedDeck.deckPath,
441
+ guardrails: effectiveGuardrails,
442
+ depth,
443
+ runId,
444
+ parentActionCallId: opts.parentActionCallId,
445
+ modelProvider: opts.modelProvider,
446
+ input: resolvedInput,
447
+ inputProvided: opts.inputProvided ?? true,
448
+ initialUserMessage: opts.initialUserMessage,
449
+ defaultModel: opts.defaultModel,
450
+ modelOverride: opts.modelOverride,
451
+ trace: opts.trace,
452
+ stream: opts.stream,
453
+ state: opts.state,
454
+ onStateUpdate: opts.onStateUpdate,
455
+ onStreamText: opts.onStreamText,
456
+ responsesMode: opts.responsesMode,
457
+ permissions: permissions.effective,
458
+ permissionsTrace: permissions.trace,
459
+ workspacePermissions: opts.workspacePermissions,
460
+ workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
461
+ sessionPermissions: opts.sessionPermissions,
462
+ sessionPermissionsBaseDir: opts.sessionPermissionsBaseDir,
463
+ runDeadlineMs,
464
+ workerSandbox,
465
+ allowRootStringInput: opts.allowRootStringInput,
466
+ isRoot,
467
+ signal: opts.signal,
468
+ });
469
+ }
470
+ }
471
+ const deck = await loadDeck(opts.path);
472
+ const permissions = resolveEffectivePermissions({
473
+ baseDir: path.dirname(deck.path),
474
+ parent: opts.parentPermissions,
475
+ workspace: opts.workspacePermissions
476
+ ? {
477
+ baseDir: opts.workspacePermissionsBaseDir ?? path.dirname(deck.path),
478
+ permissions: opts.workspacePermissions,
479
+ }
480
+ : undefined,
481
+ declaration: deck.permissions
482
+ ? { baseDir: path.dirname(deck.path), permissions: deck.permissions }
483
+ : undefined,
484
+ reference: opts.referencePermissions
485
+ ? {
486
+ baseDir: opts.referencePermissionsBaseDir ?? path.dirname(deck.path),
487
+ permissions: opts.referencePermissions,
488
+ }
489
+ : undefined,
490
+ session: opts.sessionPermissions
491
+ ? {
492
+ baseDir: opts.sessionPermissionsBaseDir ?? dntShim.Deno.cwd(),
493
+ permissions: opts.sessionPermissions,
494
+ }
495
+ : undefined,
496
+ });
497
+ const deckGuardrails = deck.guardrails ?? {};
498
+ const effectiveGuardrails = {
499
+ ...guardrails,
500
+ ...deckGuardrails,
501
+ };
502
+ const runDeadlineMs = deadlineForRun(effectiveGuardrails, opts.runDeadlineMs);
503
+ ensureRunActive(runDeadlineMs, opts.signal);
504
+ ensureSchemaPresence(deck, isRoot);
505
+ const resolvedInput = resolveInput({
506
+ deck,
507
+ input: opts.input,
508
+ state: opts.state,
509
+ isRoot,
510
+ initialUserMessage: opts.initialUserMessage,
511
+ });
512
+ const validatedInput = validateInput(deck, resolvedInput, isRoot, opts.allowRootStringInput ?? false);
513
+ const useOrchestrationWorker = workerSandbox &&
514
+ !opts.inOrchestrationWorker &&
515
+ isRoot &&
516
+ !opts.onTool &&
517
+ Boolean(deck.modelParams?.model || deck.modelParams?.temperature !== undefined);
518
+ if (useOrchestrationWorker) {
519
+ return await runLlmDeckInWorker({
520
+ deckPath: deck.path,
521
+ guardrails: effectiveGuardrails,
522
+ depth,
523
+ runId,
524
+ parentActionCallId: opts.parentActionCallId,
525
+ modelProvider: opts.modelProvider,
526
+ input: validatedInput,
527
+ inputProvided: opts.inputProvided ?? true,
528
+ initialUserMessage: opts.initialUserMessage,
529
+ defaultModel: opts.defaultModel,
530
+ modelOverride: opts.modelOverride,
531
+ trace: opts.trace,
532
+ stream: opts.stream,
533
+ state: opts.state,
534
+ onStateUpdate: opts.onStateUpdate,
535
+ onStreamText: opts.onStreamText,
536
+ responsesMode: opts.responsesMode,
537
+ permissions: permissions.effective,
538
+ permissionsTrace: permissions.trace,
539
+ workspacePermissions: opts.workspacePermissions,
540
+ workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
541
+ sessionPermissions: opts.sessionPermissions,
542
+ sessionPermissionsBaseDir: opts.sessionPermissionsBaseDir,
543
+ runDeadlineMs,
544
+ workerSandbox,
545
+ allowRootStringInput: opts.allowRootStringInput,
546
+ isRoot,
547
+ signal: opts.signal,
548
+ });
549
+ }
550
+ if (shouldEmitRun) {
551
+ opts.trace?.({
552
+ type: "run.start",
553
+ runId,
554
+ deckPath: deck.path,
555
+ input: validatedInput,
556
+ initialUserMessage: opts
557
+ .initialUserMessage,
558
+ permissions: permissions.trace,
559
+ });
560
+ }
56
561
  if (deck.modelParams?.model || deck.modelParams?.temperature !== undefined) {
57
562
  return await runLlmDeck({
58
563
  deck,
@@ -71,6 +576,17 @@ export async function runDeck(opts) {
71
576
  state: opts.state,
72
577
  onStateUpdate: opts.onStateUpdate,
73
578
  onStreamText: opts.onStreamText,
579
+ responsesMode: opts.responsesMode,
580
+ permissions: permissions.effective,
581
+ permissionsTrace: permissions.trace,
582
+ workspacePermissions: opts.workspacePermissions,
583
+ workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
584
+ sessionPermissions: opts.sessionPermissions,
585
+ sessionPermissionsBaseDir: opts.sessionPermissionsBaseDir,
586
+ runDeadlineMs,
587
+ workerSandbox,
588
+ onTool: opts.onTool,
589
+ signal: opts.signal,
74
590
  });
75
591
  }
76
592
  if (!deck.executor) {
@@ -81,6 +597,7 @@ export async function runDeck(opts) {
81
597
  guardrails: effectiveGuardrails,
82
598
  depth,
83
599
  runId,
600
+ initialUserMessage: opts.initialUserMessage,
84
601
  parentActionCallId: opts.parentActionCallId,
85
602
  modelProvider: opts.modelProvider,
86
603
  input: validatedInput,
@@ -88,19 +605,42 @@ export async function runDeck(opts) {
88
605
  modelOverride: opts.modelOverride,
89
606
  trace: opts.trace,
90
607
  stream: opts.stream,
608
+ state: opts.state,
609
+ onStateUpdate: opts.onStateUpdate,
91
610
  onStreamText: opts.onStreamText,
611
+ responsesMode: opts.responsesMode,
612
+ permissions: permissions.effective,
613
+ permissionsTrace: permissions.trace,
614
+ workspacePermissions: opts.workspacePermissions,
615
+ workspacePermissionsBaseDir: opts.workspacePermissionsBaseDir,
616
+ sessionPermissions: opts.sessionPermissions,
617
+ sessionPermissionsBaseDir: opts.sessionPermissionsBaseDir,
618
+ runDeadlineMs,
619
+ workerSandbox,
620
+ onTool: opts.onTool,
621
+ signal: opts.signal,
92
622
  });
93
623
  }
624
+ catch (err) {
625
+ if (isRunCanceledError(err)) {
626
+ canceled = true;
627
+ await handleCancel();
628
+ }
629
+ throw err;
630
+ }
94
631
  finally {
95
632
  if (shouldEmitRun) {
96
633
  opts.trace?.({ type: "run.end", runId });
97
634
  }
635
+ if (opts.signal?.aborted && !canceled) {
636
+ await handleCancel();
637
+ }
98
638
  }
99
639
  }
100
640
  function toProviderParams(params) {
101
641
  if (!params)
102
642
  return undefined;
103
- const { model: _model, temperature, top_p, frequency_penalty, presence_penalty, max_tokens, } = params;
643
+ const { model: _model, temperature, top_p, frequency_penalty, presence_penalty, max_tokens, verbosity, reasoning, } = params;
104
644
  const out = {};
105
645
  if (temperature !== undefined)
106
646
  out.temperature = temperature;
@@ -113,15 +653,51 @@ function toProviderParams(params) {
113
653
  out.presence_penalty = presence_penalty;
114
654
  if (max_tokens !== undefined)
115
655
  out.max_tokens = max_tokens;
656
+ if (verbosity !== undefined)
657
+ out.verbosity = verbosity;
658
+ if (reasoning !== undefined)
659
+ out.reasoning = reasoning;
116
660
  return Object.keys(out).length ? out : undefined;
117
661
  }
662
+ async function resolveModelChoice(args) {
663
+ const resolver = args.modelProvider.resolveModel;
664
+ if (resolver) {
665
+ return await resolver({
666
+ model: args.model,
667
+ params: args.params,
668
+ deckPath: args.deckPath,
669
+ });
670
+ }
671
+ if (Array.isArray(args.model)) {
672
+ const first = args.model.find((entry) => typeof entry === "string" && entry.trim().length > 0);
673
+ if (!first) {
674
+ throw new Error(`No model configured for deck ${args.deckPath}`);
675
+ }
676
+ return { model: first, params: args.params };
677
+ }
678
+ if (!args.model || !args.model.trim()) {
679
+ throw new Error(`No model configured for deck ${args.deckPath}`);
680
+ }
681
+ return { model: args.model, params: args.params };
682
+ }
683
+ function resolveContextSchema(deck) {
684
+ return deck.contextSchema ?? deck.inputSchema;
685
+ }
686
+ function resolveResponseSchema(deck) {
687
+ return deck.responseSchema ?? deck.outputSchema;
688
+ }
689
+ function isContextToolName(name) {
690
+ return name === GAMBIT_TOOL_CONTEXT || name === GAMBIT_TOOL_INIT;
691
+ }
118
692
  function ensureSchemaPresence(deck, isRoot) {
119
693
  if (!isRoot) {
120
- if (!deck.inputSchema || !deck.outputSchema) {
121
- throw new Error(`Deck ${deck.path} must declare inputSchema and outputSchema (non-root)`);
694
+ const contextSchema = resolveContextSchema(deck);
695
+ const responseSchema = resolveResponseSchema(deck);
696
+ if (!contextSchema || !responseSchema) {
697
+ throw new Error(`Deck ${deck.path} must declare contextSchema and responseSchema (non-root)`);
122
698
  }
123
- assertZodSchema(deck.inputSchema, "inputSchema");
124
- assertZodSchema(deck.outputSchema, "outputSchema");
699
+ assertZodSchema(contextSchema, "contextSchema");
700
+ assertZodSchema(responseSchema, "responseSchema");
125
701
  }
126
702
  }
127
703
  function resolveInput(args) {
@@ -129,11 +705,11 @@ function resolveInput(args) {
129
705
  return args.input;
130
706
  if (!args.isRoot)
131
707
  return args.input;
132
- const persisted = extractInitInput(args.state);
708
+ const persisted = extractContextInput(args.state);
133
709
  if (persisted !== undefined)
134
710
  return persisted;
135
711
  if (args.initialUserMessage !== undefined) {
136
- const schema = args.deck.inputSchema;
712
+ const schema = resolveContextSchema(args.deck);
137
713
  if (schema?.safeParse) {
138
714
  const candidates = [undefined, {}, ""];
139
715
  for (const candidate of candidates) {
@@ -151,12 +727,30 @@ function resolveInput(args) {
151
727
  }
152
728
  return args.input;
153
729
  }
154
- function extractInitInput(state) {
155
- if (!state?.messages)
730
+ function resolveInputWithoutDeck(args) {
731
+ if (args.input !== undefined)
732
+ return args.input;
733
+ if (!args.isRoot)
734
+ return args.input;
735
+ const persisted = extractContextInput(args.state);
736
+ if (persisted !== undefined)
737
+ return persisted;
738
+ if (args.initialUserMessage !== undefined) {
739
+ return "";
740
+ }
741
+ return args.input;
742
+ }
743
+ function extractContextInput(state) {
744
+ if (!state)
745
+ return undefined;
746
+ if (state.format === "responses" && Array.isArray(state.items)) {
747
+ return extractContextInputFromItems(state.items);
748
+ }
749
+ if (!state.messages)
156
750
  return undefined;
157
751
  for (let i = state.messages.length - 1; i >= 0; i--) {
158
752
  const msg = state.messages[i];
159
- if (msg.role === "tool" && msg.name === GAMBIT_TOOL_INIT) {
753
+ if (msg.role === "tool" && isContextToolName(msg.name ?? "")) {
160
754
  const content = msg.content;
161
755
  if (typeof content !== "string")
162
756
  return undefined;
@@ -170,17 +764,244 @@ function extractInitInput(state) {
170
764
  }
171
765
  return undefined;
172
766
  }
767
+ function extractContextInputFromItems(items) {
768
+ const contextToolNames = new Set([GAMBIT_TOOL_CONTEXT, GAMBIT_TOOL_INIT]);
769
+ const callNameById = new Map();
770
+ for (const item of items) {
771
+ if (item.type === "function_call") {
772
+ callNameById.set(item.call_id, item.name);
773
+ }
774
+ }
775
+ for (let i = items.length - 1; i >= 0; i--) {
776
+ const item = items[i];
777
+ if (item.type !== "function_call_output")
778
+ continue;
779
+ const name = callNameById.get(item.call_id);
780
+ if (!name || !contextToolNames.has(name))
781
+ continue;
782
+ try {
783
+ return JSON.parse(item.output);
784
+ }
785
+ catch {
786
+ return item.output;
787
+ }
788
+ }
789
+ return undefined;
790
+ }
791
+ function messagesFromResponseItems(items) {
792
+ const messages = [];
793
+ const callNameById = new Map();
794
+ for (const item of items) {
795
+ if (item.type === "message") {
796
+ const text = item.content.map((part) => part.text).join("");
797
+ messages.push({
798
+ role: item.role,
799
+ content: text || null,
800
+ });
801
+ continue;
802
+ }
803
+ if (item.type === "function_call") {
804
+ callNameById.set(item.call_id, item.name);
805
+ messages.push({
806
+ role: "assistant",
807
+ content: null,
808
+ tool_calls: [{
809
+ id: item.call_id,
810
+ type: "function",
811
+ function: { name: item.name, arguments: item.arguments },
812
+ }],
813
+ });
814
+ continue;
815
+ }
816
+ if (item.type === "function_call_output") {
817
+ messages.push({
818
+ role: "tool",
819
+ name: callNameById.get(item.call_id),
820
+ tool_call_id: item.call_id,
821
+ content: item.output,
822
+ });
823
+ }
824
+ }
825
+ return messages;
826
+ }
827
+ function responseItemsFromMessages(messages) {
828
+ const items = [];
829
+ for (const message of messages) {
830
+ if (message.role === "tool") {
831
+ if (!message.tool_call_id || message.content === null)
832
+ continue;
833
+ items.push({
834
+ type: "function_call_output",
835
+ call_id: message.tool_call_id,
836
+ output: String(message.content),
837
+ });
838
+ continue;
839
+ }
840
+ const contentText = message.content ?? "";
841
+ if (typeof contentText === "string" && contentText.length > 0) {
842
+ items.push({
843
+ type: "message",
844
+ role: message.role,
845
+ content: [{
846
+ type: message.role === "assistant" ? "output_text" : "input_text",
847
+ text: contentText,
848
+ }],
849
+ });
850
+ }
851
+ if (message.role === "assistant" && message.tool_calls) {
852
+ for (const call of message.tool_calls) {
853
+ items.push({
854
+ type: "function_call",
855
+ call_id: call.id,
856
+ name: call.function.name,
857
+ arguments: call.function.arguments,
858
+ });
859
+ }
860
+ }
861
+ }
862
+ return items;
863
+ }
864
+ function safeJsonArgs(value) {
865
+ if (!value)
866
+ return {};
867
+ try {
868
+ const parsed = JSON.parse(value);
869
+ if (parsed && typeof parsed === "object") {
870
+ return parsed;
871
+ }
872
+ }
873
+ catch {
874
+ // ignore
875
+ }
876
+ return {};
877
+ }
878
+ function asToolKind(value, fallback) {
879
+ if (value === "action" || value === "external" || value === "mcp_bridge" ||
880
+ value === "internal") {
881
+ return value;
882
+ }
883
+ return fallback;
884
+ }
885
+ function projectStreamToolTraceEvents(input) {
886
+ if (!input.trace)
887
+ return;
888
+ const type = typeof input.streamEvent.type === "string"
889
+ ? input.streamEvent.type
890
+ : "";
891
+ if (type !== "tool.call" && type !== "tool.result")
892
+ return;
893
+ const actionCallId = typeof input.streamEvent.actionCallId === "string"
894
+ ? input.streamEvent.actionCallId
895
+ : "";
896
+ const name = typeof input.streamEvent.name === "string"
897
+ ? input.streamEvent.name
898
+ : input.toolNames.get(actionCallId) ?? "";
899
+ if (!actionCallId || !name)
900
+ return;
901
+ if (type === "tool.call") {
902
+ if (input.emittedCalls.has(actionCallId))
903
+ return;
904
+ input.emittedCalls.add(actionCallId);
905
+ input.toolNames.set(actionCallId, name);
906
+ const args = "args" in input.streamEvent
907
+ ? (input.streamEvent.args ?? {})
908
+ : {};
909
+ const toolKind = asToolKind(input.streamEvent.toolKind, "mcp_bridge");
910
+ input.trace({
911
+ type: "tool.call",
912
+ runId: input.runId,
913
+ actionCallId,
914
+ name,
915
+ args,
916
+ toolKind,
917
+ parentActionCallId: input.parentActionCallId,
918
+ });
919
+ return;
920
+ }
921
+ if (input.emittedResults.has(actionCallId))
922
+ return;
923
+ input.emittedResults.add(actionCallId);
924
+ const result = "result" in input.streamEvent
925
+ ? (input.streamEvent.result ?? null)
926
+ : null;
927
+ const toolKind = asToolKind(input.streamEvent.toolKind, "mcp_bridge");
928
+ input.trace({
929
+ type: "tool.result",
930
+ runId: input.runId,
931
+ actionCallId,
932
+ name,
933
+ result,
934
+ toolKind,
935
+ parentActionCallId: input.parentActionCallId,
936
+ });
937
+ }
938
+ function traceOpenResponsesStreamEvent(input) {
939
+ if (!input.trace)
940
+ return false;
941
+ const type = typeof input.streamEvent.type === "string"
942
+ ? input.streamEvent.type
943
+ : "";
944
+ if (!type.startsWith("response."))
945
+ return false;
946
+ const rawMeta = input.streamEvent._gambit;
947
+ const existingMeta = rawMeta && typeof rawMeta === "object" &&
948
+ !Array.isArray(rawMeta)
949
+ ? rawMeta
950
+ : {};
951
+ input.trace({
952
+ ...input.streamEvent,
953
+ type,
954
+ _gambit: {
955
+ ...existingMeta,
956
+ run_id: input.runId,
957
+ action_call_id: input.actionCallId,
958
+ parent_action_call_id: input.parentActionCallId,
959
+ deck_path: input.deckPath,
960
+ model: input.model,
961
+ },
962
+ });
963
+ return true;
964
+ }
965
+ function mapResponseOutput(output) {
966
+ const toolCalls = [];
967
+ const textParts = [];
968
+ for (const item of output) {
969
+ if (item.type === "function_call") {
970
+ toolCalls.push({
971
+ id: item.call_id,
972
+ name: item.name,
973
+ args: safeJsonArgs(item.arguments),
974
+ });
975
+ continue;
976
+ }
977
+ if (item.type === "message" && item.role === "assistant") {
978
+ for (const part of item.content) {
979
+ if (part.type === "output_text") {
980
+ textParts.push(part.text);
981
+ }
982
+ }
983
+ }
984
+ }
985
+ return {
986
+ message: {
987
+ role: "assistant",
988
+ content: textParts.length ? textParts.join("") : null,
989
+ },
990
+ toolCalls: toolCalls.length ? toolCalls : undefined,
991
+ };
992
+ }
173
993
  function validateInput(deck, input, isRoot, allowRootStringInput) {
174
- if (deck.inputSchema) {
994
+ const contextSchema = resolveContextSchema(deck);
995
+ if (contextSchema) {
175
996
  if (isRoot && typeof input === "string" && allowRootStringInput) {
176
997
  try {
177
- return validateWithSchema(deck.inputSchema, input);
998
+ return validateWithSchema(contextSchema, input);
178
999
  }
179
1000
  catch {
180
1001
  return input;
181
1002
  }
182
1003
  }
183
- return validateWithSchema(deck.inputSchema, input);
1004
+ return validateWithSchema(contextSchema, input);
184
1005
  }
185
1006
  if (isRoot) {
186
1007
  if (input === undefined)
@@ -189,28 +1010,1209 @@ function validateInput(deck, input, isRoot, allowRootStringInput) {
189
1010
  return input;
190
1011
  return input;
191
1012
  }
192
- throw new Error(`Deck ${deck.path} requires inputSchema (non-root)`);
1013
+ throw new Error(`Deck ${deck.path} requires contextSchema (non-root)`);
193
1014
  }
194
1015
  function validateOutput(deck, output, isRoot) {
195
- if (deck.outputSchema) {
196
- return validateWithSchema(deck.outputSchema, output);
1016
+ const responseSchema = resolveResponseSchema(deck);
1017
+ if (responseSchema) {
1018
+ return validateWithSchema(responseSchema, output);
1019
+ }
1020
+ if (isRoot) {
1021
+ if (typeof output === "string")
1022
+ return output;
1023
+ return JSON.stringify(output);
1024
+ }
1025
+ throw new Error(`Deck ${deck.path} requires responseSchema (non-root)`);
1026
+ }
1027
+ async function runComputeDeck(ctx) {
1028
+ if (ctx.workerSandbox) {
1029
+ return await runComputeDeckInWorker({
1030
+ guardrails: ctx.guardrails,
1031
+ depth: ctx.depth,
1032
+ runId: ctx.runId,
1033
+ inputProvided: ctx.inputProvided,
1034
+ initialUserMessage: ctx.initialUserMessage,
1035
+ parentActionCallId: ctx.parentActionCallId,
1036
+ modelProvider: ctx.modelProvider,
1037
+ input: ctx.input,
1038
+ defaultModel: ctx.defaultModel,
1039
+ modelOverride: ctx.modelOverride,
1040
+ trace: ctx.trace,
1041
+ stream: ctx.stream,
1042
+ state: ctx.state,
1043
+ onStateUpdate: ctx.onStateUpdate,
1044
+ onStreamText: ctx.onStreamText,
1045
+ responsesMode: ctx.responsesMode,
1046
+ permissions: ctx.permissions,
1047
+ permissionsTrace: ctx.permissionsTrace,
1048
+ workspacePermissions: ctx.workspacePermissions,
1049
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
1050
+ sessionPermissions: ctx.sessionPermissions,
1051
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
1052
+ runDeadlineMs: ctx.runDeadlineMs,
1053
+ deckPath: ctx.deck.path,
1054
+ isRoot: ctx.depth === 0 && !ctx.parentActionCallId,
1055
+ allowRootStringInput: false,
1056
+ signal: ctx.signal,
1057
+ });
1058
+ }
1059
+ return await runComputeDeckInProcess(ctx);
1060
+ }
1061
+ function toDenoPermissionList(scope) {
1062
+ if (scope.all)
1063
+ return true;
1064
+ if (scope.values.size === 0)
1065
+ return false;
1066
+ return Array.from(scope.values).sort();
1067
+ }
1068
+ function toDenoRunPermission(scope) {
1069
+ if (scope.all)
1070
+ return true;
1071
+ const values = new Set([
1072
+ ...Array.from(scope.paths),
1073
+ ...Array.from(scope.commands),
1074
+ ]);
1075
+ if (values.size === 0)
1076
+ return false;
1077
+ return Array.from(values).sort();
1078
+ }
1079
+ const IMPORT_SOURCE_EXTENSIONS = new Set([
1080
+ ".ts",
1081
+ ".tsx",
1082
+ ".mts",
1083
+ ".cts",
1084
+ ".js",
1085
+ ".jsx",
1086
+ ".mjs",
1087
+ ".cjs",
1088
+ ]);
1089
+ const RESOLVABLE_MODULE_EXTENSIONS = [
1090
+ ".ts",
1091
+ ".tsx",
1092
+ ".mts",
1093
+ ".cts",
1094
+ ".js",
1095
+ ".jsx",
1096
+ ".mjs",
1097
+ ".cjs",
1098
+ ".json",
1099
+ ];
1100
+ function stripSpecifierSuffix(specifier) {
1101
+ let out = specifier;
1102
+ const q = out.indexOf("?");
1103
+ if (q >= 0)
1104
+ out = out.slice(0, q);
1105
+ const h = out.indexOf("#");
1106
+ if (h >= 0)
1107
+ out = out.slice(0, h);
1108
+ return out.trim();
1109
+ }
1110
+ function isIdentifierStart(ch) {
1111
+ return /[A-Za-z_$]/.test(ch);
1112
+ }
1113
+ function isIdentifierContinue(ch) {
1114
+ return /[A-Za-z0-9_$]/.test(ch);
1115
+ }
1116
+ function skipWhitespaceAndComments(source, start) {
1117
+ let i = start;
1118
+ while (i < source.length) {
1119
+ const ch = source[i];
1120
+ if (/\s/.test(ch)) {
1121
+ i++;
1122
+ continue;
1123
+ }
1124
+ if (ch === "/" && source[i + 1] === "/") {
1125
+ i += 2;
1126
+ while (i < source.length && source[i] !== "\n" && source[i] !== "\r") {
1127
+ i++;
1128
+ }
1129
+ continue;
1130
+ }
1131
+ if (ch === "/" && source[i + 1] === "*") {
1132
+ i += 2;
1133
+ while (i < source.length) {
1134
+ if (source[i] === "*" && source[i + 1] === "/") {
1135
+ i += 2;
1136
+ break;
1137
+ }
1138
+ i++;
1139
+ }
1140
+ continue;
1141
+ }
1142
+ break;
1143
+ }
1144
+ return i;
1145
+ }
1146
+ function readIdentifier(source, start) {
1147
+ if (start >= source.length)
1148
+ return undefined;
1149
+ if (!isIdentifierStart(source[start]))
1150
+ return undefined;
1151
+ let i = start + 1;
1152
+ while (i < source.length && isIdentifierContinue(source[i]))
1153
+ i++;
1154
+ return { value: source.slice(start, i), end: i };
1155
+ }
1156
+ function readStringLiteral(source, start) {
1157
+ const quote = source[start];
1158
+ if (quote !== "'" && quote !== '"')
1159
+ return undefined;
1160
+ let i = start + 1;
1161
+ let value = "";
1162
+ while (i < source.length) {
1163
+ const ch = source[i];
1164
+ if (ch === "\\") {
1165
+ if (i + 1 >= source.length)
1166
+ return undefined;
1167
+ value += source[i + 1];
1168
+ i += 2;
1169
+ continue;
1170
+ }
1171
+ if (ch === quote)
1172
+ return { value, end: i + 1 };
1173
+ if (ch === "\n" || ch === "\r")
1174
+ return undefined;
1175
+ value += ch;
1176
+ i++;
1177
+ }
1178
+ return undefined;
1179
+ }
1180
+ function skipTemplateExpression(source, start) {
1181
+ let i = start;
1182
+ let depth = 1;
1183
+ while (i < source.length && depth > 0) {
1184
+ i = skipWhitespaceAndComments(source, i);
1185
+ if (i >= source.length)
1186
+ break;
1187
+ const ch = source[i];
1188
+ if (ch === "'" || ch === '"') {
1189
+ const stringLiteral = readStringLiteral(source, i);
1190
+ i = stringLiteral ? stringLiteral.end : i + 1;
1191
+ continue;
1192
+ }
1193
+ if (ch === "`") {
1194
+ i = skipTemplateLiteral(source, i);
1195
+ continue;
1196
+ }
1197
+ if (ch === "{") {
1198
+ depth++;
1199
+ i++;
1200
+ continue;
1201
+ }
1202
+ if (ch === "}") {
1203
+ depth--;
1204
+ i++;
1205
+ continue;
1206
+ }
1207
+ i++;
1208
+ }
1209
+ return i;
1210
+ }
1211
+ function skipTemplateLiteral(source, start) {
1212
+ let i = start + 1;
1213
+ while (i < source.length) {
1214
+ const ch = source[i];
1215
+ if (ch === "\\") {
1216
+ i += 2;
1217
+ continue;
1218
+ }
1219
+ if (ch === "`")
1220
+ return i + 1;
1221
+ if (ch === "$" && source[i + 1] === "{") {
1222
+ i = skipTemplateExpression(source, i + 2);
1223
+ continue;
1224
+ }
1225
+ i++;
1226
+ }
1227
+ return i;
1228
+ }
1229
+ function readSpecifierAfterFrom(source, start) {
1230
+ const i = skipWhitespaceAndComments(source, start);
1231
+ const stringLiteral = readStringLiteral(source, i);
1232
+ if (!stringLiteral)
1233
+ return { end: i };
1234
+ return { specifier: stringLiteral.value, end: stringLiteral.end };
1235
+ }
1236
+ function readImportCallSpecifier(source, start) {
1237
+ let i = skipWhitespaceAndComments(source, start);
1238
+ if (source[i] !== "(")
1239
+ return { end: i };
1240
+ i = skipWhitespaceAndComments(source, i + 1);
1241
+ const stringLiteral = readStringLiteral(source, i);
1242
+ if (!stringLiteral)
1243
+ return { end: i };
1244
+ i = skipWhitespaceAndComments(source, stringLiteral.end);
1245
+ if (source[i] === ")")
1246
+ i++;
1247
+ return { specifier: stringLiteral.value, end: i };
1248
+ }
1249
+ function readImportOrExportStatementSpecifier(source, start, keyword) {
1250
+ let i = skipWhitespaceAndComments(source, start);
1251
+ if (keyword === "import") {
1252
+ if (source[i] === ".")
1253
+ return { end: i + 1 }; // import.meta
1254
+ const sideEffectImport = readStringLiteral(source, i);
1255
+ if (sideEffectImport) {
1256
+ return { specifier: sideEffectImport.value, end: sideEffectImport.end };
1257
+ }
1258
+ }
1259
+ let depth = 0;
1260
+ while (i < source.length) {
1261
+ i = skipWhitespaceAndComments(source, i);
1262
+ if (i >= source.length)
1263
+ break;
1264
+ const ch = source[i];
1265
+ if (ch === "'" || ch === '"') {
1266
+ const stringLiteral = readStringLiteral(source, i);
1267
+ i = stringLiteral ? stringLiteral.end : i + 1;
1268
+ continue;
1269
+ }
1270
+ if (ch === "`") {
1271
+ i = skipTemplateLiteral(source, i);
1272
+ continue;
1273
+ }
1274
+ if (ch === "(" || ch === "[" || ch === "{") {
1275
+ depth++;
1276
+ i++;
1277
+ continue;
1278
+ }
1279
+ if (ch === ")" || ch === "]" || ch === "}") {
1280
+ if (depth > 0)
1281
+ depth--;
1282
+ i++;
1283
+ continue;
1284
+ }
1285
+ if (depth === 0) {
1286
+ if (ch === ";")
1287
+ return { end: i + 1 };
1288
+ const identifier = readIdentifier(source, i);
1289
+ if (identifier?.value === "from") {
1290
+ return readSpecifierAfterFrom(source, identifier.end);
1291
+ }
1292
+ if (identifier) {
1293
+ i = identifier.end;
1294
+ continue;
1295
+ }
1296
+ }
1297
+ i++;
1298
+ }
1299
+ return { end: i };
1300
+ }
1301
+ function extractModuleSpecifiers(source) {
1302
+ const out = new Set();
1303
+ let i = 0;
1304
+ while (i < source.length) {
1305
+ i = skipWhitespaceAndComments(source, i);
1306
+ if (i >= source.length)
1307
+ break;
1308
+ const ch = source[i];
1309
+ if (ch === "'" || ch === '"') {
1310
+ const stringLiteral = readStringLiteral(source, i);
1311
+ i = stringLiteral ? stringLiteral.end : i + 1;
1312
+ continue;
1313
+ }
1314
+ if (ch === "`") {
1315
+ i = skipTemplateLiteral(source, i);
1316
+ continue;
1317
+ }
1318
+ const identifier = readIdentifier(source, i);
1319
+ if (!identifier) {
1320
+ i++;
1321
+ continue;
1322
+ }
1323
+ if (identifier.value === "import") {
1324
+ const afterImport = skipWhitespaceAndComments(source, identifier.end);
1325
+ if (source[afterImport] === "(") {
1326
+ const result = readImportCallSpecifier(source, afterImport);
1327
+ if (result.specifier)
1328
+ out.add(result.specifier);
1329
+ i = Math.max(result.end, afterImport + 1);
1330
+ continue;
1331
+ }
1332
+ const result = readImportOrExportStatementSpecifier(source, identifier.end, "import");
1333
+ if (result.specifier)
1334
+ out.add(result.specifier);
1335
+ i = Math.max(result.end, identifier.end);
1336
+ continue;
1337
+ }
1338
+ if (identifier.value === "export") {
1339
+ const result = readImportOrExportStatementSpecifier(source, identifier.end, "export");
1340
+ if (result.specifier)
1341
+ out.add(result.specifier);
1342
+ i = Math.max(result.end, identifier.end);
1343
+ continue;
1344
+ }
1345
+ i = identifier.end;
1346
+ }
1347
+ return out;
1348
+ }
1349
+ function resolveExistingModulePath(candidate) {
1350
+ const resolved = path.resolve(candidate);
1351
+ const candidates = new Set([resolved]);
1352
+ if (!path.extname(resolved)) {
1353
+ for (const ext of RESOLVABLE_MODULE_EXTENSIONS) {
1354
+ candidates.add(`${resolved}${ext}`);
1355
+ candidates.add(path.join(resolved, `index${ext}`));
1356
+ }
1357
+ }
1358
+ for (const filePath of candidates) {
1359
+ try {
1360
+ if (dntShim.Deno.statSync(filePath).isFile) {
1361
+ return path.resolve(filePath);
1362
+ }
1363
+ }
1364
+ catch {
1365
+ // ignore unresolved module candidates
1366
+ }
1367
+ }
1368
+ return undefined;
1369
+ }
1370
+ function resolveLocalImportPath(importerPath, specifier) {
1371
+ const cleaned = stripSpecifierSuffix(specifier);
1372
+ if (!cleaned)
1373
+ return undefined;
1374
+ if (cleaned.startsWith("file://")) {
1375
+ try {
1376
+ return resolveExistingModulePath(path.fromFileUrl(cleaned));
1377
+ }
1378
+ catch {
1379
+ return undefined;
1380
+ }
1381
+ }
1382
+ if (!(cleaned.startsWith("./") || cleaned.startsWith("../") ||
1383
+ path.isAbsolute(cleaned))) {
1384
+ return undefined;
1385
+ }
1386
+ const base = path.isAbsolute(cleaned)
1387
+ ? cleaned
1388
+ : path.resolve(path.dirname(importerPath), cleaned);
1389
+ return resolveExistingModulePath(base);
1390
+ }
1391
+ function collectLocalImportGraph(entryPath) {
1392
+ const visited = new Set();
1393
+ const queue = [path.resolve(entryPath)];
1394
+ while (queue.length > 0) {
1395
+ const current = queue.pop();
1396
+ if (visited.has(current))
1397
+ continue;
1398
+ visited.add(current);
1399
+ const ext = path.extname(current).toLowerCase();
1400
+ if (!IMPORT_SOURCE_EXTENSIONS.has(ext)) {
1401
+ continue;
1402
+ }
1403
+ let source;
1404
+ try {
1405
+ source = dntShim.Deno.readTextFileSync(current);
1406
+ }
1407
+ catch {
1408
+ continue;
1409
+ }
1410
+ const specifiers = extractModuleSpecifiers(source);
1411
+ for (const specifier of specifiers) {
1412
+ const resolved = resolveLocalImportPath(current, specifier);
1413
+ if (resolved && !visited.has(resolved)) {
1414
+ queue.push(resolved);
1415
+ }
1416
+ }
1417
+ }
1418
+ return visited;
1419
+ }
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");
1426
+ let builtinSchemaBootstrapCache;
1427
+ function builtinSchemaBootstrapReads() {
1428
+ if (builtinSchemaBootstrapCache)
1429
+ return builtinSchemaBootstrapCache;
1430
+ const schemaModules = [];
1431
+ const stack = [BUILTIN_SCHEMAS_DIR];
1432
+ while (stack.length > 0) {
1433
+ const current = stack.pop();
1434
+ let entries = [];
1435
+ try {
1436
+ entries = Array.from(dntShim.Deno.readDirSync(current));
1437
+ }
1438
+ catch {
1439
+ continue;
1440
+ }
1441
+ for (const entry of entries) {
1442
+ const target = path.join(current, entry.name);
1443
+ if (entry.isDirectory) {
1444
+ stack.push(target);
1445
+ continue;
1446
+ }
1447
+ if (!entry.isFile)
1448
+ continue;
1449
+ const ext = path.extname(entry.name).toLowerCase();
1450
+ if (ext !== ".ts")
1451
+ continue;
1452
+ schemaModules.push(target);
1453
+ }
1454
+ }
1455
+ builtinSchemaBootstrapCache = Array.from(new Set(schemaModules.flatMap((entry) => Array.from(collectLocalImportGraph(entry))))).sort();
1456
+ return builtinSchemaBootstrapCache;
1457
+ }
1458
+ let builtinSnippetBootstrapCache;
1459
+ function builtinSnippetBootstrapReads() {
1460
+ if (builtinSnippetBootstrapCache)
1461
+ return builtinSnippetBootstrapCache;
1462
+ const snippetFiles = [];
1463
+ const stack = [BUILTIN_SNIPPETS_DIR];
1464
+ while (stack.length > 0) {
1465
+ const current = stack.pop();
1466
+ let entries = [];
1467
+ try {
1468
+ entries = Array.from(dntShim.Deno.readDirSync(current));
1469
+ }
1470
+ catch {
1471
+ continue;
1472
+ }
1473
+ for (const entry of entries) {
1474
+ const target = path.join(current, entry.name);
1475
+ if (entry.isDirectory) {
1476
+ stack.push(target);
1477
+ continue;
1478
+ }
1479
+ if (!entry.isFile)
1480
+ continue;
1481
+ const ext = path.extname(entry.name).toLowerCase();
1482
+ if (ext !== ".md")
1483
+ continue;
1484
+ snippetFiles.push(target);
1485
+ }
1486
+ }
1487
+ builtinSnippetBootstrapCache = Array.from(new Set(snippetFiles)).sort();
1488
+ return builtinSnippetBootstrapCache;
1489
+ }
1490
+ function workerBootstrapReadAllowlist(deckPath) {
1491
+ return Array.from(new Set([
1492
+ ...Array.from(collectLocalImportGraph(deckPath)),
1493
+ ...WORKER_ENTRY_PATHS.flatMap((entry) => Array.from(collectLocalImportGraph(entry))),
1494
+ ...builtinSchemaBootstrapReads(),
1495
+ ...builtinSnippetBootstrapReads(),
1496
+ ])).sort();
1497
+ }
1498
+ let trustedWorkerBootstrapCache;
1499
+ function trustedWorkerBootstrapReads() {
1500
+ if (trustedWorkerBootstrapCache)
1501
+ return trustedWorkerBootstrapCache;
1502
+ const definitionsPath = path.fromFileUrl(new URL("./definitions.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
1503
+ const modPath = path.fromFileUrl(new URL("../mod.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url));
1504
+ trustedWorkerBootstrapCache = Array.from(new Set([
1505
+ ...WORKER_ENTRY_PATHS.flatMap((entry) => Array.from(collectLocalImportGraph(entry))),
1506
+ ...Array.from(collectLocalImportGraph(definitionsPath)),
1507
+ ...Array.from(collectLocalImportGraph(modPath)),
1508
+ ...builtinSchemaBootstrapReads(),
1509
+ ...builtinSnippetBootstrapReads(),
1510
+ ])).sort();
1511
+ return trustedWorkerBootstrapCache;
1512
+ }
1513
+ function pathMatchesPermissionRoot(root, target) {
1514
+ if (root === target)
1515
+ return true;
1516
+ const rel = path.relative(root, target);
1517
+ return rel.length > 0 && !rel.startsWith("..") && !path.isAbsolute(rel);
1518
+ }
1519
+ function constrainBootstrapReads(permissions, roots, trustedReads, reads) {
1520
+ const allowedRoots = [
1521
+ ...roots.map((entry) => path.resolve(entry)),
1522
+ ...Array.from(permissions.read.values).map((entry) => path.resolve(permissions.baseDir, entry)),
1523
+ ];
1524
+ if (permissions.read.all) {
1525
+ return Array.from(new Set(reads)).sort();
1526
+ }
1527
+ if (allowedRoots.length === 0)
1528
+ return [];
1529
+ return reads.filter((entry) => {
1530
+ const target = path.resolve(permissions.baseDir, entry);
1531
+ if (trustedReads.has(target))
1532
+ return true;
1533
+ return allowedRoots.some((root) => pathMatchesPermissionRoot(root, target));
1534
+ });
1535
+ }
1536
+ function buildWorkerPermissions(permissions, deckPath) {
1537
+ const workerDirs = WORKER_ENTRY_PATHS.map((entry) => path.dirname(entry));
1538
+ const bootstrapReads = constrainBootstrapReads(permissions, [path.dirname(deckPath), ...workerDirs], new Set(trustedWorkerBootstrapReads()), workerBootstrapReadAllowlist(deckPath));
1539
+ const mergedRead = permissions.read.all ? true : Array.from(new Set([
1540
+ ...Array.from(permissions.read.values),
1541
+ ...bootstrapReads,
1542
+ ])).sort();
1543
+ return {
1544
+ permissions: {
1545
+ read: mergedRead === true
1546
+ ? true
1547
+ : mergedRead.length > 0
1548
+ ? mergedRead
1549
+ : false,
1550
+ write: toDenoPermissionList(permissions.write),
1551
+ run: toDenoRunPermission(permissions.run),
1552
+ net: toDenoPermissionList(permissions.net),
1553
+ env: toDenoPermissionList(permissions.env),
1554
+ // Worker module graphs include JSR dependencies (e.g. @std/*). Allow
1555
+ // manifest resolution without widening deck runtime file/run permissions.
1556
+ import: ["jsr.io:443"],
1557
+ },
1558
+ };
1559
+ }
1560
+ function buildDeckInspectWorkerPermissions(deckPath) {
1561
+ const deckDir = path.dirname(deckPath);
1562
+ const workerDirs = WORKER_ENTRY_PATHS.map((entry) => path.dirname(entry));
1563
+ const inspectSeedPermissions = {
1564
+ baseDir: deckDir,
1565
+ read: { all: false, values: new Set() },
1566
+ write: { all: false, values: new Set() },
1567
+ run: { all: false, paths: new Set(), commands: new Set() },
1568
+ net: { all: false, values: new Set() },
1569
+ env: { all: false, values: new Set() },
1570
+ };
1571
+ const bootstrapReads = constrainBootstrapReads(inspectSeedPermissions, [path.dirname(deckPath), ...workerDirs], new Set(trustedWorkerBootstrapReads()), workerBootstrapReadAllowlist(deckPath));
1572
+ const inspectReads = Array.from(new Set([deckDir, ...bootstrapReads])).sort();
1573
+ return {
1574
+ permissions: {
1575
+ read: inspectReads.length > 0 ? inspectReads : false,
1576
+ write: false,
1577
+ run: false,
1578
+ net: false,
1579
+ env: false,
1580
+ },
1581
+ };
1582
+ }
1583
+ async function inspectDeckInWorker(deckPath, runDeadlineMs) {
1584
+ if (typeof runDeadlineMs === "number" && Number.isFinite(runDeadlineMs)) {
1585
+ ensureNotExpired(runDeadlineMs);
1586
+ }
1587
+ const bridgeSession = randomId("bridge");
1588
+ const worker = createWorkerSandboxBridge(new URL("./runtime_worker.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).href, buildDeckInspectWorkerPermissions(deckPath));
1589
+ let settled = false;
1590
+ const clearAndTerminate = () => {
1591
+ try {
1592
+ worker.terminate();
1593
+ }
1594
+ catch {
1595
+ // ignore
1596
+ }
1597
+ };
1598
+ let timeoutId;
1599
+ const outcome = new Promise((resolve, reject) => {
1600
+ const finishResolve = (value) => {
1601
+ if (settled)
1602
+ return;
1603
+ settled = true;
1604
+ if (timeoutId !== undefined)
1605
+ clearTimeout(timeoutId);
1606
+ resolve(value);
1607
+ };
1608
+ const finishReject = (err) => {
1609
+ if (settled)
1610
+ return;
1611
+ settled = true;
1612
+ if (timeoutId !== undefined)
1613
+ clearTimeout(timeoutId);
1614
+ reject(err);
1615
+ };
1616
+ const deadlineConstrained = typeof runDeadlineMs === "number" &&
1617
+ Number.isFinite(runDeadlineMs);
1618
+ const timeoutMs = deadlineConstrained
1619
+ ? Math.max(0, Math.min(INSPECT_WORKER_TIMEOUT_MS, Math.floor(runDeadlineMs - performance.now())))
1620
+ : INSPECT_WORKER_TIMEOUT_MS;
1621
+ const timeoutMessage = deadlineConstrained &&
1622
+ timeoutMs < INSPECT_WORKER_TIMEOUT_MS
1623
+ ? WORKER_TIMEOUT_MESSAGE
1624
+ : INSPECT_WORKER_TIMEOUT_MESSAGE;
1625
+ timeoutId = setTimeout(() => {
1626
+ finishReject(new Error(timeoutMessage));
1627
+ clearAndTerminate();
1628
+ }, timeoutMs);
1629
+ worker.addEventListener("error", (event) => {
1630
+ event.preventDefault?.();
1631
+ finishReject(event.error ??
1632
+ new Error(typeof event.message === "string"
1633
+ ? event.message
1634
+ : "Worker execution failed"));
1635
+ });
1636
+ worker.addEventListener("messageerror", () => {
1637
+ finishReject(new Error("Worker bridge message serialization failed"));
1638
+ });
1639
+ worker.addEventListener("message", (event) => {
1640
+ const msg = event.data;
1641
+ const receivedSession = typeof msg.bridgeSession === "string"
1642
+ ? msg.bridgeSession
1643
+ : "";
1644
+ if (receivedSession !== bridgeSession) {
1645
+ if (typeof msg.type === "string") {
1646
+ logger.warn(`[gambit] rejected inspect-worker message with mismatched bridge session (type=${msg.type})`);
1647
+ }
1648
+ return;
1649
+ }
1650
+ const type = typeof msg.type === "string" ? msg.type : "";
1651
+ if (type === "deck.inspect.result") {
1652
+ finishResolve(msg.result);
1653
+ return;
1654
+ }
1655
+ if (type === "deck.inspect.error" || type === "run.error") {
1656
+ finishReject(normalizeWorkerError(msg.error));
1657
+ }
1658
+ });
1659
+ });
1660
+ try {
1661
+ worker.postMessage({ type: "deck.inspect", bridgeSession, deckPath });
1662
+ return await outcome;
1663
+ }
1664
+ finally {
1665
+ if (timeoutId !== undefined)
1666
+ clearTimeout(timeoutId);
1667
+ clearAndTerminate();
1668
+ }
1669
+ }
1670
+ function normalizeWorkerError(err) {
1671
+ if (!err || typeof err !== "object") {
1672
+ return new Error(String(err));
1673
+ }
1674
+ const rec = err;
1675
+ const message = typeof rec.message === "string" && rec.message.trim().length > 0
1676
+ ? rec.message
1677
+ : "Worker execution failed";
1678
+ const code = typeof rec.code === "string" ? rec.code : undefined;
1679
+ const name = typeof rec.name === "string" ? rec.name : undefined;
1680
+ const source = typeof rec.source === "string" ? rec.source : undefined;
1681
+ const out = new Error(source ? `[${source}] ${message}${code ? ` (${code})` : ""}` : message);
1682
+ if (name)
1683
+ out.name = name;
1684
+ return out;
1685
+ }
1686
+ async function runLlmDeckInWorker(ctx) {
1687
+ throwIfCanceled(ctx.signal);
1688
+ const bridgeSession = randomId("bridge");
1689
+ const completionNonce = randomId("done");
1690
+ 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));
1691
+ let settled = false;
1692
+ const clearAndTerminate = () => {
1693
+ try {
1694
+ worker.terminate();
1695
+ }
1696
+ catch {
1697
+ // ignore
1698
+ }
1699
+ };
1700
+ let timeoutId;
1701
+ const outcome = new Promise((resolve, reject) => {
1702
+ const finishResolve = (value) => {
1703
+ if (settled)
1704
+ return;
1705
+ settled = true;
1706
+ if (timeoutId !== undefined)
1707
+ clearTimeout(timeoutId);
1708
+ resolve(value);
1709
+ };
1710
+ const finishReject = (err) => {
1711
+ if (settled)
1712
+ return;
1713
+ settled = true;
1714
+ if (timeoutId !== undefined)
1715
+ clearTimeout(timeoutId);
1716
+ reject(err);
1717
+ };
1718
+ const remainingMs = Math.max(0, Math.floor(ctx.runDeadlineMs - performance.now()));
1719
+ timeoutId = setTimeout(() => {
1720
+ finishReject(new Error(WORKER_TIMEOUT_MESSAGE));
1721
+ clearAndTerminate();
1722
+ }, remainingMs);
1723
+ worker.addEventListener("error", (event) => {
1724
+ event.preventDefault?.();
1725
+ finishReject(event.error ??
1726
+ new Error(typeof event.message === "string"
1727
+ ? event.message
1728
+ : "Worker execution failed"));
1729
+ });
1730
+ worker.addEventListener("messageerror", () => {
1731
+ finishReject(new Error("Worker bridge message serialization failed"));
1732
+ });
1733
+ worker.addEventListener("message", (event) => {
1734
+ const msg = event.data;
1735
+ if (!msg || typeof msg !== "object")
1736
+ return;
1737
+ if (msg.bridgeSession !== bridgeSession) {
1738
+ logger.warn(`[gambit] rejected orchestration-worker message with mismatched bridge session (type=${msg.type})`);
1739
+ return;
1740
+ }
1741
+ if (msg.type === "trace.event") {
1742
+ ctx.trace?.(msg.event);
1743
+ return;
1744
+ }
1745
+ if (msg.type === "state.update") {
1746
+ ctx.onStateUpdate?.(msg.state);
1747
+ return;
1748
+ }
1749
+ if (msg.type === "stream.text") {
1750
+ ctx.onStreamText?.(msg.chunk);
1751
+ return;
1752
+ }
1753
+ if (msg.type === "model.chat.request") {
1754
+ (async () => {
1755
+ try {
1756
+ const result = await ctx.modelProvider.chat({
1757
+ ...msg.input,
1758
+ signal: ctx.signal,
1759
+ onStreamText: (chunk) => {
1760
+ worker.postMessage({
1761
+ type: "model.chat.stream",
1762
+ requestId: msg.requestId,
1763
+ chunk,
1764
+ });
1765
+ },
1766
+ });
1767
+ worker.postMessage({
1768
+ type: "model.chat.result",
1769
+ requestId: msg.requestId,
1770
+ result,
1771
+ });
1772
+ }
1773
+ catch (err) {
1774
+ worker.postMessage({
1775
+ type: "model.chat.error",
1776
+ requestId: msg.requestId,
1777
+ error: {
1778
+ source: "model",
1779
+ name: err instanceof Error ? err.name : undefined,
1780
+ message: err instanceof Error ? err.message : String(err),
1781
+ code: err?.code,
1782
+ },
1783
+ });
1784
+ }
1785
+ })();
1786
+ return;
1787
+ }
1788
+ if (msg.type === "model.responses.request") {
1789
+ (async () => {
1790
+ try {
1791
+ if (!ctx.modelProvider.responses) {
1792
+ throw new Error("Responses API unavailable for current model provider");
1793
+ }
1794
+ const result = await ctx.modelProvider.responses({
1795
+ ...msg.input,
1796
+ signal: ctx.signal,
1797
+ onStreamEvent: (streamEvent) => {
1798
+ worker.postMessage({
1799
+ type: "model.responses.event",
1800
+ requestId: msg.requestId,
1801
+ event: streamEvent,
1802
+ });
1803
+ },
1804
+ });
1805
+ worker.postMessage({
1806
+ type: "model.responses.result",
1807
+ requestId: msg.requestId,
1808
+ result,
1809
+ });
1810
+ }
1811
+ catch (err) {
1812
+ worker.postMessage({
1813
+ type: "model.responses.error",
1814
+ requestId: msg.requestId,
1815
+ error: {
1816
+ source: "model",
1817
+ name: err instanceof Error ? err.name : undefined,
1818
+ message: err instanceof Error ? err.message : String(err),
1819
+ code: err?.code,
1820
+ },
1821
+ });
1822
+ }
1823
+ })();
1824
+ return;
1825
+ }
1826
+ if (msg.type === "model.resolveModel.request") {
1827
+ (async () => {
1828
+ try {
1829
+ const result = ctx.modelProvider.resolveModel
1830
+ ? await ctx.modelProvider.resolveModel(msg.input)
1831
+ : {
1832
+ model: Array.isArray(msg.input.model)
1833
+ ? msg.input.model[0]
1834
+ : msg.input.model,
1835
+ params: msg.input.params,
1836
+ };
1837
+ worker.postMessage({
1838
+ type: "model.resolveModel.result",
1839
+ requestId: msg.requestId,
1840
+ result,
1841
+ });
1842
+ }
1843
+ catch (err) {
1844
+ worker.postMessage({
1845
+ type: "model.resolveModel.error",
1846
+ requestId: msg.requestId,
1847
+ error: {
1848
+ source: "model",
1849
+ name: err instanceof Error ? err.name : undefined,
1850
+ message: err instanceof Error ? err.message : String(err),
1851
+ code: err?.code,
1852
+ },
1853
+ });
1854
+ }
1855
+ })();
1856
+ return;
1857
+ }
1858
+ if (msg.type === "run.result") {
1859
+ if (msg.completionNonce !== completionNonce) {
1860
+ logger.warn(`[gambit] rejected orchestration-worker run.result with invalid completion nonce`);
1861
+ return;
1862
+ }
1863
+ finishResolve(msg.result);
1864
+ return;
1865
+ }
1866
+ if (msg.type === "run.error") {
1867
+ if (msg.completionNonce !== completionNonce) {
1868
+ logger.warn(`[gambit] rejected orchestration-worker run.error with invalid completion nonce`);
1869
+ return;
1870
+ }
1871
+ finishReject(normalizeWorkerError(msg.error));
1872
+ }
1873
+ });
1874
+ });
1875
+ try {
1876
+ worker.postMessage({
1877
+ type: "run.start",
1878
+ bridgeSession,
1879
+ completionNonce,
1880
+ options: {
1881
+ path: ctx.deckPath,
1882
+ input: ctx.input,
1883
+ inputProvided: ctx.inputProvided,
1884
+ initialUserMessage: ctx.initialUserMessage,
1885
+ isRoot: ctx.isRoot,
1886
+ guardrails: ctx.guardrails,
1887
+ depth: ctx.depth,
1888
+ parentActionCallId: ctx.parentActionCallId,
1889
+ runId: ctx.runId,
1890
+ defaultModel: ctx.defaultModel,
1891
+ modelOverride: ctx.modelOverride,
1892
+ stream: ctx.stream,
1893
+ state: ctx.state,
1894
+ responsesMode: ctx.responsesMode,
1895
+ allowRootStringInput: ctx.allowRootStringInput,
1896
+ runDeadlineMs: ctx.runDeadlineMs,
1897
+ },
1898
+ permissionCeiling: toWirePermissionSet(ctx.permissions),
1899
+ });
1900
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
1901
+ return await outcome;
1902
+ }
1903
+ finally {
1904
+ if (timeoutId !== undefined)
1905
+ clearTimeout(timeoutId);
1906
+ clearAndTerminate();
1907
+ }
1908
+ }
1909
+ async function runComputeDeckInWorker(ctx) {
1910
+ throwIfCanceled(ctx.signal);
1911
+ const { runId } = ctx;
1912
+ const actionCallId = randomId("action");
1913
+ const bridgeSession = randomId("bridge");
1914
+ const completionNonce = randomId("done");
1915
+ const worker = createWorkerSandboxBridge(new URL("./runtime_worker.ts", globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).url).href, buildWorkerPermissions(ctx.permissions, ctx.deckPath));
1916
+ let settled = false;
1917
+ const clearAndTerminate = () => {
1918
+ try {
1919
+ worker.terminate();
1920
+ }
1921
+ catch {
1922
+ // ignore
1923
+ }
1924
+ };
1925
+ let timeoutId;
1926
+ const activeSpawnRequests = new Set();
1927
+ let currentState = ctx.state;
1928
+ const outcome = new Promise((resolve, reject) => {
1929
+ const finishResolve = (value) => {
1930
+ if (settled)
1931
+ return;
1932
+ settled = true;
1933
+ if (timeoutId !== undefined)
1934
+ clearTimeout(timeoutId);
1935
+ resolve(value);
1936
+ };
1937
+ const finishReject = (err) => {
1938
+ if (settled)
1939
+ return;
1940
+ settled = true;
1941
+ if (timeoutId !== undefined)
1942
+ clearTimeout(timeoutId);
1943
+ reject(err);
1944
+ };
1945
+ const remainingMs = Math.max(0, Math.floor(ctx.runDeadlineMs - performance.now()));
1946
+ timeoutId = setTimeout(() => {
1947
+ finishReject(new Error(WORKER_TIMEOUT_MESSAGE));
1948
+ clearAndTerminate();
1949
+ }, remainingMs);
1950
+ worker.addEventListener("error", (event) => {
1951
+ event.preventDefault?.();
1952
+ finishReject(event.error ??
1953
+ new Error(typeof event.message === "string"
1954
+ ? event.message
1955
+ : "Worker execution failed"));
1956
+ });
1957
+ worker.addEventListener("messageerror", () => {
1958
+ finishReject(new Error("Worker bridge message serialization failed"));
1959
+ });
1960
+ worker.addEventListener("message", (event) => {
1961
+ const msg = event.data;
1962
+ const receivedBridgeSession = typeof msg.bridgeSession === "string"
1963
+ ? msg.bridgeSession
1964
+ : "";
1965
+ if (receivedBridgeSession !== bridgeSession) {
1966
+ const type = typeof msg.type === "string" ? msg.type : "unknown";
1967
+ logger.warn(`[gambit] rejected compute-worker message with mismatched bridge session (type=${type})`);
1968
+ return;
1969
+ }
1970
+ // Ignore any late worker messages once this run has already settled.
1971
+ if (settled)
1972
+ return;
1973
+ const type = typeof msg.type === "string" ? msg.type : "";
1974
+ if (type === "log.entry") {
1975
+ if (!ctx.trace)
1976
+ return;
1977
+ const entry = msg.entry;
1978
+ const raw = typeof entry === "string"
1979
+ ? { message: entry }
1980
+ : entry && typeof entry === "object"
1981
+ ? entry
1982
+ : { message: "" };
1983
+ const message = typeof raw.message === "string"
1984
+ ? raw.message
1985
+ : raw.message !== undefined
1986
+ ? String(raw.message)
1987
+ : "";
1988
+ const title = typeof raw.title === "string" ? raw.title : undefined;
1989
+ const body = raw.body ?? raw.message ?? message;
1990
+ ctx.trace({
1991
+ type: "log",
1992
+ runId,
1993
+ deckPath: ctx.deckPath,
1994
+ actionCallId,
1995
+ parentActionCallId: ctx.parentActionCallId,
1996
+ level: raw.level ?? "info",
1997
+ title: title ?? (message || undefined),
1998
+ message,
1999
+ body,
2000
+ meta: raw.meta,
2001
+ });
2002
+ return;
2003
+ }
2004
+ if (type === "spawn.request") {
2005
+ const req = msg;
2006
+ const requestId = req.requestId;
2007
+ if (!requestId)
2008
+ return;
2009
+ if (activeSpawnRequests.has(requestId)) {
2010
+ logger.warn(`[gambit] rejected duplicate compute-worker spawn.request (${requestId})`);
2011
+ return;
2012
+ }
2013
+ activeSpawnRequests.add(requestId);
2014
+ (async () => {
2015
+ try {
2016
+ const parentFromWorker = normalizePermissionBaseDir(fromWirePermissionSet(req.payload.parentPermissions), req.payload.parentPermissionsBaseDir);
2017
+ // Enforce monotonicity against the parent effective ceiling.
2018
+ const bridgedParent = intersectPermissions(ctx.permissions, parentFromWorker, req.payload.parentPermissionsBaseDir);
2019
+ const childResult = await runDeck({
2020
+ path: req.payload.path,
2021
+ input: req.payload.input,
2022
+ modelProvider: ctx.modelProvider,
2023
+ isRoot: false,
2024
+ guardrails: ctx.guardrails,
2025
+ depth: ctx.depth + 1,
2026
+ parentActionCallId: req.payload.parentActionCallId,
2027
+ runId,
2028
+ defaultModel: ctx.defaultModel,
2029
+ modelOverride: ctx.modelOverride,
2030
+ trace: ctx.trace,
2031
+ stream: ctx.stream,
2032
+ state: currentState,
2033
+ onStateUpdate: (state) => {
2034
+ currentState = state;
2035
+ ctx.onStateUpdate?.(state);
2036
+ },
2037
+ onStreamText: ctx.onStreamText,
2038
+ responsesMode: ctx.responsesMode,
2039
+ initialUserMessage: req.payload.initialUserMessage,
2040
+ inputProvided: true,
2041
+ parentPermissions: bridgedParent,
2042
+ workspacePermissions: req.payload.workspacePermissions,
2043
+ workspacePermissionsBaseDir: req.payload.workspacePermissionsBaseDir,
2044
+ sessionPermissions: req.payload.sessionPermissions,
2045
+ sessionPermissionsBaseDir: req.payload.sessionPermissionsBaseDir,
2046
+ runDeadlineMs: Math.min(ctx.runDeadlineMs, Number.isFinite(req.payload.runDeadlineMs)
2047
+ ? req.payload.runDeadlineMs
2048
+ : ctx.runDeadlineMs),
2049
+ workerSandbox: true,
2050
+ signal: ctx.signal,
2051
+ onTool: ctx.onTool,
2052
+ });
2053
+ worker.postMessage({
2054
+ type: "spawn.result",
2055
+ requestId,
2056
+ result: childResult,
2057
+ });
2058
+ }
2059
+ catch (err) {
2060
+ worker.postMessage({
2061
+ type: "spawn.error",
2062
+ requestId,
2063
+ error: {
2064
+ source: "child",
2065
+ name: err instanceof Error ? err.name : undefined,
2066
+ message: err instanceof Error ? err.message : String(err),
2067
+ code: err?.code,
2068
+ },
2069
+ });
2070
+ }
2071
+ finally {
2072
+ activeSpawnRequests.delete(requestId);
2073
+ }
2074
+ })();
2075
+ return;
2076
+ }
2077
+ if (type === "state.update") {
2078
+ const nextState = msg.state;
2079
+ if (!nextState || typeof nextState !== "object")
2080
+ return;
2081
+ currentState = nextState;
2082
+ ctx.onStateUpdate?.(nextState);
2083
+ return;
2084
+ }
2085
+ if (type === "run.result") {
2086
+ if (msg.completionNonce !==
2087
+ completionNonce) {
2088
+ logger.warn(`[gambit] rejected compute-worker run.result with invalid completion nonce`);
2089
+ return;
2090
+ }
2091
+ finishResolve(msg.result);
2092
+ return;
2093
+ }
2094
+ if (type === "run.error") {
2095
+ if (msg.completionNonce !==
2096
+ completionNonce) {
2097
+ logger.warn(`[gambit] rejected compute-worker run.error with invalid completion nonce`);
2098
+ return;
2099
+ }
2100
+ finishReject(normalizeWorkerError(msg.error));
2101
+ }
2102
+ });
2103
+ });
2104
+ try {
2105
+ worker.postMessage({
2106
+ type: "run.start",
2107
+ bridgeSession,
2108
+ completionNonce,
2109
+ runId,
2110
+ actionCallId,
2111
+ deckPath: ctx.deckPath,
2112
+ input: ctx.input,
2113
+ state: ctx.state,
2114
+ initialUserMessage: ctx.initialUserMessage,
2115
+ depth: ctx.depth,
2116
+ parentActionCallId: ctx.parentActionCallId,
2117
+ permissions: toWirePermissionSet(ctx.permissions),
2118
+ workspacePermissions: ctx.workspacePermissions,
2119
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
2120
+ sessionPermissions: ctx.sessionPermissions,
2121
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
2122
+ runDeadlineMs: ctx.runDeadlineMs,
2123
+ isRoot: ctx.isRoot,
2124
+ allowRootStringInput: ctx.allowRootStringInput,
2125
+ });
2126
+ const raw = await outcome;
2127
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
2128
+ return raw;
197
2129
  }
198
- if (isRoot) {
199
- if (typeof output === "string")
200
- return output;
201
- return JSON.stringify(output);
2130
+ finally {
2131
+ if (timeoutId !== undefined)
2132
+ clearTimeout(timeoutId);
2133
+ clearAndTerminate();
202
2134
  }
203
- throw new Error(`Deck ${deck.path} requires outputSchema (non-root)`);
204
2135
  }
205
- async function runComputeDeck(ctx) {
2136
+ async function runComputeDeckInProcess(ctx) {
206
2137
  const { deck, runId } = ctx;
207
2138
  const actionCallId = randomId("action");
2139
+ let computeState = ctx.state
2140
+ ? {
2141
+ ...ctx.state,
2142
+ messages: Array.isArray(ctx.state.messages)
2143
+ ? ctx.state.messages.map(sanitizeMessage)
2144
+ : [],
2145
+ meta: ctx.state.meta ? { ...ctx.state.meta } : undefined,
2146
+ messageRefs: Array.isArray(ctx.state.messageRefs)
2147
+ ? [...ctx.state.messageRefs]
2148
+ : undefined,
2149
+ }
2150
+ : undefined;
2151
+ const ensureComputeState = () => {
2152
+ if (computeState)
2153
+ return computeState;
2154
+ computeState = {
2155
+ runId,
2156
+ messages: [],
2157
+ meta: {},
2158
+ messageRefs: [],
2159
+ };
2160
+ return computeState;
2161
+ };
2162
+ const publishComputeState = () => {
2163
+ if (!computeState)
2164
+ return;
2165
+ ctx.onStateUpdate?.({
2166
+ ...computeState,
2167
+ messages: computeState.messages.map(sanitizeMessage),
2168
+ meta: computeState.meta ? { ...computeState.meta } : undefined,
2169
+ messageRefs: Array.isArray(computeState.messageRefs)
2170
+ ? [...computeState.messageRefs]
2171
+ : undefined,
2172
+ });
2173
+ };
208
2174
  const execContext = {
209
2175
  runId,
210
2176
  actionCallId,
211
2177
  parentActionCallId: ctx.parentActionCallId,
212
2178
  depth: ctx.depth,
213
2179
  input: ctx.input,
2180
+ initialUserMessage: ctx.initialUserMessage,
2181
+ getSessionMeta: (key) => {
2182
+ if (!key)
2183
+ return undefined;
2184
+ return computeState?.meta?.[key];
2185
+ },
2186
+ setSessionMeta: (key, value) => {
2187
+ if (!key)
2188
+ return;
2189
+ const state = ensureComputeState();
2190
+ const nextMeta = { ...(state.meta ?? {}) };
2191
+ if (value === undefined) {
2192
+ delete nextMeta[key];
2193
+ }
2194
+ else {
2195
+ nextMeta[key] = value;
2196
+ }
2197
+ state.meta = nextMeta;
2198
+ publishComputeState();
2199
+ },
2200
+ appendMessage: (message) => {
2201
+ const role = message.role;
2202
+ const content = String(message.content ?? "");
2203
+ if ((role !== "user" && role !== "assistant") || !content.trim()) {
2204
+ return;
2205
+ }
2206
+ const state = ensureComputeState();
2207
+ const sanitized = sanitizeMessage({ role, content: content.trim() });
2208
+ state.messages = [...(state.messages ?? []), sanitized];
2209
+ const refs = Array.isArray(state.messageRefs)
2210
+ ? [...state.messageRefs]
2211
+ : [];
2212
+ refs.push({ id: randomId("msg"), role: sanitized.role });
2213
+ state.messageRefs = refs;
2214
+ publishComputeState();
2215
+ },
214
2216
  label: deck.label,
215
2217
  log: (entry) => {
216
2218
  if (!ctx.trace)
@@ -241,9 +2243,13 @@ async function runComputeDeck(ctx) {
241
2243
  });
242
2244
  },
243
2245
  spawnAndWait: async (opts) => {
2246
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
244
2247
  const childPath = path.isAbsolute(opts.path)
245
2248
  ? opts.path
246
2249
  : path.resolve(path.dirname(deck.path), opts.path);
2250
+ const childInitialUserMessage = Object.hasOwn(opts, "initialUserMessage")
2251
+ ? opts.initialUserMessage
2252
+ : ctx.initialUserMessage;
247
2253
  return await runDeck({
248
2254
  path: childPath,
249
2255
  input: opts.input,
@@ -257,11 +2263,33 @@ async function runComputeDeck(ctx) {
257
2263
  modelOverride: ctx.modelOverride,
258
2264
  trace: ctx.trace,
259
2265
  stream: ctx.stream,
260
- state: ctx.state,
261
- onStateUpdate: ctx.onStateUpdate,
2266
+ state: computeState,
2267
+ onStateUpdate: (state) => {
2268
+ computeState = {
2269
+ ...state,
2270
+ messages: Array.isArray(state.messages)
2271
+ ? state.messages.map(sanitizeMessage)
2272
+ : [],
2273
+ meta: state.meta ? { ...state.meta } : undefined,
2274
+ messageRefs: Array.isArray(state.messageRefs)
2275
+ ? [...state.messageRefs]
2276
+ : undefined,
2277
+ };
2278
+ ctx.onStateUpdate?.(state);
2279
+ },
262
2280
  onStreamText: ctx.onStreamText,
263
- initialUserMessage: undefined,
2281
+ responsesMode: ctx.responsesMode,
2282
+ initialUserMessage: childInitialUserMessage,
264
2283
  inputProvided: true,
2284
+ parentPermissions: ctx.permissions,
2285
+ workspacePermissions: ctx.workspacePermissions,
2286
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
2287
+ sessionPermissions: ctx.sessionPermissions,
2288
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
2289
+ runDeadlineMs: ctx.runDeadlineMs,
2290
+ workerSandbox: ctx.workerSandbox,
2291
+ signal: ctx.signal,
2292
+ onTool: ctx.onTool,
265
2293
  });
266
2294
  },
267
2295
  fail: (opts) => {
@@ -269,7 +2297,9 @@ async function runComputeDeck(ctx) {
269
2297
  },
270
2298
  return: (payload) => Promise.resolve(payload),
271
2299
  };
2300
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
272
2301
  const raw = await deck.executor(execContext);
2302
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
273
2303
  return validateOutput(deck, raw, ctx.depth === 0);
274
2304
  }
275
2305
  async function runLlmDeck(ctx) {
@@ -277,13 +2307,17 @@ async function runLlmDeck(ctx) {
277
2307
  const actionCallId = randomId("action");
278
2308
  const start = performance.now();
279
2309
  const respondEnabled = Boolean(deck.respond);
2310
+ const useResponses = Boolean(ctx.responsesMode) ||
2311
+ ctx.state?.format === "responses";
280
2312
  const systemPrompt = buildSystemPrompt(deck);
281
2313
  const refToolCallId = randomId("call");
282
- const messages = ctx.state?.messages
2314
+ const messages = ctx.state?.messages?.length
283
2315
  ? ctx.state.messages.map(sanitizeMessage)
284
- : [];
2316
+ : ctx.state?.items?.length
2317
+ ? messagesFromResponseItems(ctx.state.items).map(sanitizeMessage)
2318
+ : [];
285
2319
  const resumed = messages.length > 0;
286
- const sendInit = Boolean(inputProvided) && input !== undefined && !resumed;
2320
+ const sendContext = Boolean(inputProvided) && input !== undefined && !resumed;
287
2321
  const idleController = createIdleController({
288
2322
  cfg: deck.handlers?.onIdle,
289
2323
  deck,
@@ -298,11 +2332,21 @@ async function runLlmDeck(ctx) {
298
2332
  stream: ctx.stream,
299
2333
  onStreamText: ctx.onStreamText,
300
2334
  pushMessages: (msgs) => messages.push(...msgs.map(sanitizeMessage)),
2335
+ responsesMode: ctx.responsesMode,
2336
+ permissions: ctx.permissions,
2337
+ workspacePermissions: ctx.workspacePermissions,
2338
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
2339
+ sessionPermissions: ctx.sessionPermissions,
2340
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
2341
+ runDeadlineMs: ctx.runDeadlineMs,
2342
+ workerSandbox: ctx.workerSandbox,
2343
+ signal: ctx.signal,
2344
+ onTool: ctx.onTool,
301
2345
  });
302
2346
  let streamingBuffer = "";
303
2347
  let streamingCommitted = false;
304
2348
  const wrappedOnStreamText = (chunk) => {
305
- if (!chunk)
2349
+ if (!chunk || ctx.signal?.aborted)
306
2350
  return;
307
2351
  idleController.touch();
308
2352
  streamingBuffer += chunk;
@@ -310,13 +2354,14 @@ async function runLlmDeck(ctx) {
310
2354
  };
311
2355
  if (!resumed) {
312
2356
  messages.push(sanitizeMessage({ role: "system", content: systemPrompt }));
313
- if (sendInit) {
2357
+ if (sendContext) {
314
2358
  ctx.trace?.({
315
2359
  type: "tool.call",
316
2360
  runId,
317
2361
  actionCallId: refToolCallId,
318
- name: GAMBIT_TOOL_INIT,
2362
+ name: GAMBIT_TOOL_CONTEXT,
319
2363
  args: {},
2364
+ toolKind: "internal",
320
2365
  parentActionCallId: actionCallId,
321
2366
  });
322
2367
  messages.push(sanitizeMessage({
@@ -326,13 +2371,13 @@ async function runLlmDeck(ctx) {
326
2371
  id: refToolCallId,
327
2372
  type: "function",
328
2373
  function: {
329
- name: GAMBIT_TOOL_INIT,
2374
+ name: GAMBIT_TOOL_CONTEXT,
330
2375
  arguments: "{}",
331
2376
  },
332
2377
  }],
333
2378
  }), sanitizeMessage({
334
2379
  role: "tool",
335
- name: GAMBIT_TOOL_INIT,
2380
+ name: GAMBIT_TOOL_CONTEXT,
336
2381
  tool_call_id: refToolCallId,
337
2382
  content: JSON.stringify(input),
338
2383
  }));
@@ -340,8 +2385,9 @@ async function runLlmDeck(ctx) {
340
2385
  type: "tool.result",
341
2386
  runId,
342
2387
  actionCallId: refToolCallId,
343
- name: GAMBIT_TOOL_INIT,
2388
+ name: GAMBIT_TOOL_CONTEXT,
344
2389
  result: input,
2390
+ toolKind: "internal",
345
2391
  parentActionCallId: actionCallId,
346
2392
  });
347
2393
  }
@@ -362,29 +2408,36 @@ async function runLlmDeck(ctx) {
362
2408
  });
363
2409
  }
364
2410
  idleController.touch();
365
- const tools = await buildToolDefs(deck);
2411
+ const tools = await buildToolDefs(deck, ctx.permissions);
366
2412
  ctx.trace?.({
367
2413
  type: "deck.start",
368
2414
  runId,
369
2415
  deckPath: deck.path,
370
2416
  actionCallId,
371
2417
  parentActionCallId: ctx.parentActionCallId,
2418
+ permissions: ctx.permissionsTrace,
372
2419
  });
373
2420
  let passes = 0;
374
2421
  try {
375
2422
  while (passes < guardrails.maxPasses) {
376
2423
  passes++;
377
- if (performance.now() - start > guardrails.timeoutMs) {
378
- throw new Error("Timeout exceeded");
379
- }
2424
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
380
2425
  streamingBuffer = "";
381
2426
  streamingCommitted = false;
382
- const model = ctx.modelOverride ??
2427
+ const modelCandidate = ctx.modelOverride ??
383
2428
  deck.modelParams?.model ??
384
2429
  ctx.defaultModel ??
385
2430
  (() => {
386
2431
  throw new Error(`No model configured for deck ${deck.path} and no --model provided`);
387
2432
  })();
2433
+ const resolved = await resolveModelChoice({
2434
+ model: modelCandidate,
2435
+ params: toProviderParams(deck.modelParams),
2436
+ modelProvider,
2437
+ deckPath: deck.path,
2438
+ });
2439
+ const model = resolved.model;
2440
+ const providerParams = resolved.params;
388
2441
  const stateMessages = ctx.state?.messages?.length;
389
2442
  ctx.trace?.({
390
2443
  type: "model.call",
@@ -398,21 +2451,134 @@ async function runLlmDeck(ctx) {
398
2451
  messages: messages.map(sanitizeMessage),
399
2452
  tools,
400
2453
  stateMessages,
401
- parentActionCallId: ctx.parentActionCallId,
402
- });
403
- const result = await modelProvider.chat({
404
- model,
405
- messages,
406
- tools,
407
- stream: ctx.stream,
408
- state: ctx.state,
409
- params: toProviderParams(deck.modelParams),
410
- onStreamText: (ctx.onStreamText || deck.handlers?.onIdle)
411
- ? wrappedOnStreamText
2454
+ mode: useResponses ? "responses" : "chat",
2455
+ responseItems: useResponses
2456
+ ? responseItemsFromMessages(messages)
412
2457
  : undefined,
2458
+ parentActionCallId: ctx.parentActionCallId,
413
2459
  });
2460
+ let responseOutputItems;
2461
+ const responses = modelProvider.responses;
2462
+ const projectedToolCalls = new Set();
2463
+ const projectedToolResults = new Set();
2464
+ const projectedToolNames = new Map();
2465
+ const result = (useResponses && responses)
2466
+ ? await (async () => {
2467
+ const responseItems = responseItemsFromMessages(messages);
2468
+ let sawDelta = false;
2469
+ const response = await responses({
2470
+ request: {
2471
+ model,
2472
+ input: responseItems,
2473
+ tools: tools,
2474
+ stream: ctx.stream,
2475
+ params: providerParams,
2476
+ },
2477
+ state: ctx.state,
2478
+ deckPath: deck.path,
2479
+ signal: ctx.signal,
2480
+ onStreamEvent: (ctx.trace || ctx.onStreamText || deck.handlers?.onIdle)
2481
+ ? (event) => {
2482
+ if (ctx.trace) {
2483
+ const streamEvent = event;
2484
+ const handledAsResponse = traceOpenResponsesStreamEvent({
2485
+ streamEvent,
2486
+ runId,
2487
+ actionCallId,
2488
+ deckPath: deck.path,
2489
+ model,
2490
+ parentActionCallId: ctx.parentActionCallId,
2491
+ trace: ctx.trace,
2492
+ });
2493
+ if (!handledAsResponse) {
2494
+ ctx.trace({
2495
+ type: "model.stream.event",
2496
+ runId,
2497
+ actionCallId,
2498
+ deckPath: deck.path,
2499
+ model,
2500
+ event: streamEvent,
2501
+ parentActionCallId: ctx.parentActionCallId,
2502
+ });
2503
+ }
2504
+ projectStreamToolTraceEvents({
2505
+ streamEvent,
2506
+ runId,
2507
+ parentActionCallId: actionCallId,
2508
+ trace: ctx.trace,
2509
+ emittedCalls: projectedToolCalls,
2510
+ emittedResults: projectedToolResults,
2511
+ toolNames: projectedToolNames,
2512
+ });
2513
+ }
2514
+ if (event.type === "response.output_text.delta") {
2515
+ sawDelta = true;
2516
+ wrappedOnStreamText(event.delta);
2517
+ }
2518
+ else if (event.type === "response.output_text.done" && !sawDelta) {
2519
+ wrappedOnStreamText(event.text);
2520
+ }
2521
+ }
2522
+ : undefined,
2523
+ });
2524
+ responseOutputItems = response.output ?? [];
2525
+ const mapped = mapResponseOutput(responseOutputItems);
2526
+ return {
2527
+ message: mapped.message,
2528
+ finishReason: mapped.toolCalls?.length ? "tool_calls" : "stop",
2529
+ toolCalls: mapped.toolCalls,
2530
+ usage: response.usage,
2531
+ updatedState: response.updatedState,
2532
+ };
2533
+ })()
2534
+ : await modelProvider.chat({
2535
+ model,
2536
+ messages,
2537
+ tools,
2538
+ stream: ctx.stream,
2539
+ state: ctx.state,
2540
+ deckPath: deck.path,
2541
+ signal: ctx.signal,
2542
+ params: providerParams,
2543
+ onStreamText: (ctx.onStreamText || deck.handlers?.onIdle)
2544
+ ? wrappedOnStreamText
2545
+ : undefined,
2546
+ onStreamEvent: ctx.trace
2547
+ ? (event) => {
2548
+ const handledAsResponse = traceOpenResponsesStreamEvent({
2549
+ streamEvent: event,
2550
+ runId,
2551
+ actionCallId,
2552
+ deckPath: deck.path,
2553
+ model,
2554
+ parentActionCallId: ctx.parentActionCallId,
2555
+ trace: ctx.trace,
2556
+ });
2557
+ if (!handledAsResponse) {
2558
+ ctx.trace?.({
2559
+ type: "model.stream.event",
2560
+ runId,
2561
+ actionCallId,
2562
+ deckPath: deck.path,
2563
+ model,
2564
+ event,
2565
+ parentActionCallId: ctx.parentActionCallId,
2566
+ });
2567
+ }
2568
+ projectStreamToolTraceEvents({
2569
+ streamEvent: event,
2570
+ runId,
2571
+ parentActionCallId: actionCallId,
2572
+ trace: ctx.trace,
2573
+ emittedCalls: projectedToolCalls,
2574
+ emittedResults: projectedToolResults,
2575
+ toolNames: projectedToolNames,
2576
+ });
2577
+ }
2578
+ : undefined,
2579
+ });
414
2580
  idleController.touch();
415
- const message = result.message;
2581
+ let message = result.message;
416
2582
  ctx.trace?.({
417
2583
  type: "model.result",
418
2584
  runId,
@@ -423,6 +2589,9 @@ async function runLlmDeck(ctx) {
423
2589
  message: sanitizeMessage(message),
424
2590
  toolCalls: result.toolCalls,
425
2591
  stateMessages: result.updatedState?.messages?.length,
2592
+ usage: result.usage,
2593
+ mode: useResponses ? "responses" : "chat",
2594
+ responseItems: responseOutputItems,
426
2595
  parentActionCallId: ctx.parentActionCallId,
427
2596
  });
428
2597
  const computeState = (updated) => {
@@ -431,17 +2600,31 @@ async function runLlmDeck(ctx) {
431
2600
  const mergedMessages = base.messages && base.messages.length > 0
432
2601
  ? base.messages.map(sanitizeMessage)
433
2602
  : messages.map(sanitizeMessage);
2603
+ const responseItems = useResponses
2604
+ ? responseItemsFromMessages(mergedMessages)
2605
+ : updated?.items ?? ctx.state?.items;
434
2606
  const priorRefs = updated?.messageRefs ?? ctx.state?.messageRefs ?? [];
435
2607
  const messageRefs = mergedMessages.map((m, idx) => priorRefs[idx] ?? { id: randomId("msg"), role: m.role });
436
2608
  const feedback = updated?.feedback ?? ctx.state?.feedback;
437
2609
  const traces = updated?.traces ?? ctx.state?.traces;
2610
+ const meta = updated?.meta ?? ctx.state?.meta;
2611
+ const notes = updated?.notes ?? ctx.state?.notes;
2612
+ const conversationScore = updated?.conversationScore ??
2613
+ ctx.state?.conversationScore;
438
2614
  return {
439
2615
  ...base,
440
2616
  runId,
441
2617
  messages: mergedMessages,
2618
+ format: useResponses
2619
+ ? "responses"
2620
+ : updated?.format ?? ctx.state?.format,
2621
+ items: responseItems,
442
2622
  messageRefs,
443
2623
  feedback,
444
2624
  traces,
2625
+ meta,
2626
+ notes,
2627
+ conversationScore,
445
2628
  };
446
2629
  };
447
2630
  if (result.toolCalls && result.toolCalls.length > 0) {
@@ -449,8 +2632,10 @@ async function runLlmDeck(ctx) {
449
2632
  let respondValue;
450
2633
  let endSignal;
451
2634
  const appendedMessages = [];
452
- if (!streamingCommitted && streamingBuffer) {
453
- messages.push(sanitizeMessage({ role: "assistant", content: streamingBuffer }));
2635
+ const toolCallText = streamingBuffer ||
2636
+ (typeof message.content === "string" ? message.content : "");
2637
+ if (!streamingCommitted && toolCallText) {
2638
+ messages.push(sanitizeMessage({ role: "assistant", content: toolCallText }));
454
2639
  streamingCommitted = true;
455
2640
  }
456
2641
  for (const call of result.toolCalls) {
@@ -488,6 +2673,7 @@ async function runLlmDeck(ctx) {
488
2673
  actionCallId: call.id,
489
2674
  name: call.name,
490
2675
  args: call.args,
2676
+ toolKind: "internal",
491
2677
  parentActionCallId: actionCallId,
492
2678
  });
493
2679
  const toolContent = JSON.stringify(call.args ?? {});
@@ -517,6 +2703,7 @@ async function runLlmDeck(ctx) {
517
2703
  actionCallId: call.id,
518
2704
  name: call.name,
519
2705
  result: respondEnvelope,
2706
+ toolKind: "internal",
520
2707
  parentActionCallId: actionCallId,
521
2708
  });
522
2709
  continue;
@@ -543,6 +2730,7 @@ async function runLlmDeck(ctx) {
543
2730
  actionCallId: call.id,
544
2731
  name: call.name,
545
2732
  args: call.args,
2733
+ toolKind: "internal",
546
2734
  parentActionCallId: actionCallId,
547
2735
  });
548
2736
  const toolContent = JSON.stringify(call.args ?? {});
@@ -582,10 +2770,23 @@ async function runLlmDeck(ctx) {
582
2770
  actionCallId: call.id,
583
2771
  name: call.name,
584
2772
  result: signal,
2773
+ toolKind: "internal",
585
2774
  parentActionCallId: actionCallId,
586
2775
  });
587
2776
  continue;
588
2777
  }
2778
+ const actionRef = deck.actionDecks.find((a) => a.name === call.name);
2779
+ const toolKind = actionRef ? "action" : "external";
2780
+ const actionPermissions = resolveEffectivePermissions({
2781
+ baseDir: path.dirname(deck.path),
2782
+ parent: ctx.permissions,
2783
+ reference: actionRef?.permissions
2784
+ ? {
2785
+ baseDir: path.dirname(deck.path),
2786
+ permissions: actionRef.permissions,
2787
+ }
2788
+ : undefined,
2789
+ });
589
2790
  ctx.trace?.({
590
2791
  type: "action.start",
591
2792
  runId,
@@ -593,6 +2794,7 @@ async function runLlmDeck(ctx) {
593
2794
  name: call.name,
594
2795
  path: call.name,
595
2796
  parentActionCallId: actionCallId,
2797
+ permissions: actionPermissions.trace,
596
2798
  });
597
2799
  ctx.trace?.({
598
2800
  type: "tool.call",
@@ -600,6 +2802,7 @@ async function runLlmDeck(ctx) {
600
2802
  actionCallId: call.id,
601
2803
  name: call.name,
602
2804
  args: call.args,
2805
+ toolKind,
603
2806
  parentActionCallId: actionCallId,
604
2807
  });
605
2808
  const toolResult = await handleToolCall(call, {
@@ -618,6 +2821,16 @@ async function runLlmDeck(ctx) {
618
2821
  runStartedAt: start,
619
2822
  inputProvided: true,
620
2823
  idle: idleController,
2824
+ responsesMode: ctx.responsesMode,
2825
+ permissions: ctx.permissions,
2826
+ workspacePermissions: ctx.workspacePermissions,
2827
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
2828
+ sessionPermissions: ctx.sessionPermissions,
2829
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
2830
+ runDeadlineMs: ctx.runDeadlineMs,
2831
+ workerSandbox: ctx.workerSandbox,
2832
+ signal: ctx.signal,
2833
+ onTool: ctx.onTool,
621
2834
  });
622
2835
  ctx.trace?.({
623
2836
  type: "tool.result",
@@ -625,6 +2838,7 @@ async function runLlmDeck(ctx) {
625
2838
  actionCallId: call.id,
626
2839
  name: call.name,
627
2840
  result: toolResult.toolContent,
2841
+ toolKind,
628
2842
  parentActionCallId: actionCallId,
629
2843
  });
630
2844
  appendedMessages.push({
@@ -662,6 +2876,7 @@ async function runLlmDeck(ctx) {
662
2876
  idleController.touch();
663
2877
  }
664
2878
  if (ctx.onStateUpdate) {
2879
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
665
2880
  const state = computeState(result.updatedState);
666
2881
  ctx.onStateUpdate(state);
667
2882
  }
@@ -687,6 +2902,12 @@ async function runLlmDeck(ctx) {
687
2902
  }
688
2903
  continue;
689
2904
  }
2905
+ if (!respondEnabled &&
2906
+ result.finishReason === "stop" &&
2907
+ (message.content === null || message.content === undefined) &&
2908
+ (!result.toolCalls || result.toolCalls.length === 0)) {
2909
+ message = { ...message, content: "" };
2910
+ }
690
2911
  if (result.finishReason === "tool_calls") {
691
2912
  throw new Error("Model requested tool_calls but provided none");
692
2913
  }
@@ -696,6 +2917,7 @@ async function runLlmDeck(ctx) {
696
2917
  }
697
2918
  if (message.content !== null && message.content !== undefined) {
698
2919
  messages.push(sanitizeMessage(message));
2920
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
699
2921
  if (ctx.onStateUpdate) {
700
2922
  const state = computeState(result.updatedState);
701
2923
  ctx.onStateUpdate(state);
@@ -738,23 +2960,11 @@ async function runLlmDeck(ctx) {
738
2960
  throw new Error("Model did not complete within guardrails");
739
2961
  }
740
2962
  async function handleToolCall(call, ctx) {
741
- const action = ctx.parentDeck.actionDecks.find((a) => a.name === call.name);
2963
+ ensureRunActive(ctx.runDeadlineMs, ctx.signal);
742
2964
  const source = {
743
2965
  deckPath: ctx.parentDeck.path,
744
- actionName: action?.name ?? call.name,
2966
+ actionName: call.name,
745
2967
  };
746
- if (!action) {
747
- return {
748
- toolContent: JSON.stringify({
749
- runId: ctx.runId,
750
- actionCallId: call.id,
751
- parentActionCallId: ctx.parentActionCallId,
752
- source,
753
- status: 404,
754
- message: "unknown action",
755
- }),
756
- };
757
- }
758
2968
  const baseComplete = (payload) => JSON.stringify({
759
2969
  runId: ctx.runId,
760
2970
  actionCallId: call.id,
@@ -768,6 +2978,480 @@ async function handleToolCall(call, ctx) {
768
2978
  });
769
2979
  const extraMessages = [];
770
2980
  const started = performance.now();
2981
+ const runBuiltinTool = async () => {
2982
+ if (!isBuiltinTool(call.name))
2983
+ return null;
2984
+ const deny = (message) => ({
2985
+ toolContent: baseComplete({
2986
+ status: 403,
2987
+ code: "permission_denied",
2988
+ message,
2989
+ }),
2990
+ });
2991
+ if (call.name === BUILTIN_TOOL_READ_FILE) {
2992
+ let targetPath;
2993
+ try {
2994
+ targetPath = resolveToolPath(ctx.permissions.baseDir, call.args.path);
2995
+ }
2996
+ catch (err) {
2997
+ return {
2998
+ toolContent: baseComplete({
2999
+ status: 400,
3000
+ code: "invalid_input",
3001
+ message: err instanceof Error ? err.message : String(err),
3002
+ }),
3003
+ };
3004
+ }
3005
+ if (!canReadPath(ctx.permissions, targetPath)) {
3006
+ return deny(`read_file denied for ${targetPath}`);
3007
+ }
3008
+ const text = await dntShim.Deno.readTextFile(targetPath);
3009
+ const lines = text.split(/\r?\n/);
3010
+ const { startLine, endLine } = parseLineRange(call.args);
3011
+ const sliced = lines.slice(startLine - 1, endLine).join("\n");
3012
+ return {
3013
+ toolContent: baseComplete({
3014
+ status: 200,
3015
+ payload: {
3016
+ path: targetPath,
3017
+ start_line: startLine,
3018
+ end_line: endLine,
3019
+ total_lines: lines.length,
3020
+ content: sliced,
3021
+ },
3022
+ }),
3023
+ };
3024
+ }
3025
+ if (call.name === BUILTIN_TOOL_LIST_DIR) {
3026
+ let targetPath;
3027
+ try {
3028
+ targetPath = resolveToolPath(ctx.permissions.baseDir, call.args.path);
3029
+ }
3030
+ catch (err) {
3031
+ return {
3032
+ toolContent: baseComplete({
3033
+ status: 400,
3034
+ code: "invalid_input",
3035
+ message: err instanceof Error ? err.message : String(err),
3036
+ }),
3037
+ };
3038
+ }
3039
+ if (!canReadPath(ctx.permissions, targetPath)) {
3040
+ return deny(`list_dir denied for ${targetPath}`);
3041
+ }
3042
+ const recursive = Boolean(call.args.recursive);
3043
+ const maxEntries = parseToolLimit(call.args.max_entries, 200, 2000);
3044
+ const out = [];
3045
+ const pending = [targetPath];
3046
+ while (pending.length > 0 && out.length < maxEntries) {
3047
+ const current = pending.pop();
3048
+ for await (const entry of dntShim.Deno.readDir(current)) {
3049
+ if (out.length >= maxEntries)
3050
+ break;
3051
+ const entryPath = path.join(current, entry.name);
3052
+ if (!canReadPath(ctx.permissions, entryPath))
3053
+ continue;
3054
+ const type = entry.isDirectory
3055
+ ? "dir"
3056
+ : entry.isSymlink
3057
+ ? "symlink"
3058
+ : "file";
3059
+ out.push({ path: entryPath, type });
3060
+ if (recursive && entry.isDirectory) {
3061
+ pending.push(entryPath);
3062
+ }
3063
+ }
3064
+ }
3065
+ return {
3066
+ toolContent: baseComplete({
3067
+ status: 200,
3068
+ payload: {
3069
+ path: targetPath,
3070
+ recursive,
3071
+ entries: out,
3072
+ truncated: out.length >= maxEntries,
3073
+ },
3074
+ }),
3075
+ };
3076
+ }
3077
+ if (call.name === BUILTIN_TOOL_GREP_FILES) {
3078
+ let targetPath;
3079
+ try {
3080
+ targetPath = resolveToolPath(ctx.permissions.baseDir, call.args.path);
3081
+ }
3082
+ catch (err) {
3083
+ return {
3084
+ toolContent: baseComplete({
3085
+ status: 400,
3086
+ code: "invalid_input",
3087
+ message: err instanceof Error ? err.message : String(err),
3088
+ }),
3089
+ };
3090
+ }
3091
+ if (!canReadPath(ctx.permissions, targetPath)) {
3092
+ return deny(`grep_files denied for ${targetPath}`);
3093
+ }
3094
+ const query = typeof call.args.query === "string" ? call.args.query : "";
3095
+ if (!query) {
3096
+ return {
3097
+ toolContent: baseComplete({
3098
+ status: 400,
3099
+ code: "invalid_input",
3100
+ message: "query is required",
3101
+ }),
3102
+ };
3103
+ }
3104
+ let re;
3105
+ try {
3106
+ re = new RegExp(query, "g");
3107
+ }
3108
+ catch (err) {
3109
+ return {
3110
+ toolContent: baseComplete({
3111
+ status: 400,
3112
+ code: "invalid_regex",
3113
+ message: err instanceof Error ? err.message : String(err),
3114
+ }),
3115
+ };
3116
+ }
3117
+ const maxMatches = parseToolLimit(call.args.max_matches, 200, 2000);
3118
+ const matches = [];
3119
+ const pending = [targetPath];
3120
+ while (pending.length > 0 && matches.length < maxMatches) {
3121
+ const current = pending.pop();
3122
+ const stat = await dntShim.Deno.stat(current);
3123
+ if (stat.isDirectory) {
3124
+ for await (const entry of dntShim.Deno.readDir(current)) {
3125
+ const entryPath = path.join(current, entry.name);
3126
+ if (!canReadPath(ctx.permissions, entryPath))
3127
+ continue;
3128
+ if (entry.isDirectory) {
3129
+ pending.push(entryPath);
3130
+ continue;
3131
+ }
3132
+ if (!entry.isFile)
3133
+ continue;
3134
+ const text = await dntShim.Deno.readTextFile(entryPath).catch(() => null);
3135
+ if (text === null)
3136
+ continue;
3137
+ const lines = text.split(/\r?\n/);
3138
+ for (let i = 0; i < lines.length; i++) {
3139
+ re.lastIndex = 0;
3140
+ if (!re.test(lines[i]))
3141
+ continue;
3142
+ matches.push({ path: entryPath, line: i + 1, text: lines[i] });
3143
+ if (matches.length >= maxMatches)
3144
+ break;
3145
+ }
3146
+ if (matches.length >= maxMatches)
3147
+ break;
3148
+ }
3149
+ continue;
3150
+ }
3151
+ if (!stat.isFile)
3152
+ continue;
3153
+ const text = await dntShim.Deno.readTextFile(current).catch(() => null);
3154
+ if (text === null)
3155
+ continue;
3156
+ const lines = text.split(/\r?\n/);
3157
+ for (let i = 0; i < lines.length; i++) {
3158
+ re.lastIndex = 0;
3159
+ if (!re.test(lines[i]))
3160
+ continue;
3161
+ matches.push({ path: current, line: i + 1, text: lines[i] });
3162
+ if (matches.length >= maxMatches)
3163
+ break;
3164
+ }
3165
+ }
3166
+ return {
3167
+ toolContent: baseComplete({
3168
+ status: 200,
3169
+ payload: {
3170
+ path: targetPath,
3171
+ query,
3172
+ matches,
3173
+ truncated: matches.length >= maxMatches,
3174
+ },
3175
+ }),
3176
+ };
3177
+ }
3178
+ if (call.name === BUILTIN_TOOL_APPLY_PATCH) {
3179
+ let targetPath;
3180
+ try {
3181
+ targetPath = resolveToolPath(ctx.permissions.baseDir, call.args.path);
3182
+ }
3183
+ catch (err) {
3184
+ return {
3185
+ toolContent: baseComplete({
3186
+ status: 400,
3187
+ code: "invalid_input",
3188
+ message: err instanceof Error ? err.message : String(err),
3189
+ }),
3190
+ };
3191
+ }
3192
+ if (!canWritePath(ctx.permissions, targetPath)) {
3193
+ return deny(`apply_patch denied for ${targetPath}`);
3194
+ }
3195
+ const rawEdits = Array.isArray(call.args.edits) ? call.args.edits : [];
3196
+ const edits = rawEdits.flatMap((entry) => {
3197
+ if (!entry || typeof entry !== "object")
3198
+ return [];
3199
+ const rec = entry;
3200
+ if (typeof rec.old_text !== "string" || typeof rec.new_text !== "string") {
3201
+ return [];
3202
+ }
3203
+ return [{
3204
+ oldText: rec.old_text,
3205
+ newText: rec.new_text,
3206
+ replaceAll: Boolean(rec.replace_all),
3207
+ }];
3208
+ });
3209
+ if (edits.length === 0) {
3210
+ return {
3211
+ toolContent: baseComplete({
3212
+ status: 400,
3213
+ code: "invalid_input",
3214
+ message: "edits must include at least one old_text/new_text pair",
3215
+ }),
3216
+ };
3217
+ }
3218
+ const createIfMissing = Boolean(call.args.create_if_missing);
3219
+ let existing = "";
3220
+ let created = false;
3221
+ try {
3222
+ if (!canReadPath(ctx.permissions, targetPath)) {
3223
+ return deny(`apply_patch read denied for ${targetPath}`);
3224
+ }
3225
+ existing = await dntShim.Deno.readTextFile(targetPath);
3226
+ }
3227
+ catch (err) {
3228
+ if (err instanceof dntShim.Deno.errors.NotFound) {
3229
+ if (!createIfMissing) {
3230
+ return {
3231
+ toolContent: baseComplete({
3232
+ status: 404,
3233
+ code: "not_found",
3234
+ message: `file not found: ${targetPath}`,
3235
+ }),
3236
+ };
3237
+ }
3238
+ created = true;
3239
+ existing = "";
3240
+ }
3241
+ else {
3242
+ throw err;
3243
+ }
3244
+ }
3245
+ const patched = applySimplePatch(existing, edits);
3246
+ if (!created && patched.applied === 0) {
3247
+ return {
3248
+ toolContent: baseComplete({
3249
+ status: 409,
3250
+ code: "no_changes",
3251
+ message: `No edit targets were found in ${targetPath}`,
3252
+ }),
3253
+ };
3254
+ }
3255
+ if (created) {
3256
+ const parentDir = path.dirname(targetPath);
3257
+ if (parentDir && parentDir !== "." && parentDir !== targetPath) {
3258
+ await dntShim.Deno.mkdir(parentDir, { recursive: true });
3259
+ }
3260
+ }
3261
+ try {
3262
+ await dntShim.Deno.writeTextFile(targetPath, patched.next);
3263
+ }
3264
+ catch (err) {
3265
+ if (err instanceof dntShim.Deno.errors.NotFound) {
3266
+ return {
3267
+ toolContent: baseComplete({
3268
+ status: 404,
3269
+ code: "not_found",
3270
+ message: `path not found: ${targetPath}`,
3271
+ }),
3272
+ };
3273
+ }
3274
+ return {
3275
+ toolContent: baseComplete({
3276
+ status: 500,
3277
+ code: "write_failed",
3278
+ message: err instanceof Error ? err.message : String(err),
3279
+ }),
3280
+ };
3281
+ }
3282
+ return {
3283
+ toolContent: baseComplete({
3284
+ status: 200,
3285
+ payload: {
3286
+ path: targetPath,
3287
+ applied: patched.applied,
3288
+ created,
3289
+ },
3290
+ }),
3291
+ };
3292
+ }
3293
+ if (call.name === BUILTIN_TOOL_EXEC) {
3294
+ const command = typeof call.args.command === "string"
3295
+ ? call.args.command
3296
+ : "";
3297
+ if (!command) {
3298
+ return {
3299
+ toolContent: baseComplete({
3300
+ status: 400,
3301
+ code: "invalid_input",
3302
+ message: "command is required",
3303
+ }),
3304
+ };
3305
+ }
3306
+ if (!canRunCommand(ctx.permissions, command) &&
3307
+ !canRunPath(ctx.permissions, command)) {
3308
+ return deny(`exec denied for command ${command}`);
3309
+ }
3310
+ const args = toStringArray(call.args.args);
3311
+ const cwd = typeof call.args.cwd === "string"
3312
+ ? path.resolve(ctx.permissions.baseDir, call.args.cwd)
3313
+ : ctx.permissions.baseDir;
3314
+ const timeoutMs = parseToolLimit(call.args.timeout_ms, 5000, 30000);
3315
+ const remainingMs = Math.max(1, Math.min(timeoutMs, Math.floor(ctx.runDeadlineMs - performance.now())));
3316
+ const controller = new AbortController();
3317
+ const onAbort = () => controller.abort();
3318
+ if (ctx.signal?.aborted) {
3319
+ controller.abort();
3320
+ }
3321
+ else if (ctx.signal) {
3322
+ ctx.signal.addEventListener("abort", onAbort, { once: true });
3323
+ }
3324
+ const timeoutId = setTimeout(() => controller.abort(), remainingMs);
3325
+ try {
3326
+ const output = await executeBuiltinCommand({
3327
+ command,
3328
+ args,
3329
+ cwd,
3330
+ signal: controller.signal,
3331
+ });
3332
+ const stdout = new TextDecoder().decode(output.stdout).slice(0, 65536);
3333
+ const stderr = new TextDecoder().decode(output.stderr).slice(0, 65536);
3334
+ return {
3335
+ toolContent: baseComplete({
3336
+ status: 200,
3337
+ payload: {
3338
+ command,
3339
+ args,
3340
+ cwd,
3341
+ code: output.code,
3342
+ success: output.success,
3343
+ stdout,
3344
+ stderr,
3345
+ },
3346
+ }),
3347
+ };
3348
+ }
3349
+ catch (err) {
3350
+ if (err instanceof ExecToolUnsupportedHostError) {
3351
+ return {
3352
+ toolContent: baseComplete({
3353
+ status: 501,
3354
+ code: err.code,
3355
+ message: err.message,
3356
+ }),
3357
+ };
3358
+ }
3359
+ return {
3360
+ toolContent: baseComplete({
3361
+ status: 500,
3362
+ code: "exec_failed",
3363
+ message: err instanceof Error ? err.message : String(err),
3364
+ }),
3365
+ };
3366
+ }
3367
+ finally {
3368
+ clearTimeout(timeoutId);
3369
+ if (ctx.signal) {
3370
+ ctx.signal.removeEventListener("abort", onAbort);
3371
+ }
3372
+ }
3373
+ }
3374
+ return null;
3375
+ };
3376
+ const builtinResult = await runBuiltinTool();
3377
+ if (builtinResult) {
3378
+ return builtinResult;
3379
+ }
3380
+ const action = ctx.parentDeck.actionDecks.find((a) => a.name === call.name);
3381
+ if (!action) {
3382
+ const externalTool = ctx.parentDeck.tools.find((tool) => tool.name === call.name);
3383
+ if (!externalTool) {
3384
+ return {
3385
+ toolContent: JSON.stringify({
3386
+ runId: ctx.runId,
3387
+ actionCallId: call.id,
3388
+ parentActionCallId: ctx.parentActionCallId,
3389
+ source,
3390
+ status: 404,
3391
+ message: "unknown action",
3392
+ }),
3393
+ };
3394
+ }
3395
+ let externalInput = call.args;
3396
+ if (externalTool.inputSchema) {
3397
+ try {
3398
+ externalInput = validateWithSchema(externalTool.inputSchema, call.args);
3399
+ }
3400
+ catch (err) {
3401
+ return {
3402
+ toolContent: baseComplete({
3403
+ status: 400,
3404
+ code: "invalid_input",
3405
+ message: err instanceof Error ? err.message : String(err),
3406
+ }),
3407
+ };
3408
+ }
3409
+ }
3410
+ if (!ctx.onTool) {
3411
+ return {
3412
+ toolContent: baseComplete({
3413
+ status: 500,
3414
+ code: "missing_on_tool",
3415
+ message: `External tool ${call.name} requires runtime onTool handler`,
3416
+ }),
3417
+ };
3418
+ }
3419
+ try {
3420
+ const result = await ctx.onTool({
3421
+ name: call.name,
3422
+ args: externalInput,
3423
+ runId: ctx.runId,
3424
+ actionCallId: call.id,
3425
+ parentActionCallId: ctx.parentActionCallId,
3426
+ deckPath: ctx.parentDeck.path,
3427
+ });
3428
+ return { toolContent: baseComplete(normalizeChildResult(result)) };
3429
+ }
3430
+ catch (err) {
3431
+ return {
3432
+ toolContent: baseComplete({
3433
+ status: 500,
3434
+ code: "tool_handler_error",
3435
+ message: err instanceof Error ? err.message : String(err),
3436
+ }),
3437
+ };
3438
+ }
3439
+ }
3440
+ let actionInput = call.args;
3441
+ if (action.contextSchema) {
3442
+ try {
3443
+ actionInput = validateWithSchema(action.contextSchema, call.args);
3444
+ }
3445
+ catch (err) {
3446
+ return {
3447
+ toolContent: baseComplete({
3448
+ status: 400,
3449
+ code: "invalid_input",
3450
+ message: err instanceof Error ? err.message : String(err),
3451
+ }),
3452
+ };
3453
+ }
3454
+ }
771
3455
  const busyCfg = ctx.parentDeck.handlers?.onBusy ??
772
3456
  ctx.parentDeck.handlers?.onInterval;
773
3457
  const busyDelay = busyCfg?.delayMs ?? DEFAULT_STATUS_DELAY_MS;
@@ -781,7 +3465,7 @@ async function handleToolCall(call, ctx) {
781
3465
  try {
782
3466
  const result = await runDeck({
783
3467
  path: action.path,
784
- input: call.args,
3468
+ input: actionInput,
785
3469
  modelProvider: ctx.modelProvider,
786
3470
  isRoot: false,
787
3471
  guardrails: ctx.guardrails,
@@ -793,7 +3477,19 @@ async function handleToolCall(call, ctx) {
793
3477
  trace: ctx.trace,
794
3478
  stream: ctx.stream,
795
3479
  onStreamText: ctx.onStreamText,
3480
+ responsesMode: ctx.responsesMode,
796
3481
  initialUserMessage: undefined,
3482
+ parentPermissions: ctx.permissions,
3483
+ referencePermissions: action.permissions,
3484
+ referencePermissionsBaseDir: path.dirname(ctx.parentDeck.path),
3485
+ workspacePermissions: ctx.workspacePermissions,
3486
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
3487
+ sessionPermissions: ctx.sessionPermissions,
3488
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
3489
+ runDeadlineMs: ctx.runDeadlineMs,
3490
+ workerSandbox: ctx.workerSandbox,
3491
+ signal: ctx.signal,
3492
+ onTool: ctx.onTool,
797
3493
  });
798
3494
  return { ok: true, result };
799
3495
  }
@@ -825,7 +3521,17 @@ async function handleToolCall(call, ctx) {
825
3521
  trace: ctx.trace,
826
3522
  stream: ctx.stream,
827
3523
  onStreamText: ctx.onStreamText,
3524
+ responsesMode: ctx.responsesMode,
828
3525
  initialUserMessage: undefined,
3526
+ permissions: ctx.permissions,
3527
+ workspacePermissions: ctx.workspacePermissions,
3528
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
3529
+ sessionPermissions: ctx.sessionPermissions,
3530
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
3531
+ runDeadlineMs: ctx.runDeadlineMs,
3532
+ workerSandbox: ctx.workerSandbox,
3533
+ signal: ctx.signal,
3534
+ onTool: ctx.onTool,
829
3535
  });
830
3536
  if (envelope.length) {
831
3537
  extraMessages.push(...envelope.map(sanitizeMessage));
@@ -883,6 +3589,9 @@ async function handleToolCall(call, ctx) {
883
3589
  throw childResult.error;
884
3590
  }
885
3591
  const normalized = normalizeChildResult(childResult.result);
3592
+ if (action.responseSchema) {
3593
+ normalized.payload = validateWithSchema(action.responseSchema, normalized.payload);
3594
+ }
886
3595
  const toolContent = baseComplete(normalized);
887
3596
  if (busyCfg?.path) {
888
3597
  const elapsedFromAction = performance.now() - started;
@@ -904,7 +3613,17 @@ async function handleToolCall(call, ctx) {
904
3613
  trace: ctx.trace,
905
3614
  stream: ctx.stream,
906
3615
  onStreamText: ctx.onStreamText,
3616
+ responsesMode: ctx.responsesMode,
907
3617
  initialUserMessage: undefined,
3618
+ permissions: ctx.permissions,
3619
+ workspacePermissions: ctx.workspacePermissions,
3620
+ workspacePermissionsBaseDir: ctx.workspacePermissionsBaseDir,
3621
+ sessionPermissions: ctx.sessionPermissions,
3622
+ sessionPermissionsBaseDir: ctx.sessionPermissionsBaseDir,
3623
+ runDeadlineMs: ctx.runDeadlineMs,
3624
+ workerSandbox: ctx.workerSandbox,
3625
+ signal: ctx.signal,
3626
+ onTool: ctx.onTool,
908
3627
  });
909
3628
  if (envelope.length) {
910
3629
  extraMessages.push(...envelope.map(sanitizeMessage));
@@ -954,6 +3673,7 @@ function normalizeChildResult(result) {
954
3673
  }
955
3674
  async function runBusyHandler(args) {
956
3675
  try {
3676
+ ensureRunActive(args.runDeadlineMs, args.signal);
957
3677
  const input = {
958
3678
  kind: "busy",
959
3679
  label: args.action.label ?? args.parentDeck.label,
@@ -978,8 +3698,18 @@ async function runBusyHandler(args) {
978
3698
  trace: args.trace,
979
3699
  stream: args.stream,
980
3700
  onStreamText: args.onStreamText,
3701
+ responsesMode: args.responsesMode,
981
3702
  initialUserMessage: args.initialUserMessage,
982
3703
  inputProvided: true,
3704
+ parentPermissions: args.permissions,
3705
+ workspacePermissions: args.workspacePermissions,
3706
+ workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
3707
+ sessionPermissions: args.sessionPermissions,
3708
+ sessionPermissionsBaseDir: args.sessionPermissionsBaseDir,
3709
+ runDeadlineMs: args.runDeadlineMs,
3710
+ workerSandbox: args.workerSandbox,
3711
+ signal: args.signal,
3712
+ onTool: args.onTool,
983
3713
  });
984
3714
  const elapsedMs = Math.floor(args.elapsedMs);
985
3715
  let message;
@@ -996,7 +3726,7 @@ async function runBusyHandler(args) {
996
3726
  }
997
3727
  if (!message)
998
3728
  return [];
999
- if (args.onStreamText) {
3729
+ if (args.onStreamText && !args.signal?.aborted) {
1000
3730
  args.onStreamText(`${message}\n`);
1001
3731
  }
1002
3732
  else {
@@ -1056,6 +3786,16 @@ function createIdleController(args) {
1056
3786
  trace: args.trace,
1057
3787
  stream: args.stream,
1058
3788
  onStreamText: args.onStreamText,
3789
+ responsesMode: args.responsesMode,
3790
+ permissions: args.permissions,
3791
+ workspacePermissions: args.workspacePermissions,
3792
+ workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
3793
+ sessionPermissions: args.sessionPermissions,
3794
+ sessionPermissionsBaseDir: args.sessionPermissionsBaseDir,
3795
+ runDeadlineMs: args.runDeadlineMs,
3796
+ workerSandbox: args.workerSandbox,
3797
+ signal: args.signal,
3798
+ onTool: args.onTool,
1059
3799
  });
1060
3800
  if (envelope.length)
1061
3801
  args.pushMessages(envelope.map(sanitizeMessage));
@@ -1095,6 +3835,7 @@ function createIdleController(args) {
1095
3835
  }
1096
3836
  async function runIdleHandler(args) {
1097
3837
  try {
3838
+ ensureRunActive(args.runDeadlineMs, args.signal);
1098
3839
  const input = {
1099
3840
  kind: "idle",
1100
3841
  label: args.deck.label,
@@ -1118,8 +3859,18 @@ async function runIdleHandler(args) {
1118
3859
  trace: args.trace,
1119
3860
  stream: args.stream,
1120
3861
  onStreamText: args.onStreamText,
3862
+ responsesMode: args.responsesMode,
1121
3863
  initialUserMessage: undefined,
1122
3864
  inputProvided: true,
3865
+ parentPermissions: args.permissions,
3866
+ workspacePermissions: args.workspacePermissions,
3867
+ workspacePermissionsBaseDir: args.workspacePermissionsBaseDir,
3868
+ sessionPermissions: args.sessionPermissions,
3869
+ sessionPermissionsBaseDir: args.sessionPermissionsBaseDir,
3870
+ runDeadlineMs: args.runDeadlineMs,
3871
+ workerSandbox: args.workerSandbox,
3872
+ signal: args.signal,
3873
+ onTool: args.onTool,
1123
3874
  });
1124
3875
  const elapsedMs = Math.floor(args.elapsedMs);
1125
3876
  let message;
@@ -1136,7 +3887,7 @@ async function runIdleHandler(args) {
1136
3887
  }
1137
3888
  if (!message)
1138
3889
  return [];
1139
- if (args.onStreamText) {
3890
+ if (args.onStreamText && !args.signal?.aborted) {
1140
3891
  args.onStreamText(`${message}\n`);
1141
3892
  }
1142
3893
  else {
@@ -1155,6 +3906,7 @@ async function maybeHandleError(args) {
1155
3906
  const handlerPath = args.ctx.parentDeck.handlers?.onError?.path;
1156
3907
  if (!handlerPath)
1157
3908
  return undefined;
3909
+ ensureRunActive(args.ctx.runDeadlineMs, args.ctx.signal);
1158
3910
  const message = args.err instanceof Error
1159
3911
  ? args.err.message
1160
3912
  : String(args.err);
@@ -1183,8 +3935,18 @@ async function maybeHandleError(args) {
1183
3935
  trace: args.ctx.trace,
1184
3936
  stream: args.ctx.stream,
1185
3937
  onStreamText: args.ctx.onStreamText,
3938
+ responsesMode: args.ctx.responsesMode,
1186
3939
  initialUserMessage: undefined,
1187
3940
  inputProvided: true,
3941
+ parentPermissions: args.ctx.permissions,
3942
+ workspacePermissions: args.ctx.workspacePermissions,
3943
+ workspacePermissionsBaseDir: args.ctx.workspacePermissionsBaseDir,
3944
+ sessionPermissions: args.ctx.sessionPermissions,
3945
+ sessionPermissionsBaseDir: args.ctx.sessionPermissionsBaseDir,
3946
+ runDeadlineMs: args.ctx.runDeadlineMs,
3947
+ workerSandbox: args.ctx.workerSandbox,
3948
+ signal: args.ctx.signal,
3949
+ onTool: args.ctx.onTool,
1188
3950
  });
1189
3951
  const parsed = typeof handlerOutput === "object" && handlerOutput !== null
1190
3952
  ? handlerOutput
@@ -1308,8 +4070,173 @@ function sanitizeMessage(msg) {
1308
4070
  : undefined;
1309
4071
  return { ...msg, tool_calls: toolCalls };
1310
4072
  }
1311
- async function buildToolDefs(deck) {
4073
+ function resolveToolPath(baseDir, rawPath) {
4074
+ if (typeof rawPath !== "string" || rawPath.trim().length === 0) {
4075
+ throw new Error("path is required");
4076
+ }
4077
+ return path.resolve(baseDir, rawPath);
4078
+ }
4079
+ function parseLineRange(args) {
4080
+ const startLine = Number.isInteger(args.start_line)
4081
+ ? Math.max(1, Number(args.start_line))
4082
+ : 1;
4083
+ const endLine = Number.isInteger(args.end_line)
4084
+ ? Math.max(startLine, Number(args.end_line))
4085
+ : startLine + 399;
4086
+ return { startLine, endLine };
4087
+ }
4088
+ function parseToolLimit(value, fallback, max) {
4089
+ if (!Number.isInteger(value))
4090
+ return fallback;
4091
+ return Math.min(max, Math.max(1, Number(value)));
4092
+ }
4093
+ function toStringArray(value) {
4094
+ if (!Array.isArray(value))
4095
+ return [];
4096
+ return value.filter((entry) => typeof entry === "string");
4097
+ }
4098
+ function hasAnyScope(scope) {
4099
+ return scope.all || scope.values.size > 0;
4100
+ }
4101
+ function hasAnyRunScope(scope) {
4102
+ return scope.all || scope.paths.size > 0 || scope.commands.size > 0;
4103
+ }
4104
+ function isBuiltinTool(name) {
4105
+ return BUILTIN_TOOL_NAMES.has(name);
4106
+ }
4107
+ function applySimplePatch(content, edits) {
4108
+ let next = content;
4109
+ let applied = 0;
4110
+ for (const edit of edits) {
4111
+ const oldText = edit.oldText ?? "";
4112
+ const newText = edit.newText ?? "";
4113
+ if (!oldText)
4114
+ continue;
4115
+ if (edit.replaceAll) {
4116
+ if (!next.includes(oldText))
4117
+ continue;
4118
+ next = next.split(oldText).join(newText);
4119
+ applied++;
4120
+ continue;
4121
+ }
4122
+ const idx = next.indexOf(oldText);
4123
+ if (idx === -1)
4124
+ continue;
4125
+ next = `${next.slice(0, idx)}${newText}${next.slice(idx + oldText.length)}`;
4126
+ applied++;
4127
+ }
4128
+ return { next, applied };
4129
+ }
4130
+ async function buildToolDefs(deck, permissions) {
1312
4131
  const defs = [];
4132
+ const addBuiltinTools = () => {
4133
+ if (hasAnyScope(permissions.read)) {
4134
+ defs.push({
4135
+ type: "function",
4136
+ function: {
4137
+ name: BUILTIN_TOOL_READ_FILE,
4138
+ description: "Read a UTF-8 text file.",
4139
+ parameters: {
4140
+ type: "object",
4141
+ properties: {
4142
+ path: { type: "string" },
4143
+ start_line: { type: "number" },
4144
+ end_line: { type: "number" },
4145
+ },
4146
+ required: ["path"],
4147
+ additionalProperties: false,
4148
+ },
4149
+ },
4150
+ }, {
4151
+ type: "function",
4152
+ function: {
4153
+ name: BUILTIN_TOOL_LIST_DIR,
4154
+ description: "List directory entries.",
4155
+ parameters: {
4156
+ type: "object",
4157
+ properties: {
4158
+ path: { type: "string" },
4159
+ recursive: { type: "boolean" },
4160
+ max_entries: { type: "number" },
4161
+ },
4162
+ required: ["path"],
4163
+ additionalProperties: false,
4164
+ },
4165
+ },
4166
+ }, {
4167
+ type: "function",
4168
+ function: {
4169
+ name: BUILTIN_TOOL_GREP_FILES,
4170
+ description: "Search text files using a regular expression.",
4171
+ parameters: {
4172
+ type: "object",
4173
+ properties: {
4174
+ path: { type: "string" },
4175
+ query: { type: "string" },
4176
+ max_matches: { type: "number" },
4177
+ },
4178
+ required: ["path", "query"],
4179
+ additionalProperties: false,
4180
+ },
4181
+ },
4182
+ });
4183
+ }
4184
+ if (hasAnyScope(permissions.write)) {
4185
+ defs.push({
4186
+ type: "function",
4187
+ function: {
4188
+ name: BUILTIN_TOOL_APPLY_PATCH,
4189
+ description: "Apply text replacements to a file using old/new edit pairs.",
4190
+ parameters: {
4191
+ type: "object",
4192
+ properties: {
4193
+ path: { type: "string" },
4194
+ create_if_missing: { type: "boolean" },
4195
+ edits: {
4196
+ type: "array",
4197
+ items: {
4198
+ type: "object",
4199
+ properties: {
4200
+ old_text: { type: "string" },
4201
+ new_text: { type: "string" },
4202
+ replace_all: { type: "boolean" },
4203
+ },
4204
+ required: ["old_text", "new_text"],
4205
+ additionalProperties: false,
4206
+ },
4207
+ },
4208
+ },
4209
+ required: ["path", "edits"],
4210
+ additionalProperties: false,
4211
+ },
4212
+ },
4213
+ });
4214
+ }
4215
+ if (hasAnyRunScope(permissions.run)) {
4216
+ defs.push({
4217
+ type: "function",
4218
+ function: {
4219
+ name: BUILTIN_TOOL_EXEC,
4220
+ description: "Run an allowed command with optional args.",
4221
+ parameters: {
4222
+ type: "object",
4223
+ properties: {
4224
+ command: { type: "string" },
4225
+ args: {
4226
+ type: "array",
4227
+ items: { type: "string" },
4228
+ },
4229
+ cwd: { type: "string" },
4230
+ timeout_ms: { type: "number" },
4231
+ },
4232
+ required: ["command"],
4233
+ additionalProperties: false,
4234
+ },
4235
+ },
4236
+ });
4237
+ }
4238
+ };
4239
+ addBuiltinTools();
1313
4240
  if (deck.allowEnd) {
1314
4241
  defs.push({
1315
4242
  type: "function",
@@ -1351,9 +4278,15 @@ async function buildToolDefs(deck) {
1351
4278
  });
1352
4279
  }
1353
4280
  for (const action of deck.actionDecks) {
1354
- const child = await loadDeck(action.path, deck.path);
1355
- ensureSchemaPresence(child, false);
1356
- const schema = child.inputSchema;
4281
+ if (isBuiltinTool(action.name)) {
4282
+ throw new Error(`Action name ${action.name} conflicts with a built-in tool name`);
4283
+ }
4284
+ let schema = action.contextSchema;
4285
+ if (!schema) {
4286
+ const child = await loadDeck(action.path, deck.path);
4287
+ ensureSchemaPresence(child, false);
4288
+ schema = resolveContextSchema(child);
4289
+ }
1357
4290
  const params = toJsonSchema(schema);
1358
4291
  defs.push({
1359
4292
  type: "function",
@@ -1364,5 +4297,23 @@ async function buildToolDefs(deck) {
1364
4297
  },
1365
4298
  });
1366
4299
  }
4300
+ const actionNames = new Set(deck.actionDecks.map((action) => action.name));
4301
+ for (const external of deck.tools) {
4302
+ if (actionNames.has(external.name))
4303
+ continue;
4304
+ if (isBuiltinTool(external.name)) {
4305
+ throw new Error(`External tool name ${external.name} conflicts with a built-in tool name`);
4306
+ }
4307
+ defs.push({
4308
+ type: "function",
4309
+ function: {
4310
+ name: external.name,
4311
+ description: external.description,
4312
+ parameters: external.inputSchema
4313
+ ? toJsonSchema(external.inputSchema)
4314
+ : { type: "object", additionalProperties: true },
4315
+ },
4316
+ });
4317
+ }
1367
4318
  return defs;
1368
4319
  }