@inflexa-ai/harness 0.47.0 → 0.48.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/README.md +1 -1
  2. package/dist/agents/conversation-agent.d.ts +6 -0
  3. package/dist/agents/conversation-agent.js +2 -0
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +13 -12
  6. package/dist/index.d.ts +5 -3
  7. package/dist/index.js +3 -2
  8. package/dist/lib/hooks.d.ts +1 -1
  9. package/dist/loop/chat-stream.js +2 -1
  10. package/dist/loop/types.d.ts +5 -0
  11. package/dist/prompts/report-session.d.ts +1 -1
  12. package/dist/prompts/report-session.js +7 -5
  13. package/dist/runtime/assemble.d.ts +13 -4
  14. package/dist/runtime/assemble.js +2 -0
  15. package/dist/runtime/dbos.d.ts +6 -0
  16. package/dist/runtime/dbos.js +14 -2
  17. package/dist/state/artifacts.d.ts +4 -1
  18. package/dist/state/artifacts.js +4 -1
  19. package/dist/state/index.d.ts +4 -4
  20. package/dist/state/index.js +2 -2
  21. package/dist/state/plans.d.ts +27 -0
  22. package/dist/state/plans.js +57 -3
  23. package/dist/state/step-executions.d.ts +13 -0
  24. package/dist/state/step-executions.js +22 -0
  25. package/dist/tools/report-authoring/authoring-tools.d.ts +2 -2
  26. package/dist/tools/report-authoring/authoring-tools.js +31 -6
  27. package/dist/tools/report-session/script-tools.d.ts +3 -0
  28. package/dist/tools/report-session/script-tools.js +10 -1
  29. package/dist/tools/script/archive.d.ts +34 -0
  30. package/dist/tools/script/archive.js +182 -0
  31. package/dist/tools/script/file-tools.d.ts +10 -4
  32. package/dist/tools/script/file-tools.js +156 -42
  33. package/dist/tools/script/metrics.d.ts +10 -1
  34. package/dist/tools/script/metrics.js +17 -2
  35. package/dist/tools/script/run-script.js +91 -4
  36. package/dist/tools/script/scratch.d.ts +3 -0
  37. package/dist/tools/script/scratch.js +7 -4
  38. package/dist/tools/workspace/mutator.d.ts +3 -0
  39. package/dist/tools/workspace/mutator.js +11 -1
  40. package/dist/workspace/session-files.d.ts +35 -0
  41. package/dist/workspace/session-files.js +16 -0
  42. package/package.json +2 -1
@@ -172,6 +172,28 @@ export function queryStepsByRun(pool, runId) {
172
172
  return result.rows.map(mapStepExecutionRow);
173
173
  });
174
174
  }
