@ny4n/durable 0.0.0-stage → 1.0.0

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 (303) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +711 -2
  3. package/dist/documents.d.ts +66 -0
  4. package/dist/documents.d.ts.map +1 -0
  5. package/dist/documents.js +98 -0
  6. package/dist/documents.js.map +1 -0
  7. package/dist/entries.d.ts +31 -0
  8. package/dist/entries.d.ts.map +1 -0
  9. package/dist/entries.js +31 -0
  10. package/dist/entries.js.map +1 -0
  11. package/dist/env/decode.d.ts +16 -0
  12. package/dist/env/decode.d.ts.map +1 -0
  13. package/dist/env/decode.js +28 -0
  14. package/dist/env/decode.js.map +1 -0
  15. package/dist/env/index.d.ts +263 -0
  16. package/dist/env/index.d.ts.map +1 -0
  17. package/dist/env/index.js +49 -0
  18. package/dist/env/index.js.map +1 -0
  19. package/dist/env/line-scan.d.ts +13 -0
  20. package/dist/env/line-scan.d.ts.map +1 -0
  21. package/dist/env/line-scan.js +147 -0
  22. package/dist/env/line-scan.js.map +1 -0
  23. package/dist/env/node-watch.d.ts +29 -0
  24. package/dist/env/node-watch.d.ts.map +1 -0
  25. package/dist/env/node-watch.js +485 -0
  26. package/dist/env/node-watch.js.map +1 -0
  27. package/dist/env/node.d.ts +56 -0
  28. package/dist/env/node.d.ts.map +1 -0
  29. package/dist/env/node.js +1166 -0
  30. package/dist/env/node.js.map +1 -0
  31. package/dist/errors.d.ts +32 -0
  32. package/dist/errors.d.ts.map +1 -0
  33. package/dist/errors.js +53 -0
  34. package/dist/errors.js.map +1 -0
  35. package/dist/harness/agent.d.ts +33 -0
  36. package/dist/harness/agent.d.ts.map +1 -0
  37. package/dist/harness/agent.js +235 -0
  38. package/dist/harness/agent.js.map +1 -0
  39. package/dist/harness/assistant.d.ts +15 -0
  40. package/dist/harness/assistant.d.ts.map +1 -0
  41. package/dist/harness/assistant.js +23 -0
  42. package/dist/harness/assistant.js.map +1 -0
  43. package/dist/harness/compaction.d.ts +56 -0
  44. package/dist/harness/compaction.d.ts.map +1 -0
  45. package/dist/harness/compaction.js +384 -0
  46. package/dist/harness/compaction.js.map +1 -0
  47. package/dist/harness/context.d.ts +63 -0
  48. package/dist/harness/context.d.ts.map +1 -0
  49. package/dist/harness/context.js +221 -0
  50. package/dist/harness/context.js.map +1 -0
  51. package/dist/harness/define.d.ts +18 -0
  52. package/dist/harness/define.d.ts.map +1 -0
  53. package/dist/harness/define.js +25 -0
  54. package/dist/harness/define.js.map +1 -0
  55. package/dist/harness/events.d.ts +180 -0
  56. package/dist/harness/events.d.ts.map +1 -0
  57. package/dist/harness/events.js +370 -0
  58. package/dist/harness/events.js.map +1 -0
  59. package/dist/harness/generation.d.ts +54 -0
  60. package/dist/harness/generation.d.ts.map +1 -0
  61. package/dist/harness/generation.js +514 -0
  62. package/dist/harness/generation.js.map +1 -0
  63. package/dist/harness/harness.d.ts +17 -0
  64. package/dist/harness/harness.d.ts.map +1 -0
  65. package/dist/harness/harness.js +332 -0
  66. package/dist/harness/harness.js.map +1 -0
  67. package/dist/harness/inbox.d.ts +59 -0
  68. package/dist/harness/inbox.d.ts.map +1 -0
  69. package/dist/harness/inbox.js +97 -0
  70. package/dist/harness/inbox.js.map +1 -0
  71. package/dist/harness/json.d.ts +10 -0
  72. package/dist/harness/json.d.ts.map +1 -0
  73. package/dist/harness/json.js +33 -0
  74. package/dist/harness/json.js.map +1 -0
  75. package/dist/harness/live.d.ts +142 -0
  76. package/dist/harness/live.d.ts.map +1 -0
  77. package/dist/harness/live.js +149 -0
  78. package/dist/harness/live.js.map +1 -0
  79. package/dist/harness/output.d.ts +69 -0
  80. package/dist/harness/output.d.ts.map +1 -0
  81. package/dist/harness/output.js +318 -0
  82. package/dist/harness/output.js.map +1 -0
  83. package/dist/harness/prompt.d.ts +27 -0
  84. package/dist/harness/prompt.d.ts.map +1 -0
  85. package/dist/harness/prompt.js +131 -0
  86. package/dist/harness/prompt.js.map +1 -0
  87. package/dist/harness/provider.d.ts +14 -0
  88. package/dist/harness/provider.d.ts.map +1 -0
  89. package/dist/harness/provider.js +30 -0
  90. package/dist/harness/provider.js.map +1 -0
  91. package/dist/harness/registry.d.ts +6 -0
  92. package/dist/harness/registry.d.ts.map +1 -0
  93. package/dist/harness/registry.js +101 -0
  94. package/dist/harness/registry.js.map +1 -0
  95. package/dist/harness/run.d.ts +17 -0
  96. package/dist/harness/run.d.ts.map +1 -0
  97. package/dist/harness/run.js +26 -0
  98. package/dist/harness/run.js.map +1 -0
  99. package/dist/harness/scheduler.d.ts +106 -0
  100. package/dist/harness/scheduler.d.ts.map +1 -0
  101. package/dist/harness/scheduler.js +1743 -0
  102. package/dist/harness/scheduler.js.map +1 -0
  103. package/dist/harness/submissions.d.ts +29 -0
  104. package/dist/harness/submissions.d.ts.map +1 -0
  105. package/dist/harness/submissions.js +175 -0
  106. package/dist/harness/submissions.js.map +1 -0
  107. package/dist/harness/task-graph.d.ts +49 -0
  108. package/dist/harness/task-graph.d.ts.map +1 -0
  109. package/dist/harness/task-graph.js +168 -0
  110. package/dist/harness/task-graph.js.map +1 -0
  111. package/dist/harness/tool.d.ts +76 -0
  112. package/dist/harness/tool.d.ts.map +1 -0
  113. package/dist/harness/tool.js +736 -0
  114. package/dist/harness/tool.js.map +1 -0
  115. package/dist/harness/types.d.ts +713 -0
  116. package/dist/harness/types.d.ts.map +1 -0
  117. package/dist/harness/types.js +2 -0
  118. package/dist/harness/types.js.map +1 -0
  119. package/dist/harness/usage.d.ts +18 -0
  120. package/dist/harness/usage.d.ts.map +1 -0
  121. package/dist/harness/usage.js +57 -0
  122. package/dist/harness/usage.js.map +1 -0
  123. package/dist/harness/util.d.ts +19 -0
  124. package/dist/harness/util.d.ts.map +1 -0
  125. package/dist/harness/util.js +58 -0
  126. package/dist/harness/util.js.map +1 -0
  127. package/dist/harness/view.d.ts +47 -0
  128. package/dist/harness/view.d.ts.map +1 -0
  129. package/dist/harness/view.js +206 -0
  130. package/dist/harness/view.js.map +1 -0
  131. package/dist/ids.d.ts +6 -0
  132. package/dist/ids.d.ts.map +1 -0
  133. package/dist/ids.js +9 -0
  134. package/dist/ids.js.map +1 -0
  135. package/dist/images/cloudflare.d.ts +4 -0
  136. package/dist/images/cloudflare.d.ts.map +1 -0
  137. package/dist/images/cloudflare.js +9 -0
  138. package/dist/images/cloudflare.js.map +1 -0
  139. package/dist/images/exif.d.ts +3 -0
  140. package/dist/images/exif.d.ts.map +1 -0
  141. package/dist/images/exif.js +71 -0
  142. package/dist/images/exif.js.map +1 -0
  143. package/dist/images/index.d.ts +18 -0
  144. package/dist/images/index.d.ts.map +1 -0
  145. package/dist/images/index.js +125 -0
  146. package/dist/images/index.js.map +1 -0
  147. package/dist/images/node-worker.d.ts +17 -0
  148. package/dist/images/node-worker.d.ts.map +1 -0
  149. package/dist/images/node-worker.js +16 -0
  150. package/dist/images/node-worker.js.map +1 -0
  151. package/dist/images/node.d.ts +21 -0
  152. package/dist/images/node.d.ts.map +1 -0
  153. package/dist/images/node.js +121 -0
  154. package/dist/images/node.js.map +1 -0
  155. package/dist/index.d.ts +24 -0
  156. package/dist/index.d.ts.map +1 -0
  157. package/dist/index.js +20 -0
  158. package/dist/index.js.map +1 -0
  159. package/dist/session/forks.d.ts +10 -0
  160. package/dist/session/forks.d.ts.map +1 -0
  161. package/dist/session/forks.js +42 -0
  162. package/dist/session/forks.js.map +1 -0
  163. package/dist/session/observation.d.ts +42 -0
  164. package/dist/session/observation.d.ts.map +1 -0
  165. package/dist/session/observation.js +281 -0
  166. package/dist/session/observation.js.map +1 -0
  167. package/dist/session/session.d.ts +133 -0
  168. package/dist/session/session.d.ts.map +1 -0
  169. package/dist/session/session.js +811 -0
  170. package/dist/session/session.js.map +1 -0
  171. package/dist/session/transaction.d.ts +119 -0
  172. package/dist/session/transaction.d.ts.map +1 -0
  173. package/dist/session/transaction.js +795 -0
  174. package/dist/session/transaction.js.map +1 -0
  175. package/dist/storage/jsonl/index.d.ts +2 -0
  176. package/dist/storage/jsonl/index.d.ts.map +1 -0
  177. package/dist/storage/jsonl/index.js +2 -0
  178. package/dist/storage/jsonl/index.js.map +1 -0
  179. package/dist/storage/jsonl/node.d.ts +6 -0
  180. package/dist/storage/jsonl/node.d.ts.map +1 -0
  181. package/dist/storage/jsonl/node.js +8 -0
  182. package/dist/storage/jsonl/node.js.map +1 -0
  183. package/dist/storage/jsonl/storage.d.ts +143 -0
  184. package/dist/storage/jsonl/storage.d.ts.map +1 -0
  185. package/dist/storage/jsonl/storage.js +682 -0
  186. package/dist/storage/jsonl/storage.js.map +1 -0
  187. package/dist/storage/memory.d.ts +62 -0
  188. package/dist/storage/memory.d.ts.map +1 -0
  189. package/dist/storage/memory.js +733 -0
  190. package/dist/storage/memory.js.map +1 -0
  191. package/dist/storage/scan.d.ts +18 -0
  192. package/dist/storage/scan.d.ts.map +1 -0
  193. package/dist/storage/scan.js +29 -0
  194. package/dist/storage/scan.js.map +1 -0
  195. package/dist/storage/sqlite/cloudflare.d.ts +46 -0
  196. package/dist/storage/sqlite/cloudflare.d.ts.map +1 -0
  197. package/dist/storage/sqlite/cloudflare.js +106 -0
  198. package/dist/storage/sqlite/cloudflare.js.map +1 -0
  199. package/dist/storage/sqlite/database.d.ts +37 -0
  200. package/dist/storage/sqlite/database.d.ts.map +1 -0
  201. package/dist/storage/sqlite/database.js +2 -0
  202. package/dist/storage/sqlite/database.js.map +1 -0
  203. package/dist/storage/sqlite/index.d.ts +4 -0
  204. package/dist/storage/sqlite/index.d.ts.map +1 -0
  205. package/dist/storage/sqlite/index.js +3 -0
  206. package/dist/storage/sqlite/index.js.map +1 -0
  207. package/dist/storage/sqlite/migrations.d.ts +11 -0
  208. package/dist/storage/sqlite/migrations.d.ts.map +1 -0
  209. package/dist/storage/sqlite/migrations.js +111 -0
  210. package/dist/storage/sqlite/migrations.js.map +1 -0
  211. package/dist/storage/sqlite/node.d.ts +41 -0
  212. package/dist/storage/sqlite/node.d.ts.map +1 -0
  213. package/dist/storage/sqlite/node.js +179 -0
  214. package/dist/storage/sqlite/node.js.map +1 -0
  215. package/dist/storage/sqlite/storage.d.ts +65 -0
  216. package/dist/storage/sqlite/storage.d.ts.map +1 -0
  217. package/dist/storage/sqlite/storage.js +670 -0
  218. package/dist/storage/sqlite/storage.js.map +1 -0
  219. package/dist/tasks.d.ts +6 -0
  220. package/dist/tasks.d.ts.map +1 -0
  221. package/dist/tasks.js +5 -0
  222. package/dist/tasks.js.map +1 -0
  223. package/dist/testing/assertions.d.ts +5 -0
  224. package/dist/testing/assertions.d.ts.map +1 -0
  225. package/dist/testing/assertions.js +25 -0
  226. package/dist/testing/assertions.js.map +1 -0
  227. package/dist/testing/env-conformance.d.ts +7 -0
  228. package/dist/testing/env-conformance.d.ts.map +1 -0
  229. package/dist/testing/env-conformance.js +431 -0
  230. package/dist/testing/env-conformance.js.map +1 -0
  231. package/dist/testing/index.d.ts +7 -0
  232. package/dist/testing/index.d.ts.map +1 -0
  233. package/dist/testing/index.js +6 -0
  234. package/dist/testing/index.js.map +1 -0
  235. package/dist/testing/runner.d.ts +12 -0
  236. package/dist/testing/runner.d.ts.map +1 -0
  237. package/dist/testing/runner.js +23 -0
  238. package/dist/testing/runner.js.map +1 -0
  239. package/dist/testing/storage-benchmark.d.ts +41 -0
  240. package/dist/testing/storage-benchmark.d.ts.map +1 -0
  241. package/dist/testing/storage-benchmark.js +350 -0
  242. package/dist/testing/storage-benchmark.js.map +1 -0
  243. package/dist/testing/storage-conformance.d.ts +4 -0
  244. package/dist/testing/storage-conformance.d.ts.map +1 -0
  245. package/dist/testing/storage-conformance.js +1213 -0
  246. package/dist/testing/storage-conformance.js.map +1 -0
  247. package/dist/testing/types.d.ts +37 -0
  248. package/dist/testing/types.d.ts.map +1 -0
  249. package/dist/testing/types.js +2 -0
  250. package/dist/testing/types.js.map +1 -0
  251. package/dist/tools/bash.d.ts +55 -0
  252. package/dist/tools/bash.d.ts.map +1 -0
  253. package/dist/tools/bash.js +138 -0
  254. package/dist/tools/bash.js.map +1 -0
  255. package/dist/tools/edit-diff.d.ts +89 -0
  256. package/dist/tools/edit-diff.d.ts.map +1 -0
  257. package/dist/tools/edit-diff.js +386 -0
  258. package/dist/tools/edit-diff.js.map +1 -0
  259. package/dist/tools/edit.d.ts +18 -0
  260. package/dist/tools/edit.d.ts.map +1 -0
  261. package/dist/tools/edit.js +117 -0
  262. package/dist/tools/edit.js.map +1 -0
  263. package/dist/tools/env.d.ts +5 -0
  264. package/dist/tools/env.d.ts.map +1 -0
  265. package/dist/tools/env.js +7 -0
  266. package/dist/tools/env.js.map +1 -0
  267. package/dist/tools/file-mutation-queue.d.ts +9 -0
  268. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  269. package/dist/tools/file-mutation-queue.js +52 -0
  270. package/dist/tools/file-mutation-queue.js.map +1 -0
  271. package/dist/tools/image-processor.d.ts +47 -0
  272. package/dist/tools/image-processor.d.ts.map +1 -0
  273. package/dist/tools/image-processor.js +30 -0
  274. package/dist/tools/image-processor.js.map +1 -0
  275. package/dist/tools/image.d.ts +20 -0
  276. package/dist/tools/image.d.ts.map +1 -0
  277. package/dist/tools/image.js +178 -0
  278. package/dist/tools/image.js.map +1 -0
  279. package/dist/tools/index.d.ts +46 -0
  280. package/dist/tools/index.d.ts.map +1 -0
  281. package/dist/tools/index.js +23 -0
  282. package/dist/tools/index.js.map +1 -0
  283. package/dist/tools/path-utils.d.ts +5 -0
  284. package/dist/tools/path-utils.d.ts.map +1 -0
  285. package/dist/tools/path-utils.js +26 -0
  286. package/dist/tools/path-utils.js.map +1 -0
  287. package/dist/tools/read.d.ts +25 -0
  288. package/dist/tools/read.d.ts.map +1 -0
  289. package/dist/tools/read.js +235 -0
  290. package/dist/tools/read.js.map +1 -0
  291. package/dist/tools/write.d.ts +10 -0
  292. package/dist/tools/write.d.ts.map +1 -0
  293. package/dist/tools/write.js +31 -0
  294. package/dist/tools/write.js.map +1 -0
  295. package/dist/truncate.d.ts +64 -0
  296. package/dist/truncate.d.ts.map +1 -0
  297. package/dist/truncate.js +154 -0
  298. package/dist/truncate.js.map +1 -0
  299. package/dist/types.d.ts +932 -0
  300. package/dist/types.d.ts.map +1 -0
  301. package/dist/types.js +3 -0
  302. package/dist/types.js.map +1 -0
  303. package/package.json +140 -5
