@inflexa-ai/harness 0.24.0 → 0.26.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 (85) hide show
  1. package/dist/agents/sandbox/__fixtures__/deps.js +6 -0
  2. package/dist/agents/sandbox/data-profiler.js +5 -4
  3. package/dist/agents/sandbox/shared.js +11 -0
  4. package/dist/agents/sandbox/types.d.ts +1 -1
  5. package/dist/app/data-profile-orientation.js +30 -1
  6. package/dist/contracts/data-profile.d.ts +166 -0
  7. package/dist/contracts/index.d.ts +1 -0
  8. package/dist/execution/input-signature.d.ts +33 -0
  9. package/dist/execution/input-signature.js +0 -0
  10. package/dist/index.d.ts +5 -2
  11. package/dist/index.js +10 -3
  12. package/dist/input-scan/coverage.d.ts +22 -0
  13. package/dist/input-scan/coverage.js +85 -0
  14. package/dist/input-scan/enrich.d.ts +41 -0
  15. package/dist/input-scan/enrich.js +227 -0
  16. package/dist/input-scan/formats.d.ts +41 -0
  17. package/dist/input-scan/formats.js +279 -0
  18. package/dist/input-scan/index.d.ts +12 -0
  19. package/dist/input-scan/index.js +11 -0
  20. package/dist/input-scan/scan.d.ts +50 -0
  21. package/dist/input-scan/scan.js +182 -0
  22. package/dist/input-scan/shapes.d.ts +46 -0
  23. package/dist/input-scan/shapes.js +0 -0
  24. package/dist/input-scan/types.d.ts +172 -0
  25. package/dist/input-scan/types.js +12 -0
  26. package/dist/lib/page-capture.d.ts +44 -15
  27. package/dist/lib/page-capture.js +78 -19
  28. package/dist/loop/run-agent.js +36 -19
  29. package/dist/memory/card-builders.d.ts +4 -6
  30. package/dist/memory/card-builders.js +4 -6
  31. package/dist/memory/conversation-display-replay.d.ts +7 -6
  32. package/dist/memory/conversation-display-replay.js +7 -6
  33. package/dist/memory/conversation-display-storage.d.ts +2 -10
  34. package/dist/memory/conversation-display-storage.js +79 -32
  35. package/dist/memory/thread-history.d.ts +31 -20
  36. package/dist/memory/thread-history.js +45 -45
  37. package/dist/prompts/report-session.d.ts +1 -1
  38. package/dist/prompts/report-session.js +22 -13
  39. package/dist/prompts/sandbox/data-profiler.d.ts +1 -1
  40. package/dist/prompts/sandbox/data-profiler.js +159 -275
  41. package/dist/provenance/exec-frame.js +9 -3
  42. package/dist/runtime/boot.d.ts +7 -8
  43. package/dist/runtime/boot.js +7 -19
  44. package/dist/sandbox/client.d.ts +7 -0
  45. package/dist/sandbox/create-sandbox.js +1 -0
  46. package/dist/sandbox/docker-client.d.ts +1 -0
  47. package/dist/sandbox/docker-client.js +20 -15
  48. package/dist/sandbox/k8s-client.d.ts +6 -5
  49. package/dist/sandbox/k8s-client.js +55 -39
  50. package/dist/sandbox/reaper.d.ts +24 -13
  51. package/dist/sandbox/reaper.js +47 -14
  52. package/dist/sandbox/types.d.ts +6 -5
  53. package/dist/schemas/data-profile-schemas.d.ts +62 -0
  54. package/dist/schemas/data-profile-schemas.js +68 -2
  55. package/dist/state/data-profile.d.ts +7 -88
  56. package/dist/state/index.d.ts +1 -1
  57. package/dist/tasks/data-profile-activity.d.ts +8 -0
  58. package/dist/tasks/data-profile-activity.js +1 -0
  59. package/dist/tasks/data-profile-index.d.ts +45 -0
  60. package/dist/tasks/data-profile-index.js +134 -0
  61. package/dist/tasks/data-profile.d.ts +8 -23
  62. package/dist/tasks/data-profile.js +150 -117
  63. package/dist/tools/define-tool.d.ts +19 -8
  64. package/dist/tools/define-tool.js +28 -12
  65. package/dist/tools/report-session/examine-page.d.ts +22 -13
  66. package/dist/tools/report-session/examine-page.js +15 -10
  67. package/dist/tools/report-session/index.d.ts +1 -1
  68. package/dist/tools/research/generate-plan.js +3 -8
  69. package/dist/tools/research/inspect-data-profile.d.ts +30 -4
  70. package/dist/tools/research/inspect-data-profile.js +75 -20
  71. package/dist/tools/workspace/scan-inputs.d.ts +38 -0
  72. package/dist/tools/workspace/scan-inputs.js +61 -0
  73. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  74. package/dist/tools/workspace/workspace-search.js +12 -9
  75. package/dist/workspace/filesystem.d.ts +27 -0
  76. package/dist/workspace/filesystem.js +27 -0
  77. package/package.json +1 -1
  78. package/dist/app/data-profile-policy.d.ts +0 -36
  79. package/dist/app/data-profile-policy.js +0 -37
  80. package/dist/memory/content-to-cortex.d.ts +0 -75
  81. package/dist/memory/content-to-cortex.js +0 -201
  82. package/dist/memory/conversation-display-backfill.d.ts +0 -23
  83. package/dist/memory/conversation-display-backfill.js +0 -91
  84. package/dist/memory/reconstruct-cards.d.ts +0 -27
  85. package/dist/memory/reconstruct-cards.js +0 -57
