@earendil-works/pi-durable 0.99.0 → 0.99.2

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 (172) hide show
  1. package/README.md +435 -46
  2. package/dist/entries.d.ts +20 -2
  3. package/dist/entries.d.ts.map +1 -1
  4. package/dist/entries.js +14 -1
  5. package/dist/entries.js.map +1 -1
  6. package/dist/env/index.d.ts +13 -38
  7. package/dist/env/index.d.ts.map +1 -1
  8. package/dist/env/index.js +2 -0
  9. package/dist/env/index.js.map +1 -1
  10. package/dist/env/node.d.ts.map +1 -1
  11. package/dist/env/node.js +51 -58
  12. package/dist/env/node.js.map +1 -1
  13. package/dist/harness/compaction.d.ts +56 -0
  14. package/dist/harness/compaction.d.ts.map +1 -0
  15. package/dist/harness/compaction.js +385 -0
  16. package/dist/harness/compaction.js.map +1 -0
  17. package/dist/harness/config.d.ts +10 -1
  18. package/dist/harness/config.d.ts.map +1 -1
  19. package/dist/harness/config.js +6 -0
  20. package/dist/harness/config.js.map +1 -1
  21. package/dist/harness/context.d.ts +8 -0
  22. package/dist/harness/context.d.ts.map +1 -1
  23. package/dist/harness/context.js +32 -21
  24. package/dist/harness/context.js.map +1 -1
  25. package/dist/harness/events.d.ts +166 -0
  26. package/dist/harness/events.d.ts.map +1 -0
  27. package/dist/harness/events.js +319 -0
  28. package/dist/harness/events.js.map +1 -0
  29. package/dist/harness/generation.d.ts +37 -6
  30. package/dist/harness/generation.d.ts.map +1 -1
  31. package/dist/harness/generation.js +350 -72
  32. package/dist/harness/generation.js.map +1 -1
  33. package/dist/harness/harness.d.ts.map +1 -1
  34. package/dist/harness/harness.js +124 -3
  35. package/dist/harness/harness.js.map +1 -1
  36. package/dist/harness/inbox.d.ts +57 -0
  37. package/dist/harness/inbox.d.ts.map +1 -0
  38. package/dist/harness/inbox.js +99 -0
  39. package/dist/harness/inbox.js.map +1 -0
  40. package/dist/harness/json.d.ts +10 -0
  41. package/dist/harness/json.d.ts.map +1 -0
  42. package/dist/harness/json.js +33 -0
  43. package/dist/harness/json.js.map +1 -0
  44. package/dist/harness/live.d.ts +66 -7
  45. package/dist/harness/live.d.ts.map +1 -1
  46. package/dist/harness/live.js +71 -7
  47. package/dist/harness/live.js.map +1 -1
  48. package/dist/harness/output.d.ts +62 -0
  49. package/dist/harness/output.d.ts.map +1 -0
  50. package/dist/harness/output.js +261 -0
  51. package/dist/harness/output.js.map +1 -0
  52. package/dist/harness/prompt.d.ts +10 -5
  53. package/dist/harness/prompt.d.ts.map +1 -1
  54. package/dist/harness/prompt.js +66 -13
  55. package/dist/harness/prompt.js.map +1 -1
  56. package/dist/harness/registry.d.ts.map +1 -1
  57. package/dist/harness/registry.js +11 -2
  58. package/dist/harness/registry.js.map +1 -1
  59. package/dist/harness/scheduler.d.ts +37 -12
  60. package/dist/harness/scheduler.d.ts.map +1 -1
  61. package/dist/harness/scheduler.js +632 -72
  62. package/dist/harness/scheduler.js.map +1 -1
  63. package/dist/harness/submissions.d.ts +12 -7
  64. package/dist/harness/submissions.d.ts.map +1 -1
  65. package/dist/harness/submissions.js +72 -33
  66. package/dist/harness/submissions.js.map +1 -1
  67. package/dist/harness/tool.d.ts +36 -0
  68. package/dist/harness/tool.d.ts.map +1 -0
  69. package/dist/harness/tool.js +412 -0
  70. package/dist/harness/tool.js.map +1 -0
  71. package/dist/harness/types.d.ts +185 -11
  72. package/dist/harness/types.d.ts.map +1 -1
  73. package/dist/harness/types.js.map +1 -1
  74. package/dist/harness/usage.d.ts +18 -0
  75. package/dist/harness/usage.d.ts.map +1 -0
  76. package/dist/harness/usage.js +57 -0
  77. package/dist/harness/usage.js.map +1 -0
  78. package/dist/harness/view.d.ts +43 -0
  79. package/dist/harness/view.d.ts.map +1 -0
  80. package/dist/harness/view.js +194 -0
  81. package/dist/harness/view.js.map +1 -0
  82. package/dist/index.d.ts +10 -4
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +6 -1
  85. package/dist/index.js.map +1 -1
  86. package/dist/session/observation.d.ts +18 -11
  87. package/dist/session/observation.d.ts.map +1 -1
  88. package/dist/session/observation.js +24 -13
  89. package/dist/session/observation.js.map +1 -1
  90. package/dist/session/session.d.ts +10 -1
  91. package/dist/session/session.d.ts.map +1 -1
  92. package/dist/session/session.js +18 -4
  93. package/dist/session/session.js.map +1 -1
  94. package/dist/session/transaction.d.ts +10 -1
  95. package/dist/session/transaction.d.ts.map +1 -1
  96. package/dist/session/transaction.js +83 -27
  97. package/dist/session/transaction.js.map +1 -1
  98. package/dist/storage/jsonl/storage.d.ts +22 -2
  99. package/dist/storage/jsonl/storage.d.ts.map +1 -1
  100. package/dist/storage/jsonl/storage.js +1 -1
  101. package/dist/storage/jsonl/storage.js.map +1 -1
  102. package/dist/storage/memory.js +1 -1
  103. package/dist/storage/memory.js.map +1 -1
  104. package/dist/storage/sqlite/migrations.js +1 -1
  105. package/dist/storage/sqlite/migrations.js.map +1 -1
  106. package/dist/testing/storage-benchmark.d.ts.map +1 -1
  107. package/dist/testing/storage-benchmark.js +0 -1
  108. package/dist/testing/storage-benchmark.js.map +1 -1
  109. package/dist/testing/storage-conformance.d.ts.map +1 -1
  110. package/dist/testing/storage-conformance.js +34 -2
  111. package/dist/testing/storage-conformance.js.map +1 -1
  112. package/dist/tools/bash.d.ts +28 -0
  113. package/dist/tools/bash.d.ts.map +1 -0
  114. package/dist/tools/bash.js +69 -0
  115. package/dist/tools/bash.js.map +1 -0
  116. package/dist/tools/edit-diff.d.ts +89 -0
  117. package/dist/tools/edit-diff.d.ts.map +1 -0
  118. package/dist/tools/edit-diff.js +386 -0
  119. package/dist/tools/edit-diff.js.map +1 -0
  120. package/dist/tools/edit.d.ts +18 -0
  121. package/dist/tools/edit.d.ts.map +1 -0
  122. package/dist/tools/edit.js +116 -0
  123. package/dist/tools/edit.js.map +1 -0
  124. package/dist/tools/env.d.ts +5 -0
  125. package/dist/tools/env.d.ts.map +1 -0
  126. package/dist/tools/env.js +7 -0
  127. package/dist/tools/env.js.map +1 -0
  128. package/dist/tools/file-mutation-queue.d.ts +5 -0
  129. package/dist/tools/file-mutation-queue.d.ts.map +1 -0
  130. package/dist/tools/file-mutation-queue.js +46 -0
  131. package/dist/tools/file-mutation-queue.js.map +1 -0
  132. package/dist/tools/image.d.ts +2 -0
  133. package/dist/tools/image.d.ts.map +1 -0
  134. package/dist/tools/image.js +92 -0
  135. package/dist/tools/image.js.map +1 -0
  136. package/dist/tools/index.d.ts +5 -0
  137. package/dist/tools/index.d.ts.map +1 -0
  138. package/dist/tools/index.js +5 -0
  139. package/dist/tools/index.js.map +1 -0
  140. package/dist/tools/path-utils.d.ts +5 -0
  141. package/dist/tools/path-utils.d.ts.map +1 -0
  142. package/dist/tools/path-utils.js +26 -0
  143. package/dist/tools/path-utils.js.map +1 -0
  144. package/dist/tools/read.d.ts +17 -0
  145. package/dist/tools/read.d.ts.map +1 -0
  146. package/dist/tools/read.js +100 -0
  147. package/dist/tools/read.js.map +1 -0
  148. package/dist/tools/write.d.ts +10 -0
  149. package/dist/tools/write.d.ts.map +1 -0
  150. package/dist/tools/write.js +30 -0
  151. package/dist/tools/write.js.map +1 -0
  152. package/dist/{env/utils/truncate.d.ts → truncate.d.ts} +1 -17
  153. package/dist/truncate.d.ts.map +1 -0
  154. package/dist/truncate.js +146 -0
  155. package/dist/truncate.js.map +1 -0
  156. package/dist/types.d.ts +86 -19
  157. package/dist/types.d.ts.map +1 -1
  158. package/dist/types.js.map +1 -1
  159. package/docs/pico-v5-handoff.md +366 -86
  160. package/docs/pico-v5.md +1373 -283
  161. package/package.json +11 -3
  162. package/dist/env/utils/adaptive-publisher.d.ts +0 -24
  163. package/dist/env/utils/adaptive-publisher.d.ts.map +0 -1
  164. package/dist/env/utils/adaptive-publisher.js +0 -79
  165. package/dist/env/utils/adaptive-publisher.js.map +0 -1
  166. package/dist/env/utils/output-capture.d.ts +0 -31
  167. package/dist/env/utils/output-capture.d.ts.map +0 -1
  168. package/dist/env/utils/output-capture.js +0 -238
  169. package/dist/env/utils/output-capture.js.map +0 -1
  170. package/dist/env/utils/truncate.d.ts.map +0 -1
  171. package/dist/env/utils/truncate.js +0 -294
  172. package/dist/env/utils/truncate.js.map +0 -1