175
+ /**
176
+ * The step counts of each run of a list, in one statement. `totalSteps` counts each row, the `synthesis` row
177
+ * included, and `doneSteps` counts the `completed` rows: the rule of the analysis summary reads. A step that
178
+ * waits counts in `totalSteps` only. A run with no row has no entry, and a caller reads it as 0.
179
+ */
180
+ export function countStepsByRuns(pool, analysisId, runIds) {
181
+ return tryQuery("stepExecutions.countStepsByRuns", async () => {
182
+ if (runIds.length === 0)
183
+ return [];
184
+ const result = await pool.query({
185
+ text: `SELECT run_id,
186
+ COUNT(*) FILTER (WHERE status = 'completed')::int AS done,
187
+ COUNT(*) FILTER (WHERE status = 'running')::int AS running,
188
+ COUNT(*)::int AS total
189
+ FROM cortex_step_executions
190
+ WHERE analysis_id = $1 AND run_id = ANY($2::text[])
191
+ GROUP BY run_id`,
192
+ values: [analysisId, [...runIds]],
193
+ });
194
+ return result.rows.map((r) => ({ runId: r.run_id, doneSteps: r.done, runningSteps: r.running, totalSteps: r.total }));
195
+ });
196
+ }
175
197
  function mapStepExecutionRow(row) {
176
198
  return StepExecutionRowSchema.parse({
177
199
  runId: row.run_id,
@@ -16,8 +16,8 @@
16
16
  * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
17
17
  * nested union. A conflict between the flat fields refuses before the core runs.
18
18
  *
19
- * Inside a durable step, a mutation stores its result for the thread and the step, in the transaction of
20
- * the document and under the lock of the row. A rerun of the step after the landing, before its
19
+ * Inside a durable step, a mutation stores its result for the thread, the step, and the call, in the
20
+ * transaction of the document and under the lock of the row. A rerun of the step after the landing, before its
21
21
  * checkpoint, gives the stored result. Thus a rerun of `add_block` does not refuse its own block id, and a
22
22
  * rerun of `remove_block` does not refuse its own removed target.
23
23
  */
@@ -16,8 +16,8 @@
16
16
  * The primitive admits a flat object input only. Thus a destination is four optional fields, and not a
17
17
  * nested union. A conflict between the flat fields refuses before the core runs.
18
18
  *
19
- * Inside a durable step, a mutation stores its result for the thread and the step, in the transaction of
20
- * the document and under the lock of the row. A rerun of the step after the landing, before its
19
+ * Inside a durable step, a mutation stores its result for the thread, the step, and the call, in the
20
+ * transaction of the document and under the lock of the row. A rerun of the step after the landing, before its
21
21
  * checkpoint, gives the stored result. Thus a rerun of `add_block` does not refuse its own block id, and a
22
22
  * rerun of `remove_block` does not refuse its own removed target.
23
23
  */
@@ -133,6 +133,23 @@ const LandedResultSchema = z.object({
133
133
  })),
134
134
  })),
135
135
  });
136
+ /** A refusal as a script reads it: the closed reason and the prose detail. */
137
+ const RefusalOutputSchema = z.object({ reason: z.string(), detail: z.string() });
138
+ const OutlineEntryOutputSchema = z.object({ id: z.string(), kind: z.string(), depth: z.number(), label: z.string() });
139
+ /** The result of an edit, as `run_script` shows it to a script. A refused edit lands nothing. */
140
+ const MutationOutputSchema = z.union([
141
+ z.object({
142
+ applied: z.literal(true),
143
+ changed: z.array(z.object({ parentId: z.string().optional(), children: z.array(OutlineEntryOutputSchema) })),
144
+ }),
145
+ z.object({ applied: z.literal(false), refusal: RefusalOutputSchema }),
146
+ ]);
147
+ const OutlineOutputSchema = z.union([z.object({ outline: z.array(OutlineEntryOutputSchema) }), z.object({ refused: RefusalOutputSchema })]);
148
+ const ReadBlockOutputSchema = z.union([
149
+ z.object({ found: z.literal(true), block: z.record(z.string(), z.unknown()) }),
150
+ z.object({ found: z.literal(false) }),
151
+ z.object({ refused: RefusalOutputSchema }),
152
+ ]);
136
153
  /**
137
154
  * Resolve the report thread of a call, and load its state through the gateway.
138
155
  *
@@ -439,7 +456,9 @@ export function createReportAuthoringTools(gateway, deps = {}) {
439
456
  * the landing of its own earlier run.
440
457
  */