@@ -46,8 +46,34 @@ interface OverviewOutput extends ProfileEnvelope {
46
46
  readonly experimentalDesign?: string;
47
47
  readonly qualityAssessment?: DataProfileResult["qualityAssessment"];
48
48
  readonly accessions?: string[];
49
- /** How many per-file records `scope:"files"` would page through. */
50
- readonly fileCount: number;
49
+ /** How many per-file records `scope:"files"` would page through — NOT the dataset's size. */
50
+ readonly describedFileCount: number;
51
+ /**
52
+ * How many files the dataset holds, or `null` when the snapshot cannot say (one
53
+ * written before kinds and coverage existed). Reported separately from
54
+ * `describedFileCount` because a profile describing eight files of several thousand
55
+ * is correct under this capability, and a single count would read as a dataset of
56
+ * eight files.
57
+ */
58
+ readonly datasetFileCount: number | null;
59
+ /** How many kinds `scope:"kinds"` would return; `null` on a pre-kinds snapshot. */
60
+ readonly kindCount: number | null;
61
+ /** Present when the two counts differ — says where the rest of the dataset is described. */
62
+ readonly structureNote?: string;
63
+ readonly coverage?: DataProfileResult["coverage"];
64
+ }
65
+ /**
66
+ * The dataset's structure. A snapshot predating kinds reports `available: false`
67
+ * rather than an empty list: "this profile predates the structure" and "this dataset
68
+ * has no structure" are different facts, and only one of them is true.
69
+ */
70
+ interface KindsOutput extends ProfileEnvelope {
71
+ readonly scope: "kinds";
72
+ readonly available: boolean;
73
+ readonly kinds?: DataProfileResult["kinds"];
74
+ readonly axes?: DataProfileResult["axes"];
75
+ readonly coverage?: DataProfileResult["coverage"];
76
+ readonly message?: string;
51
77
  }