@@ -0,0 +1,736 @@
1
+ import { validateToolArguments } from "@ny4n/ai/utils/validation";
2
+ import { copyJson } from "@ny4n/chord";
3
+ import { awaitWithContext } from "@ny4n/chord/context";
4
+ import { overlap } from "@ny4n/chord/delta";
5
+ import { Compile } from "typebox/compile";
6
+ import { defineDocFamily } from "../documents.js";
7
+ import { AssistantEntry, ToolResultEntry } from "../entries.js";
8
+ import { errorMessage } from "../errors.js";
9
+ import { defineTask } from "../tasks.js";
10
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, utf8ByteLength } from "../truncate.js";
11
+ import { assignJson } from "./json.js";
12
+ import { clearProgress, finishSlot, LiveDoc, removeNestedSlots, toolSlot, } from "./live.js";
13
+ import { boundOutput, OutputBuffer, PROGRESS_BYTES_PER_SECOND, Progress } from "./output.js";
14
+ import { recordUsage } from "./usage.js";
15
+ /**
16
+ * Memo of a tool task that made a nested call, in any attempt, committed before the first admission of each attempt:
17
+ * only then can `nyan.live` hold slots below it, which settling removes. A crash between the two only costs a scan.
18
+ */
19
+ const MADE_NESTED_CALLS = "nyan.tool.madeNestedCalls";
20
+ /**
21
+ * The nested calls of a tool call, a task-scoped family of the calling task keyed by the nested call's key: so a rerun
22
+ * finds the calls it already made, and the caller reads each result. Created with the nested task; the nested call's
23
+ * terminal commit adds its result. One document per call, so no commit rewrites the others. Retires with the caller.
24
+ */
25
+ export const NestedCallDoc = defineDocFamily({
26
+ kind: "nyan.tool.nested-call",
27
+ version: 1,
28
+ scope: "task",
29
+ family: true,
30
+ initial: () => ({}),
31
+ });
32
+ /**
33
+ * Built-in tool task: resolves the called tool among its phase agent's tools, validates, runs `beforeTool`, records intent,
34
+ * executes, runs `afterTool`, and settles the result, all in one `call` handler so nothing separates resolution from
35
+ * settlement. `execute` is reached only by recovery and applies the replay rule. A model-issued call settles by
36
+ * appending its result entry; a nested call by writing its result to its caller's `NestedCallDoc`.
37
+ */
38
+ export const ToolTask = defineTask({
39
+ name: "nyan.tool",
40
+ version: 2,
41
+ initial: () => ({ phase: "call" }),
42
+ // Version 1 had only model-issued calls.
43
+ migrate: (input, checkpoint) => ({
44
+ input: { kind: "model", ...input },
45
+ checkpoint: checkpoint,
46
+ }),
47
+ phases: {
48
+ call: async (task, runtime, context) => {
49
+ const call = await readCall(runtime, task.input, context);
50
+ const tool = await resolveTool(runtime, task.input, call.name, context);
51
+ if (tool === undefined) {
52
+ const error = harnessError("tool_unavailable", `Tool ${call.name} is not available`);
53
+ return settle(runtime, task.input, call, COMPLETED, () => error, context);
54
+ }
55
+ const prepared = prepare(tool, call.arguments);
56
+ const checked = "error" in prepared ? prepared : validate(tool, call, prepared.args);
57
+ if ("error" in checked)
58
+ return settle(runtime, task.input, call, COMPLETED, () => invalid(checked.error), context);
59
+ let args = checked.args;
60
+ let block;
61
+ await runtime.hooks.each("beforeTool", async (hook) => {
62
+ if (block !== undefined)
63
+ return;
64
+ try {
65
+ const decision = await hook({ ...call, arguments: args }, runtime, context);
66
+ if (decision?.block !== undefined)
67
+ block = decision.block;
68
+ else if (decision?.arguments !== undefined)
69
+ args = decision.arguments;
70
+ }
71
+ catch (error) {
72
+ if (runtime.signal.aborted)
73
+ throw error;
74
+ block = errorMessage(error);
75
+ }
76
+ });
77
+ if (block !== undefined) {
78
+ const blocked = harnessError("blocked", `Tool call blocked: ${block}`);
79
+ return settle(runtime, task.input, call, COMPLETED, () => blocked, context);
80
+ }
81
+ const validated = validate(tool, call, args);
82
+ if ("error" in validated)
83
+ return settle(runtime, task.input, call, COMPLETED, () => invalid(validated.error), context);
84
+ // Strict JSON: repair may set optional properties to undefined, which the slot and checkpoint cannot hold.
85
+ const final = copyJson(validated.args, { omitUndefinedProperties: true });
86
+ await runtime.commit(async (tx) => {
87
+ const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
88
+ if (slot !== undefined)
89
+ slot.status = "running";
90
+ // A nested slot shows the arguments the call runs with, which repair, hooks, and coercion may have changed.
91
+ // Compared with the input, which the slot got at admission: reading the draft would track every leaf.
92
+ const changed = task.input.kind === "nested" && !jsonEqual(task.input.call.arguments, final);
93
+ if (changed && slot !== undefined && "parentTaskId" in slot)
94
+ slot.arguments = final;
95
+ const intent = { phase: "execute", arguments: final, replay: tool.replay ?? "unsafe" };
96
+ return { status: "running", checkpoint: intent };
97
+ }, context);
98
+ await run(runtime, task.input, call, tool, final, context);
99
+ },
100
+ /** Recovery after intent: rerun only when the stored and the current policy both say `safe`. */
101
+ execute: async (task, runtime, context) => {
102
+ const { arguments: args, replay } = task.state.checkpoint;
103
+ const call = await readCall(runtime, task.input, context);
104
+ const tool = await resolveTool(runtime, task.input, call.name, context);
105
+ if (replay === "safe" && tool?.replay === "safe") {
106
+ // The rerun reports from scratch; clear what the interrupted attempt published.
107
+ await runtime.commit(async (tx) => {
108
+ const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
109
+ if (slot !== undefined)
110
+ clearProgress(slot);
111
+ return undefined;
112
+ }, context);
113
+ return run(runtime, task.input, call, tool, args, context);
114
+ }
115
+ const message = `Tool ${call.name} was interrupted and may have partially run`;
116
+ // `failed` records cancellation intent, so the call's owned conversations, left unsupervised, are aborted.
117
+ const ending = { status: "failed", message };
118
+ await settle(runtime, task.input, call, ending, (slot) => fromSlot(slot, "interrupted", message), context);
119
+ },
120
+ },
121
+ abort: async (task, runtime, context) => {
122
+ const call = await readCall(runtime, task.input, context);
123
+ // Abandoned after a restart (`TaskOptions.abandonOnRestart`): say whether the call may have run.
124
+ const [code, message] = task.abortReason !== "restart"
125
+ ? ["aborted", `Tool ${call.name} was aborted`]
126
+ : task.state.checkpoint.phase === "execute"
127
+ ? ["interrupted", `Tool ${call.name} was interrupted by a restart and may have partially run`]
128
+ : ["abandoned", `Tool ${call.name} was not started: its caller ended with a restart`];
129
+ const ending = { status: "aborted" };
130
+ await settle(runtime, task.input, call, ending, (slot) => fromSlot(slot, code, message), context);
131
+ },
132
+ });
133
+ /** The called tool: among the tools the model is offered for a model-issued call, else among those tools may call. */
134
+ async function resolveTool(runtime, input, name, context) {
135
+ const agent = await runtime.agent(context);
136
+ return (input.kind === "model" ? agent.tools : agent.callable).find((tool) => tool.name === name);
137
+ }
138
+ /** The call: from the input for a nested call, which hooks see with its parent, else from the assistant entry. */
139
+ async function readCall(runtime, input, context) {
140
+ if (input.kind === "nested")
141
+ return { ...input.call, parent: { taskId: input.parent, callId: input.parentCallId } };
142
+ const entry = await runtime.entry(AssistantEntry, input.assistant, context);
143
+ const message = entry?.model?.[0];
144
+ const call = message?.role === "assistant"
145
+ ? message.content.find((content) => content.type === "toolCall" && content.id === input.callId)
146
+ : undefined;
147
+ if (call === undefined)
148
+ throw new Error(`Entry ${input.assistant} has no tool call ${input.callId}`);
149
+ return call;
150
+ }
151
+ /** The call's arguments as repaired by the tool; a throwing repair makes them invalid. */
152
+ function prepare(tool, args) {
153
+ if (tool.prepareArguments === undefined)
154
+ return { args };
155
+ try {
156
+ return { args: tool.prepareArguments(args) };
157
+ }
158
+ catch (error) {
159
+ return { error: errorMessage(error) };
160
+ }
161
+ }
162
+ /** Arguments validated and coerced against the implementation's schema. */
163
+ function validate(tool, call, args) {
164
+ try {
165
+ return { args: validateToolArguments(tool, { ...call, arguments: args }) };
166
+ }
167
+ catch (error) {
168
+ return { error: errorMessage(error) };
169
+ }
170
+ }
171
+ function invalid(message) {
172
+ return harnessError("invalid_arguments", message);
173
+ }
174
+ /** Execute with the resolved implementation, then settle its result. */
175
+ async function run(runtime, input, call, tool, args, context) {
176
+ const limits = {
177
+ maxBytes: tool.outputLimits?.maxBytes ?? DEFAULT_MAX_BYTES,
178
+ maxLines: tool.outputLimits?.maxLines ?? DEFAULT_MAX_LINES,
179
+ retain: tool.outputLimits?.retain ?? "head",
180
+ };
181
+ const reported = { output: new OutputBuffer(limits), limits, diagnostics: [], details: undefined };
182
+ const streams = input.kind === "model" || input.progress !== false;
183
+ const progress = publishProgress(runtime, reported, streams, context);
184
+ /** Nested call admissions this invocation started; cleanup waits for them, so it sees every nested call. */
185
+ const admissions = [];
186
+ // A caller that can rerun reattaches to its nested calls and child tasks after a restart; any other caller never
187
+ // resumes, so they are abandoned (`TaskOptions.abandonOnRestart`).
188
+ const resumes = tool.replay === "safe";
189
+ /** Default keys: the order of this invocation's calls, so a rerun that calls in the same order reattaches. */
190
+ let sequence = 0;
191
+ /** This attempt's commit of the `MADE_NESTED_CALLS` memo. */
192
+ let madeNestedMemo;
193
+ let ended = false;
194
+ const assertLive = () => {
195
+ if (ended)
196
+ throw new Error(`Tool call ${call.id} has settled`);
197
+ };
198
+ const api = {
199
+ taskId: runtime.taskId,
200
+ conversationId: runtime.conversationId,
201
+ callId: call.id,
202
+ registry: runtime.registry,
203
+ agent: runtime.agent,
204
+ models: runtime.models,
205
+ output: (chunk, skipped) => {
206
+ assertLive();
207
+ if (reported.output.push(chunk, skipped))
208
+ progress.mark();
209
+ },
210
+ outputWindow: limits.retain === "tail"
211
+ ? {
212
+ maxBytes: limits.maxBytes,
213
+ maxLines: limits.maxLines,
214
+ minIntervalMs: runtime.settings.progress.outputIntervalMs,
215
+ bytesPerSecond: PROGRESS_BYTES_PER_SECOND,
216
+ }
217
+ : undefined,
218
+ retainedOutput: () => {
219
+ assertLive();
220
+ const retained = reported.output.snapshot();
221
+ return { text: retained.text, truncated: retained.droppedBytes > 0 };
222
+ },
223
+ diagnostic: (diagnostic) => {
224
+ assertLive();
225
+ reported.diagnostics.push(copyJson(diagnostic, { omitUndefinedProperties: true }));
226
+ progress.mark();
227
+ },
228
+ details: async (value, detailsContext) => {
229
+ assertLive();
230
+ detailsContext.abortSignal?.throwIfAborted();
231
+ reported.details = copyJson(value, { omitUndefinedProperties: true });
232
+ const committed = progress.markAndWait();
233
+ // Cancelling the wait leaves the update in place; the commit's own outcome stays observed.
234
+ committed.catch(() => { });
235
+ return awaitWithContext(committed, detailsContext);
236
+ },
237
+ commit: async (change, commitContext) => {
238
+ let result;
239
+ await runtime.commit(async (tx) => {
240
+ result = await change(tx);
241
+ return undefined;
242
+ }, commitContext);
243
+ return result;
244
+ },
245
+ memo: runtime.memo,
246
+ createTask: async (task, input, options, taskContext) => {
247
+ const child = options.ownership.kind === "task" && !resumes ? { abandonOnRestart: true, ...options } : options;
248
+ let id;
249
+ await runtime.commit(async (tx) => {
250
+ id = await tx.createTask(task, input, child);
251
+ return undefined;
252
+ }, taskContext);
253
+ return id;
254
+ },
255
+ getTask: runtime.getTask,
256
+ waitForTask: runtime.waitForTask,
257
+ conversation: runtime.conversation,
258
+ snapshot: runtime.snapshot,
259
+ snapshotAsOf: runtime.snapshotAsOf,
260
+ watchDoc: runtime.watchDoc,
261
+ executeTool: async (name, toolArgs, callContext, options = {}) => {
262
+ assertLive();
263
+ const key = options.key ?? String(++sequence);
264
+ if (options.key !== undefined)
265
+ checkKey(options.key);
266
+ // The memo first, once per attempt; a failed one is tried again by the next call.
267
+ madeNestedMemo ??= runtime.memo(MADE_NESTED_CALLS, true, callContext).catch((error) => {
268
+ madeNestedMemo = undefined;
269
+ throw error;
270
+ });
271
+ // Registered now, so cleanup waits for it.
272
+ const admission = madeNestedMemo.then(() => startNestedCall(runtime, call.id, name, toolArgs, { key, progress: options.progress, abandonOnRestart: !resumes }, callContext));
273
+ admissions.push(admission);
274
+ const id = await admission;
275
+ const settled = await runtime.waitForTask(id, callContext);
276
+ const stored = (await runtime.snapshot(NestedCallDoc, runtime.taskId, key, callContext))?.result;
277
+ // A copy the tool may change; a nested call the scheduler faulted or orphaned stored none.
278
+ return stored === undefined
279
+ ? fallbackResult(id, name, settled.state.outcome)
280
+ : copyJson(stored);
281
+ },
282
+ };
283
+ let result;
284
+ let ending = COMPLETED;
285
+ // Execution time of this attempt; a rerun after recovery measures only itself.
286
+ let durationMs;
287
+ try {
288
+ // Built for this call, so a rerun after recovery gets the conversation's environment at that time.
289
+ const env = await runtime.env(context);
290
+ const startedAt = performance.now();
291
+ try {
292
+ result = await tool.execute(args, { ...api, env }, context);
293
+ }
294
+ finally {
295
+ durationMs = Math.round(performance.now() - startedAt);
296
+ }
297
+ }
298
+ catch (error) {
299
+ if (runtime.signal.aborted) {
300
+ ended = true;
301
+ for (const waiter of await progress.stop())
302
+ waiter.reject(error);
303
+ throw error;
304
+ }
305
+ result = { isError: true, diagnostics: [toolDiagnostic("tool_error", errorMessage(error))] };
306
+ // A throw, from `execute()` or from building the environment, ends the task `failed`, which cancels what the call
307
+ // owned; it no longer supervises it. The error text is already in the result.
308
+ ending = { status: "failed", message: `Tool ${call.name} threw` };
309
+ }
310
+ ended = true;
311
+ // No admission starts after `ended`; let those underway commit, so settlement aborts and lists their calls.
312
+ await Promise.allSettled(admissions);
313
+ reported.output.end();
314
+ // Details still waiting for a progress commit settle with the terminal commit, the final flush.
315
+ const pending = await progress.stop();
316
+ try {
317
+ const settled = await finalResult(runtime, input, call, tool, result, reported, context);
318
+ await settle(runtime, input, call, ending, () => settled, context, durationMs);
319
+ }
320
+ catch (error) {
321
+ for (const waiter of pending)
322
+ waiter.reject(error);
323
+ throw error;
324
+ }
325
+ for (const waiter of pending)
326
+ waiter.resolve();
327
+ }
328
+ /**
329
+ * Throttled commits of what the tool reported into its slot, in `nyan.live.tools` or `nyan.live.nestedTools`, each writing
330
+ * only what changed since the last one; none when `enabled` is false.
331
+ */
332
+ function publishProgress(runtime, reported, enabled, context) {
333
+ let written = { text: "", details: undefined, diagnostics: 0 };
334
+ return new Progress(async () => {
335
+ if (!enabled)
336
+ return 0;
337
+ // Capture everything synchronously: the tool keeps reporting while the commit is in flight.
338
+ const snapshot = reported.output.snapshot();
339
+ const current = { text: snapshot.text, details: reported.details, diagnostics: reported.diagnostics.length };
340
+ const added = reported.diagnostics.slice(written.diagnostics, current.diagnostics);
341
+ const detailsChanged = current.details !== written.details;
342
+ // What the commit writes, as Chord diffs the string: an append, a trim plus an append of what follows the shared
343
+ // part, or the whole window when its bounded overlap search finds nothing.
344
+ let bytes = 0;
345
+ if (snapshot.text !== written.text) {
346
+ const shared = snapshot.text.startsWith(written.text)
347
+ ? written.text.length
348
+ : overlap(written.text, snapshot.text, 65_536);
349
+ bytes += utf8ByteLength(snapshot.text.slice(shared));
350
+ }
351
+ if (detailsChanged)
352
+ bytes += utf8ByteLength(JSON.stringify(current.details ?? null));
353
+ if (added.length > 0)
354
+ bytes += utf8ByteLength(JSON.stringify(added));
355
+ await runtime.commit(async (tx) => {
356
+ const slot = toolSlot(await tx.doc(LiveDoc, runtime.conversationId), runtime.taskId);
357
+ if (slot === undefined)
358
+ return undefined;
359
+ // REMINDER: assign `output` as one string field. Chord then diffs it into an append, or a trim plus an
360
+ // append for a sliding tail; replacing the slot object would record the whole window on every commit.
361
+ if ((slot.output ?? "") !== snapshot.text)
362
+ slot.output = snapshot.text;
363
+ if (snapshot.droppedBytes > 0)
364
+ slot.droppedBytes = snapshot.droppedBytes;
365
+ if (snapshot.droppedLines > 0)
366
+ slot.droppedLines = snapshot.droppedLines;
367
+ // Diff details leaf by leaf and append new diagnostics, so each commit writes only what changed.
368
+ if (detailsChanged && current.details !== undefined) {
369
+ assignJson(slot, "details", current.details);
370
+ }
371
+ if (added.length > 0) {
372
+ if (slot.diagnostics === undefined)
373
+ slot.diagnostics = [];
374
+ for (const diagnostic of added)
375
+ slot.diagnostics.push(diagnostic);
376
+ }
377
+ return undefined;
378
+ }, context);
379
+ written = current;
380
+ return bytes;
381
+ }, (error) => {
382
+ // Rejections after an abort mark or close are expected; the committed state stays consistent.
383
+ if (!runtime.signal.aborted)
384
+ runtime.report(error);
385
+ }, runtime.settings.progress.outputIntervalMs);
386
+ }
387
+ /**
388
+ * The settled result: the tool's result with the retained output and last details as fallbacks, its diagnostics after
389
+ * those reported through the api, `afterTool` applied, explicit text bounded, and `structuredOutput` checked against the
390
+ * tool's schema, with the Harness's diagnostics last. A nested call whose `structuredOutput` breaks the contract gets an
391
+ * error result without it; a model-issued call only loses it, and the break is reported to the host.
392
+ */
393
+ async function finalResult(runtime, input, call, tool, result, reported, context) {
394
+ const harness = [];
395
+ const retained = result.output === undefined ? reported.output.snapshot() : undefined;
396
+ const output = retained === undefined ? result.output : retained.text === "" ? [] : [{ type: "text", text: retained.text }];
397
+ let final = {
398
+ ...result,
399
+ output,
400
+ details: result.details === undefined ? reported.details : result.details,
401
+ diagnostics: [...reported.diagnostics, ...(result.diagnostics ?? [])],
402
+ };
403
+ await runtime.hooks.each("afterTool", async (hook) => {
404
+ final = (await hook(call, final, runtime, context)) ?? final;
405
+ });
406
+ // The retained output's truncation applies only while afterTool kept that output.
407
+ if (retained !== undefined && retained.droppedBytes > 0 && final.output === output) {
408
+ harness.push(truncated(retained, reported.limits.retain));
409
+ }
410
+ const bounded = boundContent(final.output ?? [], reported.limits);
411
+ if (bounded.droppedBytes > 0)
412
+ harness.push(truncated(bounded, reported.limits.retain));
413
+ const broken = structuredOutputError(tool, final);
414
+ if (broken !== undefined) {
415
+ const { structuredOutput: _dropped, ...rest } = final;
416
+ if (input.kind === "nested") {
417
+ final = { ...rest, isError: true };
418
+ harness.push(toolDiagnostic("invalid_structured_output", broken));
419
+ }
420
+ else {
421
+ // The model never sees structured output, so its call stands; the host learns of the broken tool.
422
+ final = rest;
423
+ runtime.report(new Error(broken));
424
+ }
425
+ }
426
+ // Without a schema, programs get the output itself; the model reads only `output`, so model-issued calls skip it.
427
+ if (input.kind === "nested" && tool.structuredOutputSchema === undefined) {
428
+ final = { ...final, structuredOutput: outputValue(bounded.content) };
429
+ }
430
+ return { ...final, output: bounded.content, diagnostics: [...(final.diagnostics ?? []), ...harness] };
431
+ }
432
+ const structuredValidators = new WeakMap();
433
+ /** Why a result's `structuredOutput` breaks the tool's contract, or `undefined` when it keeps it. */
434
+ function structuredOutputError(tool, result) {
435
+ const schema = tool.structuredOutputSchema;
436
+ const value = result.structuredOutput;
437
+ if (schema === undefined) {
438
+ return value === undefined
439
+ ? undefined
440
+ : `Tool ${tool.name} returned structuredOutput but declares no structuredOutputSchema`;
441
+ }
442
+ if (value === undefined) {
443
+ return result.isError === true ? undefined : `Tool ${tool.name} returned no structuredOutput`;
444
+ }
445
+ let validator = structuredValidators.get(schema);
446
+ if (validator === undefined) {
447
+ validator = Compile(schema);
448
+ structuredValidators.set(schema, validator);
449
+ }
450
+ if (validator.Check(value))
451
+ return undefined;
452
+ const [first] = validator.Errors(value);
453
+ const path = first?.instancePath.replace(/^\//, "").replace(/\//g, ".") || "root";
454
+ return `Tool ${tool.name} returned structuredOutput that does not match its schema: ${path}: ${first?.message ?? "invalid"}`;
455
+ }
456
+ /**
457
+ * Commit the tool's terminal state and mark its slot done. A model-issued call appends its result entry and ends with
458
+ * the entry ID; a nested call writes its result to the caller's `NestedCallDoc`, records its usage, and ends with a
459
+ * small receipt. Nested calls this call left running are aborted first, so they report into their slots before the
460
+ * slots below the call leave `nyan.live`. `build` receives the slot so interruption and abort can report the durable
461
+ * partial output.
462
+ */
463
+ async function settle(runtime, input, call, ending, build, context, durationMs) {
464
+ // A crash in between leaves the call in `execute`: a safe rerun reattaches to the calls, an interruption ends here.
465
+ // Every live nested call the task owns, also those an earlier attempt made and this one never called again.
466
+ const nestedIds = (await runtime.ownedTasks(context))
467
+ .filter((record) => record.kind === ToolTask.definition.name && record.input.kind === "nested")
468
+ .map((record) => record.id);
469
+ // Read before the settling commit: no admission starts after `ended`, and those underway were awaited.
470
+ const madeNested = (await runtime.memo(MADE_NESTED_CALLS, context)) === true;
471
+ // Mark them all before waiting for any: one may run until a sibling is cancelled.
472
+ await Promise.all(nestedIds.map((id) => runtime.abortOwned(id, context)));
473
+ await runtime.commit(async (tx) => {
474
+ const conversationId = runtime.conversationId;
475
+ const live = await tx.doc(LiveDoc, conversationId);
476
+ const slot = toolSlot(live, runtime.taskId);
477
+ const result = build(slot);
478
+ let settled;
479
+ if (input.kind === "nested") {
480
+ const nested = nestedResult(runtime.taskId, result, durationMs);
481
+ if (nested.usage !== undefined)
482
+ await recordUsage(tx, conversationId, "tools", call.name, nested.usage);
483
+ // The caller is never terminal before its owned nested calls, so its document is open; another task in it is a
484
+ // bug, not a race.
485
+ const state = await tx.doc(NestedCallDoc, input.parent, input.key, null);
486
+ if (state.taskId !== runtime.taskId)
487
+ throw new Error(`Nested call ${call.id} is not in its caller's index`);
488
+ state.result = nested;
489
+ if (slot !== undefined && "parentTaskId" in slot) {
490
+ finishSlot(slot);
491
+ slot.summary = summaryOf(nested, result.output ?? []);
492
+ }
493
+ settled = { kind: "nested" };
494
+ }
495
+ else {
496
+ const timing = { timestamp: runtime.now(), durationMs };
497
+ const entry = await appendToolResult(tx, conversationId, call, result, timing);
498
+ if (slot !== undefined && !("parentTaskId" in slot)) {
499
+ finishSlot(slot);
500
+ slot.entry = entry.id;
501
+ }
502
+ // Tools build control objects freely; drop keys set to undefined so the task result is strict JSON.
503
+ const control = result.control === undefined || ending.status !== "completed"
504
+ ? {}
505
+ : { control: copyJson(result.control, { omitUndefinedProperties: true }) };
506
+ settled = { kind: "model", entryId: entry.id, ...control };
507
+ }
508
+ // A call that made no nested calls, in any attempt, has no slots below it; skipping the scan keeps many leaves
509
+ // linear. Committed before the read: no nested call is admitted after `ended`.
510
+ if (madeNested)
511
+ removeNestedSlots(live, runtime.taskId);
512
+ if (ending.status === "aborted")
513
+ return { status: "terminal", outcome: { status: "aborted", result: settled } };
514
+ if (ending.status === "failed") {
515
+ const error = { message: ending.message };
516
+ return { status: "terminal", outcome: { status: "failed", error, result: settled } };
517
+ }
518
+ return { status: "terminal", outcome: { status: "completed", result: settled } };
519
+ }, context);
520
+ }
521
+ /**
522
+ * A nested call's result, as stored and returned: strict JSON, without `output`, which only the model reads, and
523
+ * without `control`, which only model-issued calls apply.
524
+ */
525
+ function nestedResult(taskId, result, durationMs) {
526
+ const nested = {
527
+ taskId,
528
+ structuredOutput: result.structuredOutput,
529
+ isError: result.isError ?? false,
530
+ details: result.details,
531
+ diagnostics: result.diagnostics ?? [],
532
+ usage: result.usage,
533
+ durationMs,
534
+ };
535
+ return copyJson(nested, {
536
+ omitUndefinedProperties: true,
537
+ });
538
+ }
539
+ /**
540
+ * Create nested call `key` of the call `parentCallId` in one commit: its tool task, owned by the calling task, its slot,
541
+ * and its `NestedCallDoc`. A key already made returns that call's task when it names the same tool and arguments.
542
+ */
543
+ async function startNestedCall(runtime, parentCallId, name, args, options, context) {
544
+ const key = options.key;
545
+ const call = {
546
+ type: "toolCall",
547
+ id: `${parentCallId}/${key}`,
548
+ name,
549
+ arguments: copyJson(args, { omitUndefinedProperties: true }),
550
+ };
551
+ let id;
552
+ await runtime.commit(async (tx) => {
553
+ // Opened, or created empty for a key not made yet.
554
+ const state = await tx.doc(NestedCallDoc, runtime.taskId, key, null);
555
+ const existing = state.taskId;
556
+ if (existing !== undefined) {
557
+ const input = (await tx.task(existing))?.input;
558
+ const same = input?.kind === "nested" &&
559
+ input.call.name === name &&
560
+ jsonEqual(input.call.arguments, call.arguments);
561
+ if (!same)
562
+ throw new Error(`Nested call ${call.id} was already made with another tool or other arguments`);
563
+ id = existing;
564
+ return undefined;
565
+ }
566
+ const input = {
567
+ kind: "nested",
568
+ parent: runtime.taskId,
569
+ parentCallId,
570
+ key,
571
+ call,
572
+ ...(options.progress === false ? { progress: false } : {}),
573
+ };
574
+ const created = await tx.createTask(ToolTask, input, {
575
+ ownership: { kind: "task", taskId: runtime.taskId },
576
+ abandonOnRestart: options.abandonOnRestart,
577
+ });
578
+ state.taskId = created;
579
+ const live = await tx.doc(LiveDoc, runtime.conversationId);
580
+ live.nestedTools ??= [];
581
+ const slot = {
582
+ callId: call.id,
583
+ parentCallId,
584
+ parentTaskId: runtime.taskId,
585
+ name,
586
+ taskId: created,
587
+ arguments: call.arguments,
588
+ status: "pending",
589
+ };
590
+ live.nestedTools.push(slot);
591
+ id = created;
592
+ return undefined;
593
+ }, context);
594
+ return id;
595
+ }
596
+ /**
597
+ * What programs get of a tool's output when it declares no schema: one text item as its string, one image as itself,
598
+ * nothing as `""`, and anything else as the content list.
599
+ */
600
+ function outputValue(output) {
601
+ if (output.length === 0)
602
+ return "";
603
+ if (output.length > 1)
604
+ return output;
605
+ const [only] = output;
606
+ return only.type === "text" ? only.text : only;
607
+ }
608
+ /** What the caller gets for a nested call without a stored result: one the scheduler faulted or orphaned. */
609
+ function fallbackResult(taskId, name, outcome) {
610
+ const message = outcome.status === "faulted"
611
+ ? `Tool ${name} failed: ${outcome.error.message}`
612
+ : outcome.status === "orphaned"
613
+ ? `Tool ${name} could not resume: ${outcome.reason}`
614
+ : `Tool ${name} ended without a result`;
615
+ const error = harnessError(outcome.status, message);
616
+ return { taskId, isError: true, diagnostics: error.diagnostics ?? [] };
617
+ }
618
+ const COMPLETED = { status: "completed" };
619
+ /** An error result from the slot's durable partial output, details, and diagnostics. */
620
+ function fromSlot(slot, code, message) {
621
+ const diagnostics = [...(slot?.diagnostics ?? [])];
622
+ const droppedBytes = slot?.droppedBytes ?? 0;
623
+ if (droppedBytes > 0)
624
+ diagnostics.push(truncated({ droppedBytes, droppedLines: slot?.droppedLines ?? 0 }));
625
+ diagnostics.push(toolDiagnostic(code, message));
626
+ return {
627
+ output: slot?.output === undefined || slot.output === "" ? [] : [{ type: "text", text: slot.output }],
628
+ isError: true,
629
+ ...(slot?.details === undefined ? {} : { details: slot.details }),
630
+ diagnostics,
631
+ };
632
+ }
633
+ /** An error result the Harness writes itself: no content and one `error` diagnostic with `code`. */
634
+ export function harnessError(code, message) {
635
+ return { output: [], isError: true, diagnostics: [toolDiagnostic(code, message)] };
636
+ }
637
+ function toolDiagnostic(code, message) {
638
+ return { severity: "error", code, message };
639
+ }
640
+ /** The Harness's truncation diagnostic; `retain` is unknown when rebuilt from a slot after recovery. */
641
+ function truncated(dropped, retain) {
642
+ const kept = retain === undefined ? "" : ` to its ${retain === "head" ? "beginning" : "end"}`;
643
+ return {
644
+ severity: "warn",
645
+ code: "truncated",
646
+ message: `Output truncated${kept}: ${dropped.droppedLines} lines, ${dropped.droppedBytes} bytes dropped`,
647
+ };
648
+ }
649
+ /** Error text a nested call's summary keeps. */
650
+ const MAX_SUMMARY_ERROR_CHARS = 500;
651
+ /** How a nested call ended, for its slot: error text, from its diagnostics or else its `output`, bounded. */
652
+ function summaryOf(result, output) {
653
+ const error = result.isError ? errorTextOf(result, output).slice(0, MAX_SUMMARY_ERROR_CHARS) : "";
654
+ return {
655
+ isError: result.isError,
656
+ ...(result.durationMs === undefined ? {} : { durationMs: result.durationMs }),
657
+ ...(result.usage === undefined ? {} : { usage: result.usage }),
658
+ ...(error === "" ? {} : { error }),
659
+ };
660
+ }
661
+ /** The error messages of a nested result, else the text items of its output, joined with newlines. */
662
+ function errorTextOf(result, output) {
663
+ const errors = result.diagnostics.filter((diagnostic) => diagnostic.severity === "error");
664
+ if (errors.length > 0)
665
+ return errors.map((diagnostic) => diagnostic.message).join("\n");
666
+ return output.flatMap((item) => (item.type === "text" ? [item.text] : [])).join("\n");
667
+ }
668
+ /**
669
+ * Append a `nyan.tool-result` entry. The content ends with the rendered diagnostics, so the stored message is exactly
670
+ * what the model sees; `data` keeps the structured list. A result's usage is added to `nyan.usage` in the same commit.
671
+ */
672
+ export async function appendToolResult(tx, conversationId, call, result, meta) {
673
+ const { timestamp, durationMs } = meta;
674
+ const diagnostics = [...(result.diagnostics ?? [])];
675
+ const content = [...(result.output ?? [])];
676
+ if (diagnostics.length > 0)
677
+ content.push({ type: "text", text: renderDiagnostics(diagnostics) });
678
+ const message = {
679
+ role: "toolResult",
680
+ toolCallId: call.id,
681
+ toolName: call.name,
682
+ content,
683
+ ...(result.details === undefined ? {} : { details: result.details }),
684
+ ...(result.usage === undefined ? {} : { usage: result.usage }),
685
+ isError: result.isError ?? false,
686
+ ...(durationMs === undefined ? {} : { durationMs }),
687
+ timestamp,
688
+ };
689
+ if (result.usage !== undefined)
690
+ await recordUsage(tx, conversationId, "tools", call.name, result.usage);
691
+ return tx.appendEntry(ToolResultEntry, conversationId, { model: [message], data: { diagnostics } });
692
+ }
693
+ function renderDiagnostics(diagnostics) {
694
+ return `<harness>\n${diagnostics.map((diagnostic) => `[${diagnostic.severity}] ${diagnostic.message}`).join("\n")}\n</harness>`;
695
+ }
696
+ /**
697
+ * Bound the text of result content. When the joined text exceeds the limits, the text items are replaced by one bounded
698
+ * item at the position of the first (head) or last (tail) text item; other content is kept.
699
+ */
700
+ function boundContent(content, limits) {
701
+ const texts = content.filter((item) => item.type === "text");
702
+ const bounded = boundOutput(texts.map((item) => item.text).join(""), limits);
703
+ if (bounded.droppedBytes === 0)
704
+ return { content, droppedBytes: 0, droppedLines: 0 };
705
+ const keep = limits.retain === "head" ? texts[0] : texts.at(-1);
706
+ const result = [];
707
+ for (const item of content) {
708
+ if (item.type !== "text")
709
+ result.push(item);
710
+ else if (item === keep)
711
+ result.push({ ...item, text: bounded.text });
712
+ }
713
+ return { content: result, droppedBytes: bounded.droppedBytes, droppedLines: bounded.droppedLines };
714
+ }
715
+ /**
716
+ * Explicit nested call keys are path segments of the call ID, so IDs of different nested calls never collide, and are
717
+ * never plain positive integers, which default keys use. `__proto__` stays reserved, as it was when keys indexed a JSON object.
718
+ */
719
+ function checkKey(key) {
720
+ if (key === "" || key.includes("/") || key === "__proto__" || /^[1-9][0-9]*$/.test(key)) {
721
+ throw new Error(`Nested call key ${JSON.stringify(key)} must be non-empty, without "/", not "__proto__", and not a positive integer`);
722
+ }
723
+ }
724
+ /** Structural JSON equality; object key order does not matter. */
725
+ function jsonEqual(a, b) {
726
+ if (a === b)
727
+ return true;
728
+ if (typeof a !== "object" || typeof b !== "object" || a === null || b === null)
729
+ return false;
730
+ if (Array.isArray(a) || Array.isArray(b)) {
731
+ return (Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((item, i) => jsonEqual(item, b[i])));
732
+ }
733
+ const keys = Object.keys(a);
734
+ return (keys.length === Object.keys(b).length && keys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key])));
735
+ }
736
+ //# sourceMappingURL=tool.js.map