441
458
  const openMutation = async (ctx) => {
442
- const callKey = stepKey();
459
+ // The calls of one script share the step of `run_script`, thus the key names the call inside its step.
460
+ const step = stepKey();
461
+ const callKey = step === undefined ? undefined : `${step}:${ctx.invocationId}`;
443
462
  const opened = await openReportThread(gateway, ctx.session.scope, callKey);
444
463
  if (opened.isErr())
445
464
  return { done: refuse(opened.error) };
@@ -504,6 +523,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
504
523
  "The root admits a section only, and an atom needs a section as its parent. " +
505
524
  "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
506
525
  inputSchema: addBlockInput,
526
+ outputSchema: MutationOutputSchema,
507
527
  describeCall: (input) => {
508
528
  const block = decodeBlockPayload(input.block);
509
529
  const kind = readKind(block);
@@ -534,10 +554,11 @@ export function createReportAuthoringTools(gateway, deps = {}) {
534
554
  });
535
555
  const change_block = defineTool({
536
556
  id: "change_block",
537
- description: "Change one block by its id. Give a `title` to retitle a section, or a `block` to replace an atom, and never both. " +
557
+ description: "Change one block by its id: a `title` retitles a section, or a `block` replaces an atom, and never both. " +
538
558
  "The change keeps the id of the target, thus the `id` of the payload does not matter. A kind change is permitted. " +
539
559
  "A reference names the path of a pinned artifact, and the session stamps the hash from the pinned evidence.",
540
560
  inputSchema: changeBlockInput,
561
+ outputSchema: MutationOutputSchema,
541
562
  describeCall: (input) => `change ${input.targetId}`,
542
563
  execute: async (input, ctx) => {
543
564
  const opening = await openMutation(ctx);
@@ -560,6 +581,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
560
581
  id: "remove_block",
561
582
  description: "Remove one block by its id. A removed section takes its whole subtree with it, and the id is free for a later add.",
562
583
  inputSchema: removeBlockInput,
584
+ outputSchema: MutationOutputSchema,
563
585
  describeCall: (input) => `remove ${input.targetId}`,
564
586
  execute: async (input, ctx) => {
565
587
  const opening = await openMutation(ctx);
@@ -579,6 +601,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
579
601
  description: "Move one block by its id to a new destination. Name the destination with `parentId` and one of `place`, `before`, or `after`. " +
580
602
  "A section cannot move into its own subtree.",
581
603
  inputSchema: moveBlockInput,
604
+ outputSchema: MutationOutputSchema,
582
605
  describeCall: (input) => `move ${input.targetId}`,
583
606
  execute: async (input, ctx) => {
584
607
  const opening = await openMutation(ctx);
@@ -596,9 +619,9 @@ export function createReportAuthoringTools(gateway, deps = {}) {
596
619
  });
597
620
  const set_title = defineTool({
598
621
  id: "set_title",
599
- description: "Set the title of the report document. A draft starts with no title, and a document needs one. " +
600
- "The title names the whole report, and it is not the title of a section.",
622
+ description: "Set the title of the whole report, not the title of a section. A draft starts with no title, and a document needs one.",
601
623
  inputSchema: setTitleInput,
624
+ outputSchema: MutationOutputSchema,
602
625
  describeCall: (input) => `title the report ${input.title}`,
603
626
  // The title sits on the document, thus no container changes its child order.
604
627
  execute: async (input, ctx) => {
@@ -624,6 +647,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
624
647
  "A label that ends with the character … is clipped. Use the outline as the primary view of the draft, " +
625
648
  "and read one block when the label is not enough.",
626
649
  inputSchema: readOutlineInput,
650
+ outputSchema: OutlineOutputSchema,
627
651
  // The input carries no field, thus a hook can only restate the name of the tool.
628
652
  describeCall: "none",
629
653
  execute: async (_input, ctx) => {
@@ -640,6 +664,7 @@ export function createReportAuthoringTools(gateway, deps = {}) {
640
664
  "because the session owns the hash. A section gives its title and the id of each child, " +
641
665
  "because the outline already names every block under it. Use it when the outline label is not enough.",
642
666
  inputSchema: readBlockInput,
667
+ outputSchema: ReadBlockOutputSchema,
643
668
  describeCall: (input) => `read ${input.targetId}`,
644
669
  execute: async (input, ctx) => {
645
670
  const opened = await openThread(ctx);
@@ -18,12 +18,15 @@ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
18
18
  import { type ResolveWorkspaceRoot } from "../../workspace/paths.js";
19
19
  import { type Tool } from "../define-tool.js";
20
20
  import { type ReportSessionStateGateway } from "../report-authoring/authoring-tools.js";
21
+ import { type SyncSessionFile } from "../../workspace/session-files.js";
21
22
  export type ReportScriptToolsDeps = {
22
23
  readonly gateway: ReportSessionStateGateway;
23
24
  readonly workspaceFs: WorkspaceFilesystem;
24
25
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
25
26
  readonly derivations: Pick<ReportSessionStateStore, "appendDerivation">;
26
27
  readonly provenance?: ProvenanceSeam;
28
+ /** The host copy of each derived table, owned by the thread of the session. */
29
+ readonly syncSessionFile?: SyncSessionFile;
27
30
  readonly logger?: Logger;
28
31
  };
29
32
  /** The script tools of a report session. */
@@ -24,6 +24,7 @@ import { openReportThread } from "../report-authoring/authoring-tools.js";
24
24
  import { ReadTableOutputSchema, readTableRows, rowsText } from "../script/file-tools.js";
25
25
  import { DERIVED_FILE_CAP_BYTES } from "../script/scratch.js";
26
26
  import { confinedWrite } from "../workspace/mutator.js";
27
+ import { syncWrittenFile } from "../../workspace/session-files.js";
27
28
  function refused(error) {
28
29
  return err({ error, retryable: false });
29
30
  }
@@ -57,7 +58,7 @@ function createReadTableTool(deps) {
57
58
  });
58
59
  if (table.isErr())
59
60
  return refused(table.error);
60
- const hash = computeSha256(table.value.content);
61
+ const hash = computeSha256(table.value.source);
61
62
  if (hash !== entry.hash)
62
63
  return refused(`${path} changed after the session pinned it, thus its bytes are not the evidence of this session`);
63
64
  const { rows } = table.value;
@@ -167,6 +168,14 @@ function createWriteDerivedTool(deps) {
167
168
  if (appended.value === "absent")
168
169
  return refused("no report session state row exists to hold the derivation");
169
170
  observeDerived(analysisId, threadId, record);
171
+ await syncWrittenFile(deps.syncSessionFile, logger, {
172
+ session: ctx.session,
173
+ owner: { kind: "thread", threadId },
174
+ path: outputPath,
175
+ hash: record.outputHash,
176
+ size: landed.bytes.length,
177
+ content: landed.bytes,
178
+ });
170
179
  return ok(derivedOutput(record, landed.bytes.length, columns));
171
180
  },
172
181
  });
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The entries of a compressed file, for the read tools of a script: a zip, a tar, a gzip-compressed tar, or one
3
+ * gzip-compressed file.
4
+ *
5
+ * Each read runs on the host and holds the bytes in memory, thus each one has a cap. A zip names the size of each
6
+ * entry in its index, thus a list reads no entry and a read decompresses one entry. A tar has no index, thus a list
7
+ * and a read of a `.tar.gz` decompress the whole stream, up to {@link ARCHIVE_SCAN_CAP_BYTES}.
8
+ */
9
+ import { type Result } from "neverthrow";
10
+ export type ArchiveKind = "zip" | "tar" | "tar-gzip" | "gzip";
11
+ /** The cap of the decompressed stream of a gzip-compressed tar or file that a list or a read decompresses. */
12
+ export declare const ARCHIVE_SCAN_CAP_BYTES: number;
13
+ export type ArchiveEntry = {
14
+ readonly name: string;
15
+ readonly bytes: number;
16
+ };
17
+ /** Why an entry did not read. `too_large` carries the size of the entry, when the archive names it. */
18
+ export type EntryRefusal = {
19
+ readonly kind: "not_found";
20
+ } | {
21
+ readonly kind: "too_large";
22
+ readonly bytes: number | undefined;
23
+ } | {
24
+ readonly kind: "unparsable";
25
+ readonly detail: string;
26
+ };
27
+ /** The kind of a compressed file by its name, or `undefined` for a file that is not one. */
28
+ export declare function archiveKindOf(path: string): ArchiveKind | undefined;
29
+ /** The one entry of a gzip-compressed file: its name with no `.gz`. */
30
+ export declare function gzipEntryName(path: string): string;
31
+ /** Each file of a compressed file, with its decompressed size. */
32
+ export declare function listArchive(kind: ArchiveKind, path: string, content: Buffer): Result<ArchiveEntry[], EntryRefusal>;
33
+ /** The decompressed bytes of one entry, up to `cap`. */
34
+ export declare function readArchiveEntry(kind: ArchiveKind, path: string, content: Buffer, entry: string, cap: number): Result<Buffer, EntryRefusal>;
@@ -0,0 +1,182 @@
1
+ /**
2
+ * The entries of a compressed file, for the read tools of a script: a zip, a tar, a gzip-compressed tar, or one
3
+ * gzip-compressed file.
4
+ *
5
+ * Each read runs on the host and holds the bytes in memory, thus each one has a cap. A zip names the size of each
6
+ * entry in its index, thus a list reads no entry and a read decompresses one entry. A tar has no index, thus a list
7
+ * and a read of a `.tar.gz` decompress the whole stream, up to {@link ARCHIVE_SCAN_CAP_BYTES}.
8
+ */
9
+ import { basename } from "node:path";
10
+ import { gunzipSync } from "node:zlib";
11
+ import { unzipSync } from "fflate";
12
+ import { err, ok } from "neverthrow";
13
+ /** The cap of the decompressed stream of a gzip-compressed tar or file that a list or a read decompresses. */
14
+ export const ARCHIVE_SCAN_CAP_BYTES = 64 * 1024 * 1024;
15
+ /** The kind of a compressed file by its name, or `undefined` for a file that is not one. */
16
+ export function archiveKindOf(path) {
17
+ const lower = path.toLowerCase();
18
+ if (lower.endsWith(".zip"))
19
+ return "zip";
20
+ if (lower.endsWith(".tar"))
21
+ return "tar";
22
+ if (lower.endsWith(".tar.gz") || lower.endsWith(".tgz"))
23
+ return "tar-gzip";
24
+ if (lower.endsWith(".gz"))
25
+ return "gzip";
26
+ return undefined;
27
+ }
28
+ /** The one entry of a gzip-compressed file: its name with no `.gz`. */
29
+ export function gzipEntryName(path) {
30
+ return basename(path).slice(0, -".gz".length);
31
+ }
32
+ /** The decompressed bytes of a gzip stream, or `too_large` past `cap`. */
33
+ function gunzip(content, cap) {
34
+ try {
35
+ return ok(gunzipSync(content, { maxOutputLength: cap }));
36
+ }
37
+ catch (cause) {
38
+ if (cause instanceof RangeError)
39
+ return err({ kind: "too_large", bytes: undefined });
40
+ return err({ kind: "unparsable", detail: "the gzip stream is damaged" });
41
+ }
42
+ }
43
+ const TAR_BLOCK = 512;
44
+ /** One text field of a tar header: the bytes up to the first NUL. */
45
+ function tarText(block, start, length) {
46
+ const field = block.subarray(start, start + length);
47
+ const end = field.indexOf(0);
48
+ return field.subarray(0, end === -1 ? field.length : end).toString("utf8");
49
+ }
50
+ /** The `path` record of a pax extended header, which names an entry longer than the header field holds. */
51
+ function paxPath(data) {
52
+ for (const line of data.toString("utf8").split("\n")) {
53
+ const record = line.slice(line.indexOf(" ") + 1);
54
+ if (record.startsWith("path="))
55
+ return record.slice("path=".length);
56
+ }
57
+ return undefined;
58
+ }
59
+ /**
60
+ * Each regular file of a tar, with its data. A GNU long name and a pax `path` record rename the entry that follows
61
+ * them. A directory, a link, and each other special entry is no file, thus it is skipped.
62
+ */
63
+ function tarFiles(tar) {
64
+ const files = [];
65
+ let offset = 0;
66
+ let longName;
67
+ while (offset + TAR_BLOCK <= tar.length) {
68
+ const header = tar.subarray(offset, offset + TAR_BLOCK);
69
+ if (header.every((byte) => byte === 0))
70
+ break;
71
+ const size = Number.parseInt(tarText(header, 124, 12).trim() || "0", 8);
72
+ if (!Number.isSafeInteger(size) || size < 0)
73
+ return err({ kind: "unparsable", detail: "a tar header has no valid size" });
74
+ const dataStart = offset + TAR_BLOCK;
75
+ if (dataStart + size > tar.length)
76
+ return err({ kind: "unparsable", detail: "the tar ends inside an entry" });
77
+ const data = tar.subarray(dataStart, dataStart + size);
78
+ const type = String.fromCharCode(header[156] ?? 0);
79
+ const prefix = tarText(header, 257, 6) === "ustar" ? tarText(header, 345, 155) : "";
80
+ const name = longName ?? (prefix === "" ? tarText(header, 0, 100) : `${prefix}/${tarText(header, 0, 100)}`);
81
+ if (type === "L")
82
+ longName = tarText(data, 0, data.length);
83
+ else if (type === "x")
84
+ longName = paxPath(data) ?? longName;
85
+ else {
86
+ longName = undefined;
87
+ if (type === "0" || type === "\0")
88
+ files.push({ name, data });
89
+ }
90
+ offset = dataStart + Math.ceil(size / TAR_BLOCK) * TAR_BLOCK;
91
+ }
92
+ return ok(files);
93
+ }
94
+ /** The tar inside a tar archive: the bytes themselves, or the decompressed stream of a `.tar.gz`. */
95
+ function tarOf(kind, content) {
96
+ return kind === "tar" ? ok(content) : gunzip(content, ARCHIVE_SCAN_CAP_BYTES);
97
+ }
98
+ /** The files of a zip, as its index names them. A directory entry is no file. */
99
+ function zipIndex(content) {
100
+ const entries = [];
101
+ try {
102
+ unzipSync(content, {
103
+ filter: (file) => {
104
+ if (!file.name.endsWith("/"))
105
+ entries.push({ name: file.name, bytes: file.originalSize });
106
+ return false;
107
+ },
108
+ });
109
+ }
110
+ catch {
111
+ return err({ kind: "unparsable", detail: "the zip is damaged" });
112
+ }
113
+ return ok(entries);
114
+ }
115
+ /** Each file of a compressed file, with its decompressed size. */
116
+ export function listArchive(kind, path, content) {
117
+ switch (kind) {
118
+ case "zip":
119
+ return zipIndex(content);
120
+ case "gzip": {
121
+ // The last 4 bytes of a gzip stream hold the decompressed size, modulo 2^32.
122
+ const bytes = content.length >= 4 ? content.readUInt32LE(content.length - 4) : 0;
123
+ return ok([{ name: gzipEntryName(path), bytes }]);
124
+ }
125
+ case "tar":
126
+ case "tar-gzip":
127
+ return tarOf(kind, content)
128
+ .andThen(tarFiles)
129
+ .map((files) => files.map((file) => ({ name: file.name, bytes: file.data.length })));
130
+ default: {
131
+ const unhandled = kind;
132
+ throw new Error(`unhandled archive kind: ${String(unhandled)}`);
133
+ }
134
+ }
135
+ }
136
+ /** The decompressed bytes of one entry, up to `cap`. */
137
+ export function readArchiveEntry(kind, path, content, entry, cap) {
138
+ switch (kind) {
139
+ case "zip": {
140
+ const index = zipIndex(content);
141
+ if (index.isErr())
142
+ return err(index.error);
143
+ const found = index.value.find((file) => file.name === entry);
144
+ if (found === undefined)
145
+ return err({ kind: "not_found" });
146
+ if (found.bytes > cap)
147
+ return err({ kind: "too_large", bytes: found.bytes });
148
+ try {
149
+ const data = unzipSync(content, { filter: (file) => file.name === entry })[entry];
150
+ if (data === undefined)
151
+ return err({ kind: "not_found" });
152
+ // The index can lie about the size, thus the cap holds on the bytes too.
153
+ if (data.length > cap)
154
+ return err({ kind: "too_large", bytes: data.length });
155
+ return ok(Buffer.from(data.buffer, data.byteOffset, data.length));
156
+ }
157
+ catch {
158
+ return err({ kind: "unparsable", detail: `the entry ${entry} did not decompress` });
159
+ }
160
+ }
161
+ case "gzip":
162
+ if (entry !== gzipEntryName(path))
163
+ return err({ kind: "not_found" });
164
+ return gunzip(content, cap);
165
+ case "tar":
166
+ case "tar-gzip": {
167
+ const files = tarOf(kind, content).andThen(tarFiles);
168
+ if (files.isErr())
169
+ return err(files.error);
170
+ const found = files.value.find((file) => file.name === entry);
171
+ if (found === undefined)
172
+ return err({ kind: "not_found" });
173
+ if (found.data.length > cap)
174
+ return err({ kind: "too_large", bytes: found.data.length });
175
+ return ok(found.data);
176
+ }
177
+ default: {
178
+ const unhandled = kind;
179
+ throw new Error(`unhandled archive kind: ${String(unhandled)}`);
180
+ }
181
+ }
182
+ }
@@ -7,7 +7,8 @@
7
7
  * directory of the scratch of the session.
8
8
  *
9
9
  * A table read holds at most 16 MiB, and the parse runs on the host. A larger table, or a format with no parser
10
- * here, is work for an analysis run, and the refusal says so.
10
+ * here, is work for an analysis run, and the refusal says so. `list_archive` lists the files of a compressed file, and
11
+ * a read names one of them as its `entry`. A gzip-compressed table reads with no entry.
11
12
  *
12
13
  * Each tool gives the files that it read or wrote as its call record. `run_script` collects the records of the
13
14
  * calls of one script, thus the provenance of the script chains each derived file to the tables that it read.
@@ -44,9 +45,12 @@ export type ScriptFileToolsDeps = {
44
45
  readonly resolveWorkspaceRoot: ResolveWorkspaceRoot;
45
46
  readonly scratch: ScriptScratch;
46
47
  };
47
- /** The rows of one table, the bytes that they came from, and the columns that they keep. */
48
+ /**
49
+ * The rows of one table, the columns that they keep, and the bytes of the file on disk that held them. A read of an
50
+ * entry names the compressed file, thus its hash is the hash of that file.
51
+ */
48
52
  export type TableRows = {
49
- readonly content: Buffer;
53
+ readonly source: Buffer;
50
54
  readonly columns: string[];
51
55
  readonly rows: Row[];
52
56
  };
@@ -58,6 +62,7 @@ export declare function readTableRows(args: {
58
62
  readonly workspaceFs: WorkspaceFilesystem;
59
63
  readonly session: AgentSession;
60
64
  readonly path: string;
65
+ readonly entry?: string;
61
66
  readonly columns: readonly string[] | undefined;
62
67
  readonly tooLarge: string;
63
68
  }): Promise<Result<TableRows, string>>;
@@ -69,6 +74,7 @@ export declare const ReadTableOutputSchema: z.ZodObject<{
69
74
  }, z.core.$strip>;
70
75
  declare const readPdfInput: z.ZodObject<{
71
76
  path: z.ZodString;
77
+ entry: z.ZodOptional<z.ZodString>;
72
78
  firstPage: z.ZodOptional<z.ZodNumber>;
73
79
  lastPage: z.ZodOptional<z.ZodNumber>;
74
80
  }, z.core.$strip>;
@@ -93,7 +99,7 @@ export type DocumentToolsDeps = ScriptFileToolsDeps & {
93
99
  readonly imageScope?: (session: AgentSession) => string | undefined;
94
100
  };
95
101
  export declare function createScriptFileTools(deps: ScriptFileToolsDeps): Tool[];
96
- /** The script tools that read a table and a PDF of the workspace, and write nothing. */
102
+ /** The script tools that read a table, a PDF, and the files of a compressed file of the workspace, and write nothing. */
97
103
  export declare function createReadScriptTools(deps: Pick<ScriptFileToolsDeps, "workspaceFs" | "resolveWorkspaceRoot">): Tool[];
98
104
  /** The tools that make a document in a script: a chart as an SVG file, and a PDF from markdown. */
99
105
  export declare function createDocumentTools(deps: DocumentToolsDeps): Tool[];