@@ -0,0 +1,100 @@
1
+ import { Type } from "typebox";
2
+ import { getOrThrow } from "../env/index.js";
3
+ import { characterEnd } from "../harness/output.js";
4
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, truncateHead } from "../truncate.js";
5
+ import { requireEnv } from "./env.js";
6
+ import { detectSupportedImageMimeType } from "./image.js";
7
+ import { resolveReadToolPath } from "./path-utils.js";
8
+ const readSchema = Type.Object({
9
+ path: Type.String({ description: "Path to the file to read (relative or absolute)" }),
10
+ offset: Type.Optional(Type.Number({ description: "Line number to start reading from (1-indexed)" })),
11
+ limit: Type.Optional(Type.Number({ description: "Maximum number of lines to read" })),
12
+ });
13
+ /** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */
14
+ export function createReadTool() {
15
+ return {
16
+ name: "read",
17
+ description: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,
18
+ parameters: readSchema,
19
+ async execute(args, api, context) {
20
+ const { path, offset, limit } = args;
21
+ const env = requireEnv(api);
22
+ const absolutePath = await resolveReadToolPath(env, path, context);
23
+ const bytes = getOrThrow(await env.readBinaryFile(absolutePath, context));
24
+ const mimeType = detectSupportedImageMimeType(bytes);
25
+ if (mimeType) {
26
+ // Image content is not supported yet.
27
+ return {
28
+ content: [],
29
+ isError: true,
30
+ diagnostics: [
31
+ {
32
+ severity: "error",
33
+ code: "unsupported_image",
34
+ message: `${path} is an image (${mimeType}); reading images is not supported`,
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ const textContent = new TextDecoder().decode(bytes);
40
+ const allLines = textContent.split("\n");
41
+ const totalFileLines = allLines.length;
42
+ const startLine = offset ? Math.max(0, offset - 1) : 0;
43
+ const startLineDisplay = startLine + 1;
44
+ if (startLine >= allLines.length) {
45
+ throw new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);
46
+ }
47
+ let selectedContent;
48
+ let userLimitedLines;
49
+ if (limit !== undefined) {
50
+ const endLine = Math.min(startLine + limit, allLines.length);
51
+ selectedContent = allLines.slice(startLine, endLine).join("\n");
52
+ userLimitedLines = endLine - startLine;
53
+ }
54
+ else {
55
+ selectedContent = allLines.slice(startLine).join("\n");
56
+ }
57
+ const { content: headText, ...truncation } = truncateHead(selectedContent);
58
+ const diagnostics = [];
59
+ let outputText = headText;
60
+ let details;
61
+ if (truncation.firstLineExceedsLimit) {
62
+ // Show the start of the line, cut at the byte limit on a character boundary.
63
+ const lineBytes = new TextEncoder().encode(allLines[startLine]);
64
+ const end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);
65
+ outputText = new TextDecoder().decode(lineBytes.subarray(0, end));
66
+ diagnostics.push({
67
+ severity: "warn",
68
+ code: "truncated",
69
+ message: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,
70
+ });
71
+ details = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };
72
+ }
73
+ else if (truncation.truncated) {
74
+ const endLineDisplay = startLineDisplay + truncation.outputLines - 1;
75
+ const nextOffset = endLineDisplay + 1;
76
+ const limitText = truncation.truncatedBy === "lines" ? "" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;
77
+ diagnostics.push({
78
+ severity: "info",
79
+ code: "truncated",
80
+ message: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,
81
+ });
82
+ details = { truncation };
83
+ }
84
+ else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {
85
+ const remaining = allLines.length - (startLine + userLimitedLines);
86
+ const nextOffset = startLine + userLimitedLines + 1;
87
+ diagnostics.push({
88
+ severity: "info",
89
+ message: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,
90
+ });
91
+ }
92
+ return {
93
+ content: outputText === "" ? [] : [{ type: "text", text: outputText }],
94
+ ...(details === undefined ? {} : { details: details }),
95
+ diagnostics,
96
+ };
97
+ },
98
+ };
99
+ }
100
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"read.js","sourceRoot":"","sources":["../../src/tools/read.ts"],"names":[],"mappings":"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","sourcesContent":["import type { JsonValue } from \"@earendil-works/chord\";\nimport { type Static, Type } from \"typebox\";\nimport { getOrThrow } from \"../env/index.ts\";\nimport { characterEnd } from \"../harness/output.ts\";\nimport type { ToolDiagnostic, ToolRegistration } from \"../harness/types.ts\";\nimport { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from \"../truncate.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { detectSupportedImageMimeType } from \"./image.ts\";\nimport { resolveReadToolPath } from \"./path-utils.ts\";\n\nconst readSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to read (relative or absolute)\" }),\n\toffset: Type.Optional(Type.Number({ description: \"Line number to start reading from (1-indexed)\" })),\n\tlimit: Type.Optional(Type.Number({ description: \"Maximum number of lines to read\" })),\n});\n\nexport type ReadToolInput = Static<typeof readSchema>;\n\nexport interface ReadToolDetails {\n\t/** How the shown text was cut; the text itself is the result content. */\n\ttruncation?: Omit<TruncationResult, \"content\">;\n}\n\n/** Reads text files. Remarks about truncation and continuation are diagnostics; the content is only file text. */\nexport function createReadTool(): ToolRegistration {\n\treturn {\n\t\tname: \"read\",\n\t\tdescription: `Read the contents of a text file. Output is truncated to ${DEFAULT_MAX_LINES} lines or ${DEFAULT_MAX_BYTES / 1024}KB (whichever is hit first). Use offset/limit for large files. When you need the full file, continue with offset until complete.`,\n\t\tparameters: readSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, offset, limit } = args as ReadToolInput;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveReadToolPath(env, path, context);\n\t\t\tconst bytes = getOrThrow(await env.readBinaryFile(absolutePath, context));\n\t\t\tconst mimeType = detectSupportedImageMimeType(bytes);\n\t\t\tif (mimeType) {\n\t\t\t\t// Image content is not supported yet.\n\t\t\t\treturn {\n\t\t\t\t\tcontent: [],\n\t\t\t\t\tisError: true,\n\t\t\t\t\tdiagnostics: [\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tseverity: \"error\",\n\t\t\t\t\t\t\tcode: \"unsupported_image\",\n\t\t\t\t\t\t\tmessage: `${path} is an image (${mimeType}); reading images is not supported`,\n\t\t\t\t\t\t},\n\t\t\t\t\t],\n\t\t\t\t};\n\t\t\t}\n\n\t\t\tconst textContent = new TextDecoder().decode(bytes);\n\t\t\tconst allLines = textContent.split(\"\\n\");\n\t\t\tconst totalFileLines = allLines.length;\n\t\t\tconst startLine = offset ? Math.max(0, offset - 1) : 0;\n\t\t\tconst startLineDisplay = startLine + 1;\n\t\t\tif (startLine >= allLines.length) {\n\t\t\t\tthrow new Error(`Offset ${offset} is beyond end of file (${allLines.length} lines total)`);\n\t\t\t}\n\n\t\t\tlet selectedContent: string;\n\t\t\tlet userLimitedLines: number | undefined;\n\t\t\tif (limit !== undefined) {\n\t\t\t\tconst endLine = Math.min(startLine + limit, allLines.length);\n\t\t\t\tselectedContent = allLines.slice(startLine, endLine).join(\"\\n\");\n\t\t\t\tuserLimitedLines = endLine - startLine;\n\t\t\t} else {\n\t\t\t\tselectedContent = allLines.slice(startLine).join(\"\\n\");\n\t\t\t}\n\n\t\t\tconst { content: headText, ...truncation } = truncateHead(selectedContent);\n\t\t\tconst diagnostics: ToolDiagnostic[] = [];\n\t\t\tlet outputText = headText;\n\t\t\tlet details: ReadToolDetails | undefined;\n\t\t\tif (truncation.firstLineExceedsLimit) {\n\t\t\t\t// Show the start of the line, cut at the byte limit on a character boundary.\n\t\t\t\tconst lineBytes = new TextEncoder().encode(allLines[startLine]);\n\t\t\t\tconst end = characterEnd(lineBytes, DEFAULT_MAX_BYTES);\n\t\t\t\toutputText = new TextDecoder().decode(lineBytes.subarray(0, end));\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"warn\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Line ${startLineDisplay} is ${formatSize(lineBytes.byteLength)}, exceeds the ${formatSize(DEFAULT_MAX_BYTES)} limit; showing its first ${formatSize(end)}. Use bash: sed -n '${startLineDisplay}p' ${path} | tail -c +${end + 1}`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation: { ...truncation, outputBytes: end, outputLines: 1 } };\n\t\t\t} else if (truncation.truncated) {\n\t\t\t\tconst endLineDisplay = startLineDisplay + truncation.outputLines - 1;\n\t\t\t\tconst nextOffset = endLineDisplay + 1;\n\t\t\t\tconst limitText = truncation.truncatedBy === \"lines\" ? \"\" : ` (${formatSize(DEFAULT_MAX_BYTES)} limit)`;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tcode: \"truncated\",\n\t\t\t\t\tmessage: `Showing lines ${startLineDisplay}-${endLineDisplay} of ${totalFileLines}${limitText}. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t\tdetails = { truncation };\n\t\t\t} else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) {\n\t\t\t\tconst remaining = allLines.length - (startLine + userLimitedLines);\n\t\t\t\tconst nextOffset = startLine + userLimitedLines + 1;\n\t\t\t\tdiagnostics.push({\n\t\t\t\t\tseverity: \"info\",\n\t\t\t\t\tmessage: `${remaining} more lines in file. Use offset=${nextOffset} to continue.`,\n\t\t\t\t});\n\t\t\t}\n\n\t\t\treturn {\n\t\t\t\tcontent: outputText === \"\" ? [] : [{ type: \"text\", text: outputText }],\n\t\t\t\t...(details === undefined ? {} : { details: details as unknown as JsonValue }),\n\t\t\t\tdiagnostics,\n\t\t\t};\n\t\t},\n\t};\n}\n"]}
@@ -0,0 +1,10 @@
1
+ import { type Static, Type } from "typebox";
2
+ import type { ToolRegistration } from "../harness/types.ts";
3
+ declare const writeSchema: Type.TObject<{
4
+ path: Type.TString;
5
+ content: Type.TString;
6
+ }>;
7
+ export type WriteToolInput = Static<typeof writeSchema>;
8
+ export declare function createWriteTool(): ToolRegistration;
9
+ export {};
10
+ //# sourceMappingURL=write.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.d.ts","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,MAAM,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE5C,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAK5D,QAAA,MAAM,WAAW;;;EAGf,CAAC;AAEH,MAAM,MAAM,cAAc,GAAG,MAAM,CAAC,OAAO,WAAW,CAAC,CAAC;AAExD,wBAAgB,eAAe,IAAI,gBAAgB,CAuBlD"}
@@ -0,0 +1,30 @@
1
+ import { Type } from "typebox";
2
+ import { getOrThrow } from "../env/index.js";
3
+ import { requireEnv } from "./env.js";
4
+ import { withFileMutationQueue } from "./file-mutation-queue.js";
5
+ import { resolveToolPath } from "./path-utils.js";
6
+ const writeSchema = Type.Object({
7
+ path: Type.String({ description: "Path to the file to write (relative or absolute)" }),
8
+ content: Type.String({ description: "Content to write to the file" }),
9
+ });
10
+ export function createWriteTool() {
11
+ return {
12
+ name: "write",
13
+ description: "Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.",
14
+ parameters: writeSchema,
15
+ async execute(args, api, context) {
16
+ const { path, content } = args;
17
+ const env = requireEnv(api);
18
+ const absolutePath = await resolveToolPath(env, path, context);
19
+ return withFileMutationQueue(env, absolutePath, async () => {
20
+ if (context.abortSignal?.aborted)
21
+ throw new Error("Operation aborted");
22
+ getOrThrow(await env.writeFile(absolutePath, content, context));
23
+ if (context.abortSignal?.aborted)
24
+ throw new Error("Operation aborted");
25
+ return { content: [{ type: "text", text: `Successfully wrote to ${path}` }] };
26
+ }, context);
27
+ },
28
+ };
29
+ }
30
+ //# sourceMappingURL=write.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"write.js","sourceRoot":"","sources":["../../src/tools/write.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,IAAI,EAAE,MAAM,SAAS,CAAC;AAC5C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtC,OAAO,EAAE,qBAAqB,EAAE,MAAM,0BAA0B,CAAC;AACjE,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAElD,MAAM,WAAW,GAAG,IAAI,CAAC,MAAM,CAAC;IAC/B,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,kDAAkD,EAAE,CAAC;IACtF,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,8BAA8B,EAAE,CAAC;CACrE,CAAC,CAAC;AAIH,MAAM,UAAU,eAAe;IAC9B,OAAO;QACN,IAAI,EAAE,OAAO;QACb,WAAW,EACV,iIAAiI;QAClI,UAAU,EAAE,WAAW;QACvB,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,EAAE,OAAO;YAC/B,MAAM,EAAE,IAAI,EAAE,OAAO,EAAE,GAAG,IAAsB,CAAC;YACjD,MAAM,GAAG,GAAG,UAAU,CAAC,GAAG,CAAC,CAAC;YAC5B,MAAM,YAAY,GAAG,MAAM,eAAe,CAAC,GAAG,EAAE,IAAI,EAAE,OAAO,CAAC,CAAC;YAC/D,OAAO,qBAAqB,CAC3B,GAAG,EACH,YAAY,EACZ,KAAK,IAAI,EAAE;gBACV,IAAI,OAAO,CAAC,WAAW,EAAE,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;gBACvE,UAAU,CAAC,MAAM,GAAG,CAAC,SAAS,CAAC,YAAY,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC;gBAChE,IAAI,OAAO,CAAC,WAAW,EAAE,OAAO;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;gBACvE,OAAO,EAAE,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,yBAAyB,IAAI,EAAE,EAAE,CAAC,EAAE,CAAC;YAC/E,CAAC,EACD,OAAO,CACP,CAAC;QACH,CAAC;KACD,CAAC;AACH,CAAC","sourcesContent":["import { type Static, Type } from \"typebox\";\nimport { getOrThrow } from \"../env/index.ts\";\nimport type { ToolRegistration } from \"../harness/types.ts\";\nimport { requireEnv } from \"./env.ts\";\nimport { withFileMutationQueue } from \"./file-mutation-queue.ts\";\nimport { resolveToolPath } from \"./path-utils.ts\";\n\nconst writeSchema = Type.Object({\n\tpath: Type.String({ description: \"Path to the file to write (relative or absolute)\" }),\n\tcontent: Type.String({ description: \"Content to write to the file\" }),\n});\n\nexport type WriteToolInput = Static<typeof writeSchema>;\n\nexport function createWriteTool(): ToolRegistration {\n\treturn {\n\t\tname: \"write\",\n\t\tdescription:\n\t\t\t\"Write content to a file. Creates the file if it doesn't exist, overwrites if it does. Automatically creates parent directories.\",\n\t\tparameters: writeSchema,\n\t\tasync execute(args, api, context) {\n\t\t\tconst { path, content } = args as WriteToolInput;\n\t\t\tconst env = requireEnv(api);\n\t\t\tconst absolutePath = await resolveToolPath(env, path, context);\n\t\t\treturn withFileMutationQueue(\n\t\t\t\tenv,\n\t\t\t\tabsolutePath,\n\t\t\t\tasync () => {\n\t\t\t\t\tif (context.abortSignal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\tgetOrThrow(await env.writeFile(absolutePath, content, context));\n\t\t\t\t\tif (context.abortSignal?.aborted) throw new Error(\"Operation aborted\");\n\t\t\t\t\treturn { content: [{ type: \"text\", text: `Successfully wrote to ${path}` }] };\n\t\t\t\t},\n\t\t\t\tcontext,\n\t\t\t);\n\t\t},\n\t};\n}\n"]}
@@ -5,11 +5,10 @@
5
5
  * - Line limit (default: 2000 lines)
6
6
  * - Byte limit (default: 50KB)
7
7
  *
8
- * Never returns partial lines (except bash tail truncation edge case).
8
+ * Never returns partial lines. Tool output streams are bounded by `src/harness/output.ts` instead.
9
9
  */
10
10
  export declare const DEFAULT_MAX_LINES = 2000;
11
11
  export declare const DEFAULT_MAX_BYTES: number;
12
- export declare const GREP_MAX_LINE_LENGTH = 500;
13
12
  export interface TruncationResult {
14
13
  /** The truncated content */
15
14
  content: string;
@@ -53,19 +52,4 @@ export declare function formatSize(bytes: number): string;
53
52
  * returns empty content with firstLineExceedsLimit=true.
54
53
  */
55
54
  export declare function truncateHead(content: string, options?: TruncationOptions): TruncationResult;
56
- /**
57
- * Truncate content from the tail (keep last N lines/bytes).
58
- * Suitable for bash output where you want to see the end (errors, final results).
59
- *
60
- * May return partial first line if the last line of original content exceeds byte limit.
61
- */
62
- export declare function truncateTail(content: string, options?: TruncationOptions): TruncationResult;
63
- /**
64
- * Truncate a single line to max characters, adding [truncated] suffix.
65
- * Used for grep match lines.
66
- */
67
- export declare function truncateLine(line: string, maxChars?: number): {
68
- text: string;
69
- wasTruncated: boolean;
70
- };
71
55
  //# sourceMappingURL=truncate.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.d.ts","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,eAAO,MAAM,iBAAiB,OAAO,CAAC;AACtC,eAAO,MAAM,iBAAiB,QAAY,CAAC;AAE3C,MAAM,WAAW,gBAAgB;IAChC,4BAA4B;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,kCAAkC;IAClC,SAAS,EAAE,OAAO,CAAC;IACnB,sEAAsE;IACtE,WAAW,EAAE,OAAO,GAAG,OAAO,GAAG,IAAI,CAAC;IACtC,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,oDAAoD;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,uDAAuD;IACvD,WAAW,EAAE,MAAM,CAAC;IACpB,8CAA8C;IAC9C,WAAW,EAAE,MAAM,CAAC;IACpB,yFAAyF;IACzF,eAAe,EAAE,OAAO,CAAC;IACzB,2EAA2E;IAC3E,qBAAqB,EAAE,OAAO,CAAC;IAC/B,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IACjC,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,8CAA8C;IAC9C,QAAQ,CAAC,EAAE,MAAM,CAAC;CAClB;AASD,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CA0BtD;AASD;;GAEG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAQhD;AAED;;;;;;GAMG;AACH,wBAAgB,YAAY,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,iBAAsB,GAAG,gBAAgB,CAgF/F"}
@@ -0,0 +1,146 @@
1
+ /**
2
+ * Shared truncation utilities for tool outputs.
3
+ *
4
+ * Truncation is based on two independent limits - whichever is hit first wins:
5
+ * - Line limit (default: 2000 lines)
6
+ * - Byte limit (default: 50KB)
7
+ *
8
+ * Never returns partial lines. Tool output streams are bounded by `src/harness/output.ts` instead.
9
+ */
10
+ export const DEFAULT_MAX_LINES = 2000;
11
+ export const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB
12
+ const runtimeBuffer = globalThis.Buffer;
13
+ const nonAsciiPattern = /[^\x00-\x7f]/;
14
+ export function utf8ByteLength(content) {
15
+ if (runtimeBuffer)
16
+ return runtimeBuffer.byteLength(content, "utf8");
17
+ const firstNonAscii = content.search(nonAsciiPattern);
18
+ if (firstNonAscii === -1)
19
+ return content.length;
20
+ let bytes = firstNonAscii;
21
+ for (let i = firstNonAscii; i < content.length; i++) {
22
+ const code = content.charCodeAt(i);
23
+ if (code <= 0x7f) {
24
+ bytes += 1;
25
+ }
26
+ else if (code <= 0x7ff) {
27
+ bytes += 2;
28
+ }
29
+ else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {
30
+ const next = content.charCodeAt(i + 1);
31
+ if (next >= 0xdc00 && next <= 0xdfff) {
32
+ bytes += 4;
33
+ i++;
34
+ }
35
+ else {
36
+ bytes += 3;
37
+ }
38
+ }
39
+ else {
40
+ bytes += 3;
41
+ }
42
+ }
43
+ return bytes;
44
+ }
45
+ function splitLinesForCounting(content) {
46
+ if (content.length === 0)
47
+ return [];
48
+ const lines = content.split("\n");
49
+ if (content.endsWith("\n"))
50
+ lines.pop();
51
+ return lines;
52
+ }
53
+ /**
54
+ * Format bytes as human-readable size.
55
+ */
56
+ export function formatSize(bytes) {
57
+ if (bytes < 1024) {
58
+ return `${bytes}B`;
59
+ }
60
+ else if (bytes < 1024 * 1024) {
61
+ return `${(bytes / 1024).toFixed(1)}KB`;
62
+ }
63
+ else {
64
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`;
65
+ }
66
+ }
67
+ /**
68
+ * Truncate content from the head (keep first N lines/bytes).
69
+ * Suitable for file reads where you want to see the beginning.
70
+ *
71
+ * Never returns partial lines. If first line exceeds byte limit,
72
+ * returns empty content with firstLineExceedsLimit=true.
73
+ */
74
+ export function truncateHead(content, options = {}) {
75
+ const maxLines = options.maxLines ?? DEFAULT_MAX_LINES;
76
+ const maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;
77
+ const totalBytes = utf8ByteLength(content);
78
+ const lines = splitLinesForCounting(content);
79
+ const totalLines = lines.length;
80
+ // Check if no truncation needed
81
+ if (totalLines <= maxLines && totalBytes <= maxBytes) {
82
+ return {
83
+ content,
84
+ truncated: false,
85
+ truncatedBy: null,
86
+ totalLines,
87
+ totalBytes,
88
+ outputLines: totalLines,
89
+ outputBytes: totalBytes,
90
+ lastLinePartial: false,
91
+ firstLineExceedsLimit: false,
92
+ maxLines,
93
+ maxBytes,
94
+ };
95
+ }
96
+ // Check if first line alone exceeds byte limit
97
+ const firstLineBytes = utf8ByteLength(lines[0]);
98
+ if (firstLineBytes > maxBytes) {
99
+ return {
100
+ content: "",
101
+ truncated: true,
102
+ truncatedBy: "bytes",
103
+ totalLines,
104
+ totalBytes,
105
+ outputLines: 0,
106
+ outputBytes: 0,
107
+ lastLinePartial: false,
108
+ firstLineExceedsLimit: true,
109
+ maxLines,
110
+ maxBytes,
111
+ };
112
+ }
113
+ // Collect complete lines that fit
114
+ const outputLinesArr = [];
115
+ let outputBytesCount = 0;
116
+ let truncatedBy = "lines";
117
+ for (let i = 0; i < lines.length && i < maxLines; i++) {
118
+ const line = lines[i];
119
+ const lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline
120
+ if (outputBytesCount + lineBytes > maxBytes) {
121
+ truncatedBy = "bytes";
122
+ break;
123
+ }
124
+ outputLinesArr.push(line);
125
+ outputBytesCount += lineBytes;
126
+ }
127
+ // Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.
128
+ if (truncatedBy !== "bytes")
129
+ truncatedBy = outputLinesArr.length < totalLines ? "lines" : "bytes";
130
+ const outputContent = outputLinesArr.join("\n");
131
+ const finalOutputBytes = utf8ByteLength(outputContent);
132
+ return {
133
+ content: outputContent,
134
+ truncated: true,
135
+ truncatedBy,
136
+ totalLines,
137
+ totalBytes,
138
+ outputLines: outputLinesArr.length,
139
+ outputBytes: finalOutputBytes,
140
+ lastLinePartial: false,
141
+ firstLineExceedsLimit: false,
142
+ maxLines,
143
+ maxBytes,
144
+ };
145
+ }
146
+ //# sourceMappingURL=truncate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"truncate.js","sourceRoot":"","sources":["../src/truncate.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Shared truncation utilities for tool outputs.\n *\n * Truncation is based on two independent limits - whichever is hit first wins:\n * - Line limit (default: 2000 lines)\n * - Byte limit (default: 50KB)\n *\n * Never returns partial lines. Tool output streams are bounded by `src/harness/output.ts` instead.\n */\n\nexport const DEFAULT_MAX_LINES = 2000;\nexport const DEFAULT_MAX_BYTES = 50 * 1024; // 50KB\n\nexport interface TruncationResult {\n\t/** The truncated content */\n\tcontent: string;\n\t/** Whether truncation occurred */\n\ttruncated: boolean;\n\t/** Which limit was hit: \"lines\", \"bytes\", or null if not truncated */\n\ttruncatedBy: \"lines\" | \"bytes\" | null;\n\t/** Total number of lines in the original content */\n\ttotalLines: number;\n\t/** Total number of bytes in the original content */\n\ttotalBytes: number;\n\t/** Number of complete lines in the truncated output */\n\toutputLines: number;\n\t/** Number of bytes in the truncated output */\n\toutputBytes: number;\n\t/** Whether the last line was partially truncated (only for tail truncation edge case) */\n\tlastLinePartial: boolean;\n\t/** Whether the first line exceeded the byte limit (for head truncation) */\n\tfirstLineExceedsLimit: boolean;\n\t/** The max lines limit that was applied */\n\tmaxLines: number;\n\t/** The max bytes limit that was applied */\n\tmaxBytes: number;\n}\n\nexport interface TruncationOptions {\n\t/** Maximum number of lines (default: 2000) */\n\tmaxLines?: number;\n\t/** Maximum number of bytes (default: 50KB) */\n\tmaxBytes?: number;\n}\n\ninterface RuntimeBuffer {\n\tbyteLength(content: string, encoding: \"utf8\"): number;\n}\n\nconst runtimeBuffer = (globalThis as { Buffer?: RuntimeBuffer }).Buffer;\nconst nonAsciiPattern = /[^\\x00-\\x7f]/;\n\nexport function utf8ByteLength(content: string): number {\n\tif (runtimeBuffer) return runtimeBuffer.byteLength(content, \"utf8\");\n\n\tconst firstNonAscii = content.search(nonAsciiPattern);\n\tif (firstNonAscii === -1) return content.length;\n\n\tlet bytes = firstNonAscii;\n\tfor (let i = firstNonAscii; i < content.length; i++) {\n\t\tconst code = content.charCodeAt(i);\n\t\tif (code <= 0x7f) {\n\t\t\tbytes += 1;\n\t\t} else if (code <= 0x7ff) {\n\t\t\tbytes += 2;\n\t\t} else if (code >= 0xd800 && code <= 0xdbff && i + 1 < content.length) {\n\t\t\tconst next = content.charCodeAt(i + 1);\n\t\t\tif (next >= 0xdc00 && next <= 0xdfff) {\n\t\t\t\tbytes += 4;\n\t\t\t\ti++;\n\t\t\t} else {\n\t\t\t\tbytes += 3;\n\t\t\t}\n\t\t} else {\n\t\t\tbytes += 3;\n\t\t}\n\t}\n\treturn bytes;\n}\n\nfunction splitLinesForCounting(content: string): string[] {\n\tif (content.length === 0) return [];\n\tconst lines = content.split(\"\\n\");\n\tif (content.endsWith(\"\\n\")) lines.pop();\n\treturn lines;\n}\n\n/**\n * Format bytes as human-readable size.\n */\nexport function formatSize(bytes: number): string {\n\tif (bytes < 1024) {\n\t\treturn `${bytes}B`;\n\t} else if (bytes < 1024 * 1024) {\n\t\treturn `${(bytes / 1024).toFixed(1)}KB`;\n\t} else {\n\t\treturn `${(bytes / (1024 * 1024)).toFixed(1)}MB`;\n\t}\n}\n\n/**\n * Truncate content from the head (keep first N lines/bytes).\n * Suitable for file reads where you want to see the beginning.\n *\n * Never returns partial lines. If first line exceeds byte limit,\n * returns empty content with firstLineExceedsLimit=true.\n */\nexport function truncateHead(content: string, options: TruncationOptions = {}): TruncationResult {\n\tconst maxLines = options.maxLines ?? DEFAULT_MAX_LINES;\n\tconst maxBytes = options.maxBytes ?? DEFAULT_MAX_BYTES;\n\n\tconst totalBytes = utf8ByteLength(content);\n\tconst lines = splitLinesForCounting(content);\n\tconst totalLines = lines.length;\n\n\t// Check if no truncation needed\n\tif (totalLines <= maxLines && totalBytes <= maxBytes) {\n\t\treturn {\n\t\t\tcontent,\n\t\t\ttruncated: false,\n\t\t\ttruncatedBy: null,\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: totalLines,\n\t\t\toutputBytes: totalBytes,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: false,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Check if first line alone exceeds byte limit\n\tconst firstLineBytes = utf8ByteLength(lines[0]);\n\tif (firstLineBytes > maxBytes) {\n\t\treturn {\n\t\t\tcontent: \"\",\n\t\t\ttruncated: true,\n\t\t\ttruncatedBy: \"bytes\",\n\t\t\ttotalLines,\n\t\t\ttotalBytes,\n\t\t\toutputLines: 0,\n\t\t\toutputBytes: 0,\n\t\t\tlastLinePartial: false,\n\t\t\tfirstLineExceedsLimit: true,\n\t\t\tmaxLines,\n\t\t\tmaxBytes,\n\t\t};\n\t}\n\n\t// Collect complete lines that fit\n\tconst outputLinesArr: string[] = [];\n\tlet outputBytesCount = 0;\n\tlet truncatedBy: \"lines\" | \"bytes\" = \"lines\";\n\n\tfor (let i = 0; i < lines.length && i < maxLines; i++) {\n\t\tconst line = lines[i];\n\t\tconst lineBytes = utf8ByteLength(line) + (i > 0 ? 1 : 0); // +1 for newline\n\n\t\tif (outputBytesCount + lineBytes > maxBytes) {\n\t\t\ttruncatedBy = \"bytes\";\n\t\t\tbreak;\n\t\t}\n\n\t\toutputLinesArr.push(line);\n\t\toutputBytesCount += lineBytes;\n\t}\n\n\t// Without a byte break, only omitted lines prove the line limit was reached; otherwise a trailing newline exceeded bytes.\n\tif (truncatedBy !== \"bytes\") truncatedBy = outputLinesArr.length < totalLines ? \"lines\" : \"bytes\";\n\n\tconst outputContent = outputLinesArr.join(\"\\n\");\n\tconst finalOutputBytes = utf8ByteLength(outputContent);\n\n\treturn {\n\t\tcontent: outputContent,\n\t\ttruncated: true,\n\t\ttruncatedBy,\n\t\ttotalLines,\n\t\ttotalBytes,\n\t\toutputLines: outputLinesArr.length,\n\t\toutputBytes: finalOutputBytes,\n\t\tlastLinePartial: false,\n\t\tfirstLineExceedsLimit: false,\n\t\tmaxLines,\n\t\tmaxBytes,\n\t};\n}\n"]}
package/dist/types.d.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { AttachedReplicatedState, Context, Draft, JsonValue } from "@earendil-works/chord";
2
2
  import type { Op } from "@earendil-works/chord/delta";
3
3
  import type { Message, Models } from "@earendil-works/pi-ai";
4
- import type { ContextView, RegistrySnapshot } from "./harness/types.ts";
4
+ import type { ExecutionEnv } from "./env/index.ts";
5
+ import type { ContextView, ConversationHandle, RegistrySnapshot, SettledTask } from "./harness/types.ts";
5
6
  /** JSON object used as the root of every durable document. */
6
7
  export type JsonObject = {
7
8
  [key: string]: JsonValue;
@@ -104,27 +105,45 @@ export type RunningTask<I, S, R> = TaskRecord<I, S, R> & {
104
105
  readonly status: "running";
105
106
  }>;
106
107
  };
107
- /** Next state a task commits for itself: a replacement checkpoint or its terminal outcome. */
108
+ /**
109
+ * Next state a task commits for itself: a replacement checkpoint, a wait, or its outcome. A returned `terminal` state is
110
+ * stored as `completing` while ordinary owned work below the task is live (spec §5.5).
111
+ */
108
112
  export type NextTaskState<S, R> = Extract<TaskState<S, R>, {
109
- readonly status: "running" | "terminal";
113
+ readonly status: "running" | "waiting" | "terminal";
110
114
  }>;
111
115
  /**
112
116
  * Runs one checkpoint phase. It must commit a changed checkpoint or a terminal outcome through `runtime.commit()`;
113
117
  * returning without durable progress faults the task.
114
118
  */
115
119
  export type PhaseHandler<I, P, S, R, H extends object> = (task: RunningTask<I, P, R>, runtime: TaskRuntime<I, S, R, H>, context: Context) => Promise<void>;
120
+ /** Dispatches one hook of a task to every matching registered handler, in registry order of the phase snapshot. */
121
+ export interface HookRunner<H extends object> {
122
+ /**
123
+ * Call `invoke` with each matching handler named `name`. An ordinary throw from `invoke` is reported and the next
124
+ * handler runs; once the invocation is signalled, the error propagates. Composition happens inside `invoke`.
125
+ */
126
+ each<K extends keyof H>(name: K, invoke: (handler: NonNullable<H[K]>) => void | Promise<void>): Promise<void>;
127
+ }
116
128
  /**
117
129
  * Operations of one task invocation. Every operation rejects after the invocation ends; watches acquired through it
118
- * stop at invocation end. `_H` is the task's hook map, consumed once the runtime gains its hook runner.
130
+ * stop at invocation end.
119
131
  */
120
- export interface TaskRuntime<I, S, R, _H extends object> extends DocumentObserver, DocumentReader {
132
+ export interface TaskRuntime<I, S, R, H extends object> extends DocumentObserver, DocumentReader {
121
133
  readonly taskId: TaskId<R>;
122
134
  readonly conversationId: ConversationId;
123
- /** Aborted when the run is signalled by `abortTask()` or the Harness closes. */
135
+ /**
136
+ * Aborted when the run is signalled by `abortTask()`, the Harness closes, or the invocation ends. Work still using it
137
+ * after the invocation ended, such as a detached wait, is cancelled; it could not write anything anyway.
138
+ */
124
139
  readonly signal: AbortSignal;
125
140
  /** Registry snapshot of the current phase; refreshed at every phase boundary. */
126
141
  readonly registry: RegistrySnapshot;
127
142
  readonly models: Models;
143
+ /** `HarnessOptions.env`; tools receive it as `api.env`. */
144
+ readonly env: ExecutionEnv | undefined;
145
+ /** Handlers registered for this task's name whose scope matches its conversation. */
146
+ readonly hooks: HookRunner<H>;
128
147
  /**
129
148
  * Commit on the Session line after rereading the task. Rejects when the task is terminal, the invocation ended, the
130
149
  * Harness is closing, or, in a run invocation, the task carries an abort mark. A returned state replaces the task's
@@ -136,6 +155,21 @@ export interface TaskRuntime<I, S, R, _H extends object> extends DocumentObserve
136
155
  memo<T extends JsonValue>(name: string, context: Context): Promise<T | undefined>;
137
156
  /** Store `candidate` unless a memo already exists; return the durable winner. */
138
157
  memo<T extends JsonValue>(name: string, candidate: T, context: Context): Promise<T>;
158
+ /** Committed task record. */
159
+ getTask<T>(id: TaskId<T>, context: Context): Promise<TaskRecord<JsonValue, JsonValue, T> | undefined>;
160
+ /** Resolve with the task's terminal receipt; rejects when the invocation ends. */
161
+ waitForTask<T>(id: TaskId<T>, context: Context): Promise<SettledTask<T>>;
162
+ /** Outcomes of terminal tasks, in order; rejects when one is missing or not terminal. Used after a wait. */
163
+ outcomes<T>(ids: readonly TaskId<T>[], context: Context): Promise<TaskOutcome<T>[]>;
164
+ /**
165
+ * Invocation-bound handle of an existing conversation, for example one this task owns; `undefined` when absent. Its
166
+ * operations and the submissions it returns reject after the invocation ends; admitted work stays durable.
167
+ */
168
+ conversation(id: ConversationId, context: Context): Promise<ConversationHandle | undefined>;
169
+ /** Committed entry visible from the task's conversation. */
170
+ entry(id: EntryId, context: Context): Promise<EntryRecord | undefined>;
171
+ /** Undefined when the entry is absent, not visible, or has another kind. */
172
+ entry<D extends JsonValue>(token: Entry<D>, id: EntryId, context: Context): Promise<TypedEntry<D> | undefined>;
139
173
  /** Committed raw active transcript and model context, optionally cut off at the visible entry `at`. */
140
174
  context(conversationId: ConversationId, context: Context, at?: EntryId): Promise<ContextView>;
141
175
  /** The Harness clock. */
@@ -176,13 +210,25 @@ export interface Task<I, S extends {
176
210
  }, R, H extends object> {
177
211
  readonly definition: TaskDefinition<I, S, R, H>;
178
212
  }
213
+ /** Who owns a task: its conversation (a top-level task) or another task of the same conversation (a child task). */
214
+ export type TaskOwnership = {
215
+ readonly kind: "conversation";
216
+ } | {
217
+ readonly kind: "task";
218
+ readonly taskId: TaskId;
219
+ };
220
+ /** How a waiting task treats the tasks it waits on (spec §5.5). */
221
+ export type JoinPolicy = "failFast" | "allSettled";
179
222
  /** Creation options for a durable task. */
180
223
  export type TaskOptions = {
181
- /** Owning conversation; required for Session commits that are not bound to a conversation. */
224
+ /** Required: a task always names its owner (spec §5.5). */
225
+ readonly ownership: TaskOwnership;
226
+ /**
227
+ * Default: the owner task's conversation, or the transaction's bound conversation; required for conversation-owned
228
+ * tasks created by Session commits that are not bound to a conversation.
229
+ */
182
230
  readonly conversationId?: ConversationId;
183
- /** Tasks that must be terminal before ordinary execution may begin. */
184
- readonly after?: readonly TaskId[];
185
- /** Excluded from ordinary idle waits and conversation aborts. */
231
+ /** Conversation-owned tasks only: excluded from ordinary idle waits, conversation aborts, and cascades. */
186
232
  readonly background?: boolean;
187
233
  };
188
234
  /** Ownership selected explicitly whenever a conversation is created. */
@@ -373,7 +419,7 @@ export type TaskOutcome<R> = {
373
419
  };
374
420
  /** Complete durable execution state of a task. */
375
421
  export type TaskState<S, R> = {
376
- /** Eligible for scheduling when its dependencies are terminal. */
422
+ /** Eligible for scheduling. */
377
423
  readonly status: "pending";
378
424
  /** Complete durable state from which execution resumes. */
379
425
  readonly checkpoint: S;
@@ -384,6 +430,18 @@ export type TaskState<S, R> = {
384
430
  /** Complete durable state from which execution resumes. */
385
431
  readonly checkpoint: S;
386
432
  readonly outcome?: never;
433
+ } | {
434
+ /** Parked without an invocation until every task in `on` is terminal; then resumes at `checkpoint`. */
435
+ readonly status: "waiting";
436
+ readonly checkpoint: S;
437
+ readonly on: readonly TaskId[];
438
+ readonly policy: JoinPolicy;
439
+ readonly outcome?: never;
440
+ } | {
441
+ /** Outcome decided; becomes terminal once no ordinary owned work below is live. Runs no more code. */
442
+ readonly status: "completing";
443
+ readonly checkpoint?: never;
444
+ readonly outcome: TaskOutcome<R>;
387
445
  } | {
388
446
  /** Permanently settled durable result receipt. */
389
447
  readonly status: "terminal";
@@ -400,9 +458,9 @@ type TaskRecordBase<I, R> = {
400
458
  readonly version: number;
401
459
  /** Original task input retained while the task is live or terminal. */
402
460
  readonly input: I;
403
- /** Tasks that must be terminal before ordinary execution may begin. */
404
- readonly after: readonly TaskId[];
405
- /** Whether this task is excluded from ordinary idle waits and conversation aborts. */
461
+ /** Owning task of a child task; absent for a task its conversation owns. Immutable. */
462
+ readonly owner?: TaskId;
463
+ /** Whether this conversation-owned task is excluded from ordinary idle waits, conversation aborts, and cascades. */
406
464
  readonly background: boolean;
407
465
  /** Durable abort mark checked before run-mode progress is committed. */
408
466
  readonly abortRequested: boolean;
@@ -410,13 +468,13 @@ type TaskRecordBase<I, R> = {
410
468
  /** Complete replacement record for one durable task state machine. */
411
469
  export type TaskRecord<I, S, R> = TaskRecordBase<I, R> & ({
412
470
  readonly state: Extract<TaskState<S, R>, {
413
- readonly status: "pending" | "running";
471
+ readonly status: "pending" | "running" | "waiting";
414
472
  }>;
415
- /** Small first-writer-wins values retained while the task is live. */
473
+ /** Small first-writer-wins values retained while the task can run. */
416
474
  readonly memos?: Readonly<Record<string, JsonValue>>;
417
475
  } | {
418
476
  readonly state: Extract<TaskState<S, R>, {
419
- readonly status: "terminal";
477
+ readonly status: "completing" | "terminal";
420
478
  }>;
421
479
  readonly memos?: never;
422
480
  });
@@ -487,7 +545,7 @@ export type EntryQuery = {
487
545
  export type TaskQuery = {
488
546
  readonly conversationId?: ConversationId;
489
547
  readonly kind?: string;
490
- readonly status?: "pending" | "running" | "terminal";
548
+ readonly status?: TaskState<JsonValue, JsonValue>["status"];
491
549
  readonly abortRequested?: boolean;
492
550
  readonly background?: boolean;
493
551
  };
@@ -606,6 +664,10 @@ export interface Tx {
606
664
  task(id: TaskId): Promise<TaskRecord<JsonValue, JsonValue, JsonValue> | undefined>;
607
665
  scanConversations(query: ConversationQuery, limit: number, cursor?: Cursor): Promise<Page<ConversationRecord, Cursor>>;
608
666
  scanEntries(query: EntryQuery, limit: number, cursor?: Cursor): Promise<Page<EntryRecord, Cursor>>;
667
+ /** Newest visible entry of the conversation that carries a `head`. */
668
+ latestHeadMarker(conversationId: ConversationId): Promise<(EntryRecord & {
669
+ readonly head: EntryId;
670
+ }) | undefined>;
609
671
  scanTasks(query: TaskQuery, limit: number, cursor?: Cursor): Promise<Page<TaskRecord<JsonValue, JsonValue, JsonValue>, Cursor>>;
610
672
  /** Create a conversation with explicitly selected ownership. */
611
673
  createConversation(options: {
@@ -621,13 +683,18 @@ export interface Tx {
621
683
  appendEntry<D extends JsonValue>(token: Entry<D>, conversationId: ConversationId, value: TypedEntryDraft<NoInfer<D>>): Promise<TypedEntry<D>>;
622
684
  createTask<I, S extends {
623
685
  phase: string;
624
- }, R, H extends object>(task: Task<I, S, R, H>, input: I, options?: TaskOptions): Promise<TaskId<R>>;
686
+ }, R, H extends object>(task: Task<I, S, R, H>, input: I, options: TaskOptions): Promise<TaskId<R>>;
625
687
  /**
626
688
  * Settle a queued or placed submission; only a placed input can be answered, and a settled submission stays
627
689
  * unchanged. Resolved against this transaction's latest record of the submission, so it works after table writes.
628
690
  * Run tasks settle the inputs they answer.
629
691
  */
630
692
  settleSubmission(id: SubmissionId, settlement: SubmissionSettlement): void;
693
+ /**
694
+ * Place a queued submission at `entry`: an input becomes `placed`, a write `done`. Resolved like
695
+ * `settleSubmission()`. Inbox boundaries place the submissions they select.
696
+ */
697
+ placeSubmission(id: SubmissionId, entry: EntryId): void;
631
698
  doc<T extends JsonObject>(token: SessionDocToken<T>): Promise<Draft<T>>;
632
699
  doc<T extends JsonObject>(token: ConversationDocToken<T>, conversationId: ConversationId): Promise<Draft<T>>;
633
700
  doc<T extends JsonObject>(token: TaskDocToken<T>, taskId: TaskId): Promise<Draft<T>>;