52
78
  interface FilesOutput extends ProfileEnvelope {
53
79
  readonly scope: "files";
@@ -101,9 +127,9 @@ interface FailedOutput {
101
127
  readonly failedAt: string | null;
102
128
  readonly message: string;
103
129
  }
104
- export type InspectDataProfileOutput = OverviewOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
130
+ export type InspectDataProfileOutput = OverviewOutput | KindsOutput | FilesOutput | AbsentOutput | PendingOutput | FailedOutput;
105
131
  export declare function createInspectDataProfileTool(pool: Pool): import("../define-tool.js").Tool<{
106
- scope?: "files" | "overview" | undefined;
132
+ scope?: "overview" | "files" | "kinds" | undefined;
107
133
  page?: number | undefined;
108
134
  pageSize?: number | undefined;
109
135
  }, InspectDataProfileOutput>;
@@ -22,7 +22,6 @@
22
22
  */
23
23
  import { ok } from "neverthrow";
24
24
  import { z } from "zod";
25
- import { isDataProfileStale } from "../../app/data-profile-policy.js";
26
25
  import { scopeResource } from "../../auth/types.js";
27
26
  import { unwrapOrThrow } from "../../lib/result.js";
28
27
  import { loadDataProfileStatus } from "../../state/index.js";
@@ -31,22 +30,36 @@ import { defineTool } from "../define-tool.js";
31
30
  const DEFAULT_PAGE_SIZE = 20;
32
31
  /** Ceiling on `pageSize` — a page is a context-window budget, not a dump. */
33
32
  const MAX_PAGE_SIZE = 100;
33
+ /**
34
+ * How many files the dataset holds, as far as the snapshot can establish it.
35
+ *
36
+ * Coverage counts the scanned tree, so it is the authority where present. Summed kind
37
+ * counts are the fallback. A snapshot with neither returns `null` rather than the
38
+ * described-file count, which would be a dataset size the row cannot support.
39
+ */
40
+ function datasetFileCount(result) {
41
+ if (result.coverage)
42
+ return result.coverage.total;
43
+ if (result.kinds && result.kinds.length > 0)
44
+ return result.kinds.reduce((sum, kind) => sum + kind.count, 0);
45
+ return null;
46
+ }
34
47
  /**
35
48
  * Name every reason the served profile may not describe the current inputs.
36
49
  *
37
- * Two independent things make a profile stale, and both are ordinary. The input set can
38
- * change under a completed profile (`isDataProfileStale` — the same predicate the
39
- * embedder's re-trigger policy uses; staleness is defined once, in
40
- * `app/data-profile-policy.ts`, not re-derived here). And the ledger can have moved off
41
- * `completed` while an older result is still on the row: `tryRerun`/`tryRetry` preserve
50
+ * Every reason is one the ledger row states outright: the row has moved off `completed`
51
+ * while an older result is still on it, because `tryRerun`/`tryRetry` preserve
42
52
  * `data_profile_result` precisely so a prior profile stays servable while the next
43
53
  * attempt runs or after it fails.
54
+ *
55
+ * A changed input set is deliberately NOT among them. Re-profiling is invoked by the
56
+ * embedder that owns the input mutation — it knows the set changed at the moment it
57
+ * changed — so a row still reading `completed` is a row nothing has superseded. Deriving
58
+ * a verdict here would mean re-deciding, from one table row, a question already answered
59
+ * by the party that watched it happen.
44
60
  */
45
- function stalenessReasons(status, result) {
61
+ function stalenessReasons(status) {
46
62
  const reasons = [];
47
- if (isDataProfileStale(status.seedInputFileIds ?? [], result.inputFileIds)) {
48
- reasons.push("the analysis's input file set changed after this profile was taken");
49
- }
50
63
  if (status.status === "running" || status.status === "pending") {
51
64
  reasons.push("a re-profile is in progress — this is the previous profile");
52
65
  }
@@ -69,14 +82,20 @@ export function createInspectDataProfileTool(pool) {
69
82
  id: "inspect_data_profile",
70
83
  description: "Read this analysis's data profile — the AUTHORITATIVE record of what the input dataset is: " +
71
84
  "organism and taxon id, scientific domain and subtype, tissue, cell type, condition, experimental design, " +
72
- "dataset-wide quality concerns and strengths, public accessions, and per-file data type, format, " +
73
- "row/column dimensions, tags, warnings, and profiling metrics. " +
85
+ "dataset-wide quality concerns and strengths, public accessions, the KINDS of file the dataset is made of " +
86
+ "and what varies across them, and per-file data type, format, row/column dimensions, tags and warnings for " +
87
+ "the files described individually. " +
74
88
  "There is NO data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
75
89
  "and do not rediscover these facts by listing or reading the raw input files. " +
76
90
  "Call it before you reason about the data (planning, writing analysis code, interpreting results). " +
77
- "scope:'overview' (the default) returns the dataset-level orientation plus the number of profiled files. " +
78
- "scope:'files' returns the per-file records, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
91
+ "scope:'overview' (the default) returns the dataset-level orientation, how many files are described individually, " +
92
+ "and how many files the dataset holds — those two differ on purpose, because a large dataset is described by its " +
93
+ "kinds rather than file by file. " +
94
+ "scope:'kinds' returns those kinds with their counts, path patterns, and axes — this is where a dataset of thousands " +
95
+ "of files is described. A profile taken before kinds existed reports the scope unavailable. " +
96
+ "scope:'files' returns the individually described files, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
79
97
  "always with the true total and hasMore, so you can see exactly what you have not read yet. " +
98
+ "For the paths of files no record describes, list the workspace tree. " +
80
99
  "The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
81
100
  "inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
82
101
  "or the analysis has no input files). " +
@@ -86,9 +105,11 @@ export function createInspectDataProfileTool(pool) {
86
105
  "needs re-running rather than diagnosing the data itself.",
87
106
  inputSchema: z.object({
88
107
  scope: z
89
- .enum(["overview", "files"])
108
+ .enum(["overview", "kinds", "files"])
90
109
  .optional()
91
- .describe("'overview' (default): dataset-level facts + file count. 'files': paged per-file records."),
110
+ .describe("'overview' (default): dataset-level facts, described-file count, and dataset file count. " +
111
+ "'kinds': the sets the dataset is made of and what varies across them. " +
112
+ "'files': paged records for the individually described files."),
92
113
  page: z.number().int().min(1).optional().describe("1-based page of per-file records. Only used when scope is 'files'. Default 1."),
93
114
  pageSize: z
94
115
  .number()
@@ -101,7 +122,7 @@ export function createInspectDataProfileTool(pool) {
101
122
  // `execute` defaults both fields internally, and the hook applies the same
102
123
  // defaults. Without them the ordinary call, which names no field at all,
103
124
  // gives no detail. `page` acts only on the `files` scope, as in `execute`.
104
- describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : "overview"),
125
+ describeCall: ({ scope, page }) => ((scope ?? "overview") === "files" ? `files (page ${page ?? 1})` : (scope ?? "overview")),
105
126
  execute: async (input, ctx) => {
106
127
  const resourceId = scopeResource(ctx.session.scope).resourceId;
107
128
  const status = unwrapOrThrow(await loadDataProfileStatus(pool, resourceId));
@@ -141,11 +162,21 @@ export function createInspectDataProfileTool(pool) {
141
162
  message: "Data profiling is still running; no profile is available yet. Proceed without it or ask the user to wait.",
142
163
  });
143
164
  }
144
- const reasons = stalenessReasons(status, result);
165
+ const reasons = stalenessReasons(status);
145
166
  const envelope = reasons.length > 0
146
167
  ? { state: "stale", staleReason: reasons.join("; "), profiledAt: result.profiledAt }
147
168
  : { state: "ready", profiledAt: result.profiledAt };
148
- if ((input.scope ?? "overview") === "overview") {
169
+ const scope = input.scope ?? "overview";
170
+ if (scope === "overview") {
171
+ const dataset = datasetFileCount(result);
172
+ const described = result.files.length;
173
+ const structureNote = dataset === null
174
+ ? "This profile predates the dataset-structure record, so only the individually described files are known; " +
175
+ "list the workspace tree for the rest."
176
+ : dataset !== described
177
+ ? `${described} of ${dataset} files are described individually. Call scope:'kinds' for what the rest are, ` +
178
+ "and list the workspace tree for their paths."
179
+ : undefined;
149
180
  return ok({
150
181
  ...envelope,
151
182
  scope: "overview",
@@ -159,7 +190,31 @@ export function createInspectDataProfileTool(pool) {
159
190
  experimentalDesign: result.experimentalDesign,
160
191
  qualityAssessment: result.qualityAssessment,
161
192
  accessions: result.accessions,
162
- fileCount: result.files.length,
193
+ describedFileCount: described,
194
+ datasetFileCount: dataset,
195
+ kindCount: result.kinds ? result.kinds.length : null,
196
+ ...(structureNote ? { structureNote } : {}),
197
+ ...(result.coverage ? { coverage: result.coverage } : {}),
198
+ });
199
+ }
200
+ if (scope === "kinds") {
201
+ if (!result.kinds) {
202
+ return ok({
203
+ ...envelope,
204
+ scope: "kinds",
205
+ available: false,
206
+ message: "This profile was taken before the dataset-structure record existed, so it carries no kinds. " +
207
+ "That is a fact about the profile, not about the dataset: use scope:'files' and the workspace " +
208
+ "listing tools, or re-profile the analysis.",
209
+ });
210
+ }
211
+ return ok({
212
+ ...envelope,
213
+ scope: "kinds",
214
+ available: true,
215
+ kinds: result.kinds,
216
+ ...(result.axes ? { axes: result.axes } : {}),
217
+ ...(result.coverage ? { coverage: result.coverage } : {}),
163
218
  });
164
219
  }
165
220
  const pageSize = input.pageSize ?? DEFAULT_PAGE_SIZE;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * `scan_inputs` tool — re-run the deterministic input scan over a subtree.
3
+ *
4
+ * The data-profile body runs the scan once before the agent loop and injects the
5
+ * manifest into the briefing, because the scan is always needed and its result does
6
+ * not depend on agent judgement. This tool is the other half: an agent that disagrees
7
+ * with the observed shapes, or wants a directory the injected manifest summarised, can
8
+ * get the evidence to group differently.
9
+ *
10
+ * Dependency-bearing factory. The walk and the shape observation run in-process over
11
+ * the workspace read seam; only the per-shape header readout reaches into the sandbox
12
+ * (see `input-scan/enrich.ts`).
13
+ */
14
+ import type { SandboxClient } from "../../sandbox/client.js";
15
+ import type { SandboxRef } from "../../sandbox/types.js";
16
+ import type { WorkspaceFilesystem } from "../../workspace/filesystem.js";
17
+ export interface ScanInputsDeps {
18
+ readonly workspaceFs: WorkspaceFilesystem;
19
+ readonly analysisId: string;
20
+ /** Omit to run the host-side scan with no header readout (no sandbox wired). */
21
+ readonly sandboxClient?: SandboxClient;
22
+ readonly sandbox?: SandboxRef;
23
+ readonly workflowId: string;
24
+ readonly stepId: string;
25
+ readonly nextFunctionId: () => string;
26
+ readonly deadlineMs: () => number;
27
+ }
28
+ type ScanInputsOutput = {
29
+ readonly path: string;
30
+ readonly fileCount: number;
31
+ readonly shapeCount: number;
32
+ /** The rendered manifest — bounded whatever the subtree's size. */
33
+ readonly manifest: string;
34
+ };
35
+ export declare function createScanInputsTool(deps: ScanInputsDeps): import("../define-tool.js").Tool<{
36
+ path: string;
37
+ }, ScanInputsOutput>;
38
+ export {};
@@ -0,0 +1,61 @@
1
+ /**
2
+ * `scan_inputs` tool — re-run the deterministic input scan over a subtree.
3
+ *
4
+ * The data-profile body runs the scan once before the agent loop and injects the
5
+ * manifest into the briefing, because the scan is always needed and its result does
6
+ * not depend on agent judgement. This tool is the other half: an agent that disagrees
7
+ * with the observed shapes, or wants a directory the injected manifest summarised, can
8
+ * get the evidence to group differently.
9
+ *
10
+ * Dependency-bearing factory. The walk and the shape observation run in-process over
11
+ * the workspace read seam; only the per-shape header readout reaches into the sandbox
12
+ * (see `input-scan/enrich.ts`).
13
+ */
14
+ import { ok } from "neverthrow";
15
+ import { z } from "zod";
16
+ import { enrichShapes } from "../../input-scan/enrich.js";
17
+ import { renderInputScanManifest, scanInputTree } from "../../input-scan/scan.js";
18
+ import { defineTool } from "../define-tool.js";
19
+ export function createScanInputsTool(deps) {
20
+ return defineTool({
21
+ id: "scan_inputs",
22
+ description: "Scan a directory of input files and report what is OBSERVABLE about it: per-format counts, " +
23
+ "the shapes its filenames form (sets of files whose names differ only at marked positions), " +
24
+ "the distinct values each varying position takes, how those positions co-occur, value overlap " +
25
+ "between shapes, and the files that share structure with nothing else. " +
26
+ "Deterministic — no model, no per-file decoding — so it costs the same on three files as on three thousand. " +
27
+ "Your briefing already carries a scan of the whole input tree; call this to look at a subtree more closely, " +
28
+ "or to check a grouping you are unsure about. " +
29
+ "It reports observations only: which files are one KIND of data, and what a varying position MEANS, are yours to decide.",
30
+ inputSchema: z.object({
31
+ path: z.string().min(1).describe("Directory to scan, relative to the analysis root (e.g. 'data/inputs' or 'data/inputs/vcf')."),
32
+ }),
33
+ describeCall: ({ path }) => path,
34
+ execute: async ({ path }, ctx) => {
35
+ const scan = await scanInputTree({
36
+ session: ctx.session,
37
+ fs: deps.workspaceFs,
38
+ root: path.replace(/^\/+/, "").replace(/\/+$/, ""),
39
+ signal: ctx.signal,
40
+ });
41
+ const shapes = deps.sandboxClient && deps.sandbox
42
+ ? await enrichShapes({
43
+ shapes: scan.manifest.shapes,
44
+ sandboxClient: deps.sandboxClient,
45
+ sandbox: deps.sandbox,
46
+ mountRoot: `/${deps.analysisId}`,
47
+ execId: `${deps.workflowId}:${deps.stepId}:${deps.nextFunctionId()}`,
48
+ deadlineMs: deps.deadlineMs(),
49
+ emit: ctx.emit,
50
+ })
51
+ : scan.manifest.shapes;
52
+ const manifest = { ...scan.manifest, shapes };
53
+ return ok({
54
+ path: manifest.root,
55
+ fileCount: manifest.fileCount,
56
+ shapeCount: manifest.shapes.length,
57
+ manifest: renderInputScanManifest(manifest),
58
+ });
59
+ },
60
+ });
61
+ }
@@ -15,7 +15,7 @@ import type { EmbeddingProvider } from "../../providers/types.js";
15
15
  export declare function createWorkspaceSearchTool(pool: Pool, embedding: EmbeddingProvider): import("../define-tool.js").Tool<{
16
16
  query: string;
17
17
  limit: number;
18
- type?: "output" | "input" | "synthesis" | "summary" | undefined;
18
+ type?: "output" | "input" | "synthesis" | "summary" | "input-kind" | undefined;
19
19
  }, {
20
20
  results: {
21
21
  id: string;
@@ -32,19 +32,22 @@ function indexName(analysisId) {
32
32
  export function createWorkspaceSearchTool(pool, embedding) {
33
33
  return defineTool({
34
34
  id: "workspace_search",
35
- description: "Semantic search over the analysis workspace. Returns ranked file paths " +
36
- "with descriptions and metadata; read a file separately to see its " +
37
- "contents. Indexed entries are exactly four types, and `type` restricts " +
38
- 'results to one of them: "input" (staged input data files — their ' +
39
- 'descriptions come from the data profiler), "output" (files a step ' +
40
- 'produced), "summary" (a step\'s summary), "synthesis" (a run\'s ' +
41
- "literature-grounded synthesis).",
35
+ description: "Semantic search over the analysis workspace. Returns ranked entries with descriptions " +
36
+ "and metadata; read a file separately to see its contents. Indexed entries are exactly " +
37
+ 'five types, and `type` restricts results to one of them: "input-kind" (a KIND of input ' +
38
+ "data — a repeating set the data profiler named, carrying its count and a pathPattern " +
39
+ 'matching its members), "input" (one entity the dataset is about — a subject, sample, or ' +
40
+ 'other axis value — with the kinds of file it carries), "output" (files a step produced), ' +
41
+ '"summary" (a step\'s summary), "synthesis" (a run\'s literature-grounded synthesis). ' +
42
+ "For output/summary/synthesis the entry id IS the workspace path; for the two input types " +
43
+ "it is not, because a kind and an entity each span many files — search input-kind for a " +
44
+ "set's pathPattern, then list the tree for its members.",
42
45
  inputSchema: z.object({
43
46
  query: z.string().min(1).describe("What to search for, in natural language"),
44
47
  type: z
45
- .enum(["input", "output", "summary", "synthesis"])
48
+ .enum(["input-kind", "input", "output", "summary", "synthesis"])
46
49
  .optional()
47
- .describe('Restrict results to one entry type: "input", "output", "summary", or "synthesis"'),
50
+ .describe('Restrict results to one entry type: "input-kind", "input", "output", "summary", or "synthesis"'),
48
51
  limit: z.number().int().min(1).max(50).default(8).describe("Maximum number of results to return"),
49
52
  }),
50
53
  describeCall: ({ query, type }) => (type === undefined ? query : `${query} (${type})`),
@@ -67,6 +67,15 @@ export type StatResult = {
67
67
  } | {
68
68
  readonly kind: "out_of_scope";
69
69
  };
70
+ /** A bounded prefix of a file's raw bytes. `bytes` may be shorter than requested. */
71
+ export type ReadBytesResult = {
72
+ readonly kind: "ok";
73
+ readonly bytes: Buffer;
74
+ } | {
75
+ readonly kind: "not_found";
76
+ } | {
77
+ readonly kind: "out_of_scope";
78
+ };
70
79
  export interface WorkspaceFilesystem {
71
80
  /**
72
81
  * Read a workspace file. `headLines` / `tailLines` (mutually exclusive,
@@ -86,6 +95,24 @@ export interface WorkspaceFilesystem {
86
95
  /** Absolute host base for relative paths. Defaults to the analysis root. */
87
96
  readonly workingDir?: string;
88
97
  }): ResultAsync<ReadFileResult, FsError>;
98
+ /**
99
+ * Read the first `length` bytes of a file, raw.
100
+ *
101
+ * Distinct from `readFile` because that surface is line- and text-oriented — it
102
+ * decodes to UTF-8 for its line windows and its byte cap is an outer bound on a
103
+ * whole-file read, not a prefix request. Magic-byte format detection needs an
104
+ * exact, small prefix of the raw bytes and nothing else.
105
+ *
106
+ * Local files only: a prefix of a presigned object would mean fetching the whole
107
+ * object to read its first 512 bytes, which defeats the bound. An absent local
108
+ * file is `not_found`.
109
+ */
110
+ readBytes(args: {
111
+ readonly session: AgentSession;
112
+ readonly path: string;
113
+ readonly length: number;
114
+ readonly workingDir?: string;
115
+ }): ResultAsync<ReadBytesResult, FsError>;
89
116
  list(args: {
90
117
  readonly session: AgentSession;
91
118
  readonly path: string;
@@ -117,6 +117,33 @@ export function createWorkspaceFilesystem(deps) {
117
117
  });
118
118
  });
119
119
  },
120
+ readBytes({ session, path, length, workingDir }) {
121
+ return confinedResolve(session, path, workingDir).andThen((resolved) => {
122
+ if (resolved.kind === "out_of_scope")
123
+ return okAsync({ kind: "out_of_scope" });
124
+ const absolute = resolved.absolute;
125
+ return safeStat(absolute).andThen((localStat) => {
126
+ if (!localStat?.isFile())
127
+ return okAsync({ kind: "not_found" });
128
+ return openReadNoFollow(absolute).andThen((opened) => {
129
+ if (opened.kind === "out_of_scope")
130
+ return okAsync({ kind: "out_of_scope" });
131
+ const fh = opened.fh;
132
+ return tryFs("workspace.readBytes", async () => {
133
+ try {
134
+ const want = Math.max(0, Math.min(length, localStat.size));
135
+ const buf = Buffer.alloc(want);
136
+ const { bytesRead } = want > 0 ? await fh.read(buf, 0, want, 0) : { bytesRead: 0 };
137
+ return { kind: "ok", bytes: buf.subarray(0, bytesRead) };
138
+ }
139
+ finally {
140
+ await fh.close();
141
+ }
142
+ }, { path: absolute });
143
+ });
144
+ });
145
+ });
146
+ },
120
147
  list({ session, path, workingDir }) {
121
148
  return confinedResolve(session, path, workingDir).andThen((resolved) => {
122
149
  if (resolved.kind === "out_of_scope") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "Host-agnostic bioinformatics agent harness — hand-rolled agent loop, DBOS-durable workflows, sandboxed compute.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -1,36 +0,0 @@
1
- /**
2
- * Data-profile policy — the pure decision an embedder applies when it reads a
3
- * data-profile status (see CONTEXT.md "Application service layer"). It decides
4
- * whether to (re)trigger profiling; the embedder executes the staging + trigger
5
- * — staging stays embedder-side per the data-profile-init spec, and the atomic running→expired
6
- * flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
7
- */
8
- export type DataProfileLifecycleStatus = "pending" | "running" | "completed" | "failed";
9
- /**
10
- * A completed profile is stale when the analysis's seed input set no longer
11
- * matches the set the profile actually covered — a file was added or swapped
12
- * since profiling. An empty seed set is never stale (nothing to compare).
13
- */
14
- export declare function isDataProfileStale(seedInputFileIds: readonly string[], profiledInputFileIds: readonly string[]): boolean;
15
- export type DataProfileAction = {
16
- readonly kind: "none";
17
- } | {
18
- readonly kind: "trigger";
19
- } | {
20
- readonly kind: "retrigger";
21
- };
22
- export interface DataProfileDecisionInput {
23
- readonly status: DataProfileLifecycleStatus;
24
- readonly seedInputFileIds: readonly string[];
25
- readonly profiledInputFileIds: readonly string[];
26
- }
27
- /**
28
- * Decide the next profiling action from a (post-expire) status:
29
- * - `pending` → `trigger` (first profile)
30
- * - `completed` && stale → `retrigger` (inputs changed since profiling)
31
- * - anything else → `none`
32
- *
33
- * `running` is left to finish (or to the SQL expiry guard); `failed` is retried
34
- * explicitly via the retry route, never implicitly on read.
35
- */
36
- export declare function decideDataProfileAction(input: DataProfileDecisionInput): DataProfileAction;
@@ -1,37 +0,0 @@
1
- /**
2
- * Data-profile policy — the pure decision an embedder applies when it reads a
3
- * data-profile status (see CONTEXT.md "Application service layer"). It decides
4
- * whether to (re)trigger profiling; the embedder executes the staging + trigger
5
- * — staging stays embedder-side per the data-profile-init spec, and the atomic running→expired
6
- * flip stays an SQL guard (`expireStaleDataProfile`), not part of this decision.
7
- */
8
- /**
9
- * A completed profile is stale when the analysis's seed input set no longer
10
- * matches the set the profile actually covered — a file was added or swapped
11
- * since profiling. An empty seed set is never stale (nothing to compare).
12
- */
13
- export function isDataProfileStale(seedInputFileIds, profiledInputFileIds) {
14
- if (seedInputFileIds.length === 0)
15
- return false;
16
- if (seedInputFileIds.length !== profiledInputFileIds.length)
17
- return true;
18
- const profiled = new Set(profiledInputFileIds);
19
- return seedInputFileIds.some((id) => !profiled.has(id));
20
- }
21
- /**
22
- * Decide the next profiling action from a (post-expire) status:
23
- * - `pending` → `trigger` (first profile)
24
- * - `completed` && stale → `retrigger` (inputs changed since profiling)
25
- * - anything else → `none`
26
- *
27
- * `running` is left to finish (or to the SQL expiry guard); `failed` is retried
28
- * explicitly via the retry route, never implicitly on read.
29
- */
30
- export function decideDataProfileAction(input) {
31
- if (input.status === "pending")
32
- return { kind: "trigger" };
33
- if (input.status === "completed" && isDataProfileStale(input.seedInputFileIds, input.profiledInputFileIds)) {
34
- return { kind: "retrigger" };
35
- }
36
- return { kind: "none" };
37
- }
@@ -1,75 +0,0 @@
1
- /**
2
- * The MIGRATION renderer: stored AI SDK model messages → `CortexMessage[]`.
3
- *
4
- * This is how a conversation written before display projections were persisted
5
- * is rendered ONCE, at startup, so it can be frozen into a display envelope
6
- * (`conversation-display-backfill.ts`). It is not a runtime read path — the
7
- * runtime reads the stored projection (`conversation-display-replay.ts`) and
8
- * never consults a tool name, a card builder, or the filesystem.
9
- *
10
- * Everything it does is inference from a transcript that was never meant to
11
- * carry display, which is why it belongs to the migration and nothing else:
12
- *
13
- * - text part → text part
14
- * - tool-call part → a reconstructed display card (`data-plan`,
15
- * `data-presentation`) when `resolveCard` recognises
16
- * the tool, else a generic tool-call part whose detail
17
- * `resolveDetail` recomputes from the persisted input
18
- * - everything else → dropped (reasoning, tool-result, etc.) — the UI
19
- * does not render them
20
- *
21
- * A `tool-result` block is not rendered, but it is not ignored either: it is the
22
- * only record of how its call ended. The blocks are indexed by `toolCallId`
23
- * before conversion so each tool-call part can report its own outcome — without
24
- * that pass every migrated call would report success, including the failed ones.
25
- *
26
- * A row that yields no renderable parts (e.g. a `tool`-role message carrying
27
- * only tool-result continuation parts) is omitted entirely, so the rendered
28
- * conversation has no empty bubbles. Storage is never mutated.
29
- */
30
- import type { CortexMessage } from "@inflexa-ai/harness/contracts/message.js";
31
- import type { ToolDetailResolver } from "../tools/detail-resolver.js";
32
- import type { ToolCardResolver } from "./reconstruct-cards.js";
33
- import type { StoredMessage } from "./thread-history.js";
34
- /**
35
- * The resolvers a caller may supply, as one object.
36
- *
37
- * An options bag rather than a growing positional list: both are optional, both
38
- * are function-typed, and a second positional parameter of the same shape is a
39
- * call site that silently does the wrong thing when the two are swapped.
40
- */
41
- export interface ContentToCortexOptions {
42
- /** Rebuilds a display card (`data-plan`, `data-presentation`) from a persisted tool call. */
43
- readonly resolveCard?: ToolCardResolver;
44
- /** Rebuilds a call's one-line detail from its tool name and persisted input. */
45
- readonly resolveDetail?: ToolDetailResolver;
46
- }
47
- /**
48
- * Map stored messages (oldest-first) to `CortexMessage[]`. Rows that produce
49
- * no renderable parts are dropped, and consecutive assistant rows are coalesced
50
- * into one message. The first row's `seq` becomes the stable message id.
51
- * `options.resolveCard` reconstructs display cards from `tool_use` blocks and
52
- * `options.resolveDetail` reconstructs each call's one-line detail; both are
53
- * optional and each tool-call part reports its outcome either way.
54
- * A row carrying the interruption marker sets `interrupted: true` on the message
55
- * it lands in, including when coalesced into an assistant run; a row carrying a
56
- * stored turn rollup sets `usage` the same way. A row that carries a stored turn
57
- * duration sets `durationMs` the same way too. Thus a reloaded conversation shows
58
- * the cost and the time that the live turn showed, with no second query.
59
- *
60
- * Coalescing mirrors the live SSE shape. The agent loop persists each assistant
61
- * step (typically a single `tool_use`) as its own row, and the `tool_result`
62
- * `user` rows between them drop to zero renderable parts above — so one assistant
63
- * turn is stored as a run of adjacent assistant rows. The live chat stream
64
- * accumulates that whole turn into a single assistant message whose `parts` hold
65
- * every step; reconstructing it row-by-row instead would emit one message per
66
- * tool call, which the UI renders as a stack of single-tool boxes. Merging the
67
- * run restores the one-bubble-per-turn shape (and lets the UI collapse the tool
68
- * calls into one group) regardless of how the loop split the turn into rows.
69
- *
70
- * Coalescing is assistant-only. User rows never legitimately split into a run —
71
- * adjacent `user` rows arise only from turns that persisted no reply (an aborted
72
- * turn's lone user message followed by the next turn's), and merging them would
73
- * fabricate one bubble from two messages the user sent separately.
74
- */
75
- export declare function contentToCortexMessages(messages: readonly StoredMessage[], options?: ContentToCortexOptions): Promise<CortexMessage[]>;