@inflexa-ai/harness 0.40.0 → 0.41.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 (62) hide show
  1. package/dist/app/chat-turn.d.ts +42 -13
  2. package/dist/app/chat-turn.js +47 -16
  3. package/dist/index.d.ts +2 -2
  4. package/dist/index.js +1 -1
  5. package/dist/prompts/briefing.d.ts +28 -1
  6. package/dist/prompts/briefing.js +44 -1
  7. package/dist/prompts/compaction.d.ts +2 -2
  8. package/dist/prompts/compaction.js +9 -5
  9. package/dist/prompts/sandbox-standards.d.ts +1 -1
  10. package/dist/prompts/sandbox-standards.js +4 -0
  11. package/dist/runtime/assemble.d.ts +4 -2
  12. package/dist/runtime/assemble.js +8 -1
  13. package/dist/schemas/file-metadata.js +9 -9
  14. package/dist/schemas/plan-schemas.d.ts +3 -3
  15. package/dist/tools/ad-hoc-router.js +9 -4
  16. package/dist/tools/bio/chembl.js +8 -5
  17. package/dist/tools/bio/comptox.js +4 -4
  18. package/dist/tools/bio/drug-gene-interactions.js +5 -4
  19. package/dist/tools/bio/gene-disease-evidence.js +4 -4
  20. package/dist/tools/bio/lookup-annotation.js +7 -3
  21. package/dist/tools/bio/opentargets.js +8 -6
  22. package/dist/tools/bio/pubchem.js +4 -2
  23. package/dist/tools/bio/pubmed.d.ts +1 -1
  24. package/dist/tools/bio/pubmed.js +10 -6
  25. package/dist/tools/bio/search-clinical-trials.js +2 -1
  26. package/dist/tools/bio/search-gene.js +2 -1
  27. package/dist/tools/bio/search-interactions.js +8 -4
  28. package/dist/tools/bio/target-safety.js +2 -1
  29. package/dist/tools/display/show-user.js +1 -1
  30. package/dist/tools/execute-analysis.js +14 -4
  31. package/dist/tools/lib/opentargets-client.d.ts +8 -3
  32. package/dist/tools/lib/opentargets-client.js +12 -3
  33. package/dist/tools/read-tool-output.js +7 -4
  34. package/dist/tools/report-authoring/authoring-tools.js +22 -15
  35. package/dist/tools/report-session/derive-table.js +5 -1
  36. package/dist/tools/report-session/examine-page.js +5 -4
  37. package/dist/tools/report-session/preview-report.js +6 -1
  38. package/dist/tools/report-session/record-version.js +2 -1
  39. package/dist/tools/research/context7-docs.js +8 -2
  40. package/dist/tools/research/generate-analogy-report.js +15 -15
  41. package/dist/tools/research/generate-plan.js +12 -7
  42. package/dist/tools/research/inspect-data-profile.js +7 -5
  43. package/dist/tools/research/inspect-run.js +6 -2
  44. package/dist/tools/research/literature-reviewer.js +11 -6
  45. package/dist/tools/research/search-arxiv.js +2 -1
  46. package/dist/tools/research/search-github-repos.js +9 -3
  47. package/dist/tools/sandbox/list-available-refs.js +3 -2
  48. package/dist/tools/sandbox/report-blocker.js +6 -1
  49. package/dist/tools/sandbox/skills.js +4 -2
  50. package/dist/tools/start-report-session.js +4 -1
  51. package/dist/tools/workspace/edit-file.js +4 -3
  52. package/dist/tools/workspace/execute-command.js +6 -5
  53. package/dist/tools/workspace/grep.js +9 -7
  54. package/dist/tools/workspace/list-files.js +1 -1
  55. package/dist/tools/workspace/read-file.js +2 -1
  56. package/dist/tools/workspace/scan-inputs.js +3 -1
  57. package/dist/tools/workspace/show-file.js +2 -0
  58. package/dist/tools/workspace/workspace-search.js +3 -1
  59. package/dist/tools/workspace/write-file.js +5 -3
  60. package/dist/workflows/execute-analysis.d.ts +28 -5
  61. package/dist/workflows/execute-analysis.js +34 -10
  62. package/package.json +1 -1
@@ -39,7 +39,7 @@ function createSubmitAnalogyReportTool(cell) {
39
39
  "relations, and key terms of phase 1, and each analogy with its " +
40
40
  "coverage and its cited solutions from phase 2. Call it one time with " +
41
41
  "the full report. If it rejects the report, correct the fields that it " +
42
- "names and call it again. Stop immediately after an accepted call.",
42
+ "names and call it again. An accepted call ends the run.",
43
43
  inputSchema: AnalogyReportSchema,
44
44
  describeCall: "none",
45
45
  execute: async (report) => {
@@ -47,7 +47,7 @@ function createSubmitAnalogyReportTool(cell) {
47
47
  cell.outcome = { kind: "report", report };
48
48
  return ok({
49
49
  recorded: true,
50
- message: "Report recorded. You are done — stop now and do not take further actions.",
50
+ message: "Report recorded.",
51
51
  });
52
52
  },
53
53
  });
@@ -135,25 +135,25 @@ export function createGenerateAnalogyReportTool(deps) {
135
135
  description: "Idea-generation engine. Extracts structural analogies for an open-ended " +
136
136
  "scientific problem and returns real, cited solutions from other fields " +
137
137
  "(control theory, ML, ecology, logistics, economics…).\n" +
138
- "WHEN: any exploratory or hypothesis-generation turn — 'what could we do " +
139
- "about X', 'what should we try', 'how could we approach this', 'we're " +
140
- "stuck', 'brainstorm ideas', 'are there precedents outside biology'. The " +
141
- "trigger is exploratory INTENT, not the words 'analogy' or 'cross-domain' " +
142
- "— users rarely name it, so infer it and reach for the tool proactively.\n" +
138
+ "WHEN: an exploratory or hypothesis-generation turn — the user asks how " +
139
+ "to approach a problem, what to try, or whether precedents exist outside " +
140
+ "biology. The intent decides, whether or not the user names analogy or " +
141
+ "cross-domain search.\n" +
143
142
  "NOT for: in-domain literature review (use `pubmed` and the other " +
144
143
  "bio-lookup tools), single-gene factual lookup (use `search_gene`), or " +
145
144
  "execution-mode turns where the user just wants the next step done. It " +
146
- "drives a multi-step research sub-agent over live literature search, so " +
147
- "it is slow — never spend it on a fact you could look up.\n" +
145
+ "drives a multi-step research sub-agent (up to 40 model turns) over live " +
146
+ "literature search, so it is slow; a fact that one lookup answers does " +
147
+ "not need it.\n" +
148
148
  "RETURNS an AnalogyReport envelope the UI renders as an inline card. An " +
149
149
  "analogy the search could not fill carries a `coverage` tag " +
150
150
  "(`queried_no_data` | `search_failed` | `not_loaded`) with an empty " +
151
- "`solutions` array — informational, NOT an error. A top-level `error` " +
152
- "field means no report was produced: on `error.kind === " +
153
- '"extraction-failed"` do NOT retry with the same or a similar problem ' +
154
- "statement (the wrapper already salvaged the run one time — you would " +
155
- "burn latency on the same failure). Surface the message and ask the " +
156
- "user to narrow the problem.",
151
+ "`solutions` array — a partial result, not an error. A top-level `error` " +
152
+ 'field with `kind: "extraction-failed"` means no report was produced: the ' +
153
+ "sub-agent found the problem empty or incoherent, failed, or ended with " +
154
+ "no report after one corrective request. `error.message` gives the cause. " +
155
+ "A retry with the same or a similar problem statement meets the same " +
156
+ "failure, thus surface the message and ask the user to narrow the problem.",
157
157
  inputSchema: generateAnalogyReportInputSchema,
158
158
  describeCall: "none",
159
159
  execute: async (input, ctx) => {
@@ -340,7 +340,8 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
340
340
  const submitPlanTool = defineTool({
341
341
  id: "submit_plan",
342
342
  description: "Submit the final plan for persistence. Re-validates the plan; on " +
343
- "success returns {accepted: true, planId} — STOP after this. On " +
343
+ "success returns {accepted: true, planId}, and an accepted call ends " +
344
+ "the planning session. On " +
344
345
  "rejection returns {accepted: false, issues} — fix and call again, " +
345
346
  "or switch to report_blocker if the plan cannot be made valid. This " +
346
347
  "arg schema is the authoritative plan contract.",
@@ -421,11 +422,12 @@ function buildInnerTools(holder, trace, persistCtx, pool, resourcePolicy, logger
421
422
  const requestClarificationTool = defineTool({
422
423
  id: "request_clarification",
423
424
  description: "Terminal. Use when a specific fact you need is missing from the " +
424
- "input and cannot be inferred. Pass a short question and optional " +
425
- "context. Stop after calling.",
425
+ "input and cannot be inferred. The question reaches the user through " +
426
+ "the conversation agent. A call ends the planning session with no " +
427
+ "plan saved.",
426
428
  inputSchema: z.object({
427
- question: z.string().min(1),
428
- questionContext: z.string().optional(),
429
+ question: z.string().min(1).describe("One short question that names each missing fact, and the step that needs it."),
430
+ questionContext: z.string().optional().describe("Optional context: why the plan needs the fact, and what the inputs already say about it."),
429
431
  }),
430
432
  describeCall: "none",
431
433
  execute: async (input) => {
@@ -698,8 +700,11 @@ export function createGeneratePlanTool(deps) {
698
700
  "to the planner directly. Do NOT summarize or re-type them into this call; you cannot restate that " +
699
701
  "record more faithfully than the record itself. Pass only what the profile cannot hold: the research " +
700
702
  "question, facts the user told you (analystNotes), prior run results, and their constraints. " +
701
- "Returns a structured plan ready for show_plan and execute_analysis plan mode, or a clarification question if " +
702
- "the planner is missing something it cannot infer.",
703
+ "The planner is a multi-step sub-agent, thus a call can take many minutes. " +
704
+ "The result carries an `event`: `plan_complete` with {planId, plan} — the plan is saved and ready for show_plan and " +
705
+ "execute_analysis plan mode, and this call neither shows nor runs it; `clarification_needed` with {question, questionContext?} " +
706
+ "when the planner is missing a fact it cannot infer; or `error` with {error} when no plan was saved (the planner reported " +
707
+ "a blocker, the save failed, the call timed out or was cancelled, or parentPlanId is not a plan of this analysis).",
703
708
  inputSchema: z.object({
704
709
  researchQuestion: z.string().describe("What the user wants to analyze — their goal and specific questions."),
705
710
  analystNotes: z
@@ -93,13 +93,14 @@ const FAILURE_SCOPE_NOTE = "This records an earlier attempt; this row cannot est
93
93
  export function createInspectDataProfileTool(pool) {
94
94
  return defineTool({
95
95
  id: "inspect_data_profile",
96
- description: "Read this analysis's data profile — the AUTHORITATIVE record of what the input dataset is: " +
96
+ description: "Read this analysis's data profile — the authoritative record of what the input dataset is: " +
97
97
  "organism and taxon id, scientific domain and subtype, tissue, cell type, condition, experimental design, " +
98
- "dataset-wide caveats, public accessions, the GROUPS of file the dataset is made of, the DIMENSIONS that vary " +
98
+ "dataset-wide caveats, public accessions, the groups of files the dataset is made of, the dimensions that vary " +
99
99
  "across it with the evidence behind each, and the file census (kept, unclassified, quarantined). " +
100
- "There is NO data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
100
+ "There is no data-profile file in the workspace — this tool is the only way to read it. Do not search for one, " +
101
101
  "and do not rediscover these facts by listing or reading the raw input files. " +
102
- "Call it before you reason about the data (planning, writing analysis code, interpreting results). " +
102
+ "Call it when a question about the data (for planning, analysis code, or interpretation) is not answered by what " +
103
+ "you already hold; a step briefing already carries a projection of this profile. " +
103
104
  "scope:'overview' (the default) returns the dataset-level orientation plus the partition accounting — how many " +
104
105
  "files were kept, how many landed in no group, how many were quarantined — and how many members are described " +
105
106
  "individually. That last figure and the dataset's file count differ on purpose: a large dataset is described by " +
@@ -110,10 +111,11 @@ export function createInspectDataProfileTool(pool) {
110
111
  "scope:'files' returns the individually annotated members, paged: page (1-based, default 1) and pageSize (default 20, max 100), " +
111
112
  "always with the true total and hasMore, so you can see exactly what you have not read yet. " +
112
113
  "For the paths of files no record describes, use scope:'groups' for the structure and list the workspace tree for the paths. " +
114
+ "The file paths of scope:'files' and the display patterns of scope:'groups' are rooted at /{analysisId}/, thus they resolve from any working directory. " +
113
115
  "The state field says what you got: 'ready'; 'stale' (a profile is returned but may not describe the current " +
114
116
  "inputs — staleReason says why); 'pending' (profiling is still running); 'failed'; or 'absent' (never profiled, " +
115
117
  "or the analysis has no input files). " +
116
- "A 'failed' state reports a PAST attempt, with failedAt naming when it was recorded — it is not a verdict on the " +
118
+ "A 'failed' state reports a past attempt, with failedAt naming when it was recorded — it is not a verdict on the " +
117
119
  "input files you are holding now, and this tool cannot tell you whether that attempt covered them. Compare failedAt " +
118
120
  "against when the input set last changed before you conclude anything: if the data moved since, say the profile " +
119
121
  "needs re-running rather than diagnosing the data itself.",
@@ -97,9 +97,13 @@ const inputSchema = z
97
97
  export function createInspectRunTool(pool, clock = systemClock) {
98
98
  return defineTool({
99
99
  id: "inspect_run",
100
- description: "Inspect workflow runs for this analysis. Without a runId, returns an active-first paged list (running, suspended, then terminal history). " +
101
- "With a runId, returns an explicit inspectionState and advertises output paths only after the run is terminal. " +
100
+ description: "Inspect workflow runs for this analysis. A run never posts its completion into the conversation; this tool is how you read it. " +
101
+ "Without a runId, returns an active-first paged list (running, suspended, then terminal history) with total and hasMore. " +
102
+ "With a runId, returns the run, its steps, and an inspectionState: not_found; in_progress; suspended (the run does not progress until it is resumed); " +
103
+ "or terminal (run.status completed, partial, failed, or canceled). " +
104
+ "Output paths appear only on a terminal run: run.synthesisPath when synthesisStatus is produced, and the summaryPath of each step that ran. " +
102
105
  "For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
106
+ "A wait on the run that the caller itself runs inside returns at once with selfWaitPrevented. " +
103
107
  "Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
104
108
  inputSchema,
105
109
  // The two calls do different work: one inspects a named run, the other
@@ -58,12 +58,17 @@ export function createLiteratureReviewerTool(deps) {
58
58
  };
59
59
  return defineTool({
60
60
  id: "literature_reviewer",
61
- description: "Delegate batch literature/biology research: looking up multiple " +
62
- "genes or pathways, validating findings against published knowledge, " +
63
- "or building evidence profiles. Pass a self-contained brief — the " +
64
- "reviewer has no access to this conversation. Returns a structured " +
65
- "evidence report. Do NOT delegate simple single-gene lookups; handle " +
66
- "those directly with your own bio-lookup tools.",
61
+ description: "Delegate batch literature/biology research to a sub-agent: looking up " +
62
+ "multiple genes or pathways, validating findings against published " +
63
+ "knowledge, or building evidence profiles. The reviewer holds gene, " +
64
+ "protein, annotation, interaction, PubMed, citation-verification, " +
65
+ "drug-gene and preclinical-profile lookups. It cannot see this " +
66
+ "conversation, the workspace, or the analysis files, thus pass a " +
67
+ "self-contained brief. One call runs a multi-step research loop of up " +
68
+ "to 30 model turns. Returns {report}: a Markdown evidence report with " +
69
+ "an evidence summary per target, cross-target patterns, and key " +
70
+ "references with PMIDs. A run that ends with no report returns a " +
71
+ "retryable error that names the finish reason.",
67
72
  inputSchema: z.object({
68
73
  brief: z
69
74
  .string()
@@ -20,7 +20,8 @@ export const searchArxivTool = defineTool({
20
20
  categories: z
21
21
  .array(z.string())
22
22
  .optional()
23
- .describe('Optional arXiv category filters (e.g., ["cs.LG", "math.OC"]). ' + "Combined with the query using AND."),
23
+ .describe('Optional arXiv category filters (e.g., ["cs.LG", "math.OC"]). A paper matches when it carries any one of them, ' +
24
+ "and that filter is combined with the query using AND."),
24
25
  limit: z.number().int().min(1).max(20).default(10).describe("Maximum results (1–20, default 10)."),
25
26
  }),
26
27
  describeCall: "none",
@@ -54,10 +54,16 @@ export function createSearchGithubReposTool(deps) {
54
54
  return defineTool({
55
55
  id: "search_github_repos",
56
56
  description: "Search GitHub for code repositories matching a free-text query, " +
57
- "optionally filtered by programming language. Returns URL, full " +
57
+ "optionally filtered by programming language. Results are sorted by " +
58
+ "star count, highest first, not by relevance. Returns URL, full " +
58
59
  "name (owner/repo), description, star count, primary language, " +
59
- "and last-updated timestamp. Use to find implementations of " +
60
- "cross-domain methods cited in analogical-reasoning results.",
60
+ "and last-updated timestamp; description and language are absent " +
61
+ "when GitHub holds none. Use to find a published implementation of " +
62
+ "a method or a pipeline.\n" +
63
+ "ABSENCE IS NORMAL: an empty repos array means no repository matched, " +
64
+ "and `success: false` with an `error` string means GitHub could not be " +
65
+ "reached or refused the request (the low unauthenticated rate limit " +
66
+ "lands here). Report either one and continue, do not retry unchanged.",
61
67
  inputSchema: z.object({
62
68
  query: z
63
69
  .string()
@@ -61,7 +61,7 @@ const ListAvailableRefsInputSchema = z.object({
61
61
  .string()
62
62
  .max(MAX_INPUT_PATH_BYTES)
63
63
  .optional()
64
- .describe('Shorthand for a top-level group to inspect (e.g. "atlas_singlecell", "msigdb"); the groups present come back in `categories`. Ignored when `path` is given.'),
64
+ .describe("The name of a top-level directory of the store to inspect, the same as `path` with one segment. A label in `categories` that is not a directory reads as `not_found`; pass such a label as `query`. Ignored when `path` is given."),
65
65
  query: z
66
66
  .string()
67
67
  .optional()
@@ -512,7 +512,8 @@ export function createListAvailableRefsTool(deps) {
512
512
  "Results are ALWAYS bounded — prefer a targeted query over a full dump: " +
513
513
  '`query` is the main entry point, a case-insensitive substring filter over each entry\'s path and its dataset/title/category/organism/format/contents labels (e.g. "regulon", "hallmark", "mouse"); ' +
514
514
  "`path` inspects one directory beneath /mnt/refs (a returned path, or store-relative) — drill in with it when a listing is truncated; " +
515
- '`category` is shorthand for a top-level group (e.g. "pathways", "regulatory-networks"), and the groups present come back in `categories`; ' +
515
+ '`categories` in the response lists the top-level directory names and the dataset group labels (for example "pathways") present; ' +
516
+ "pass a group label as `query`, because `category` inspects a top-level directory by name and a label that is not a directory reads as `not_found`; " +
516
517
  "`limit` caps the entries returned. The response carries `returned`, `total`, and `hasMore`, so truncation is never silent. " +
517
518
  "An empty result means the dataset is absent right now — not that it is unobtainable. Say so; never guess a path, and never silently substitute a different dataset. " +
518
519
  "The store is provisioned by the host and is NOT frozen: datasets can be added through the host's own provisioning path, and whatever is added shows up on a later call.",
@@ -28,7 +28,12 @@ export function createBlockerHolder() {
28
28
  return { outcome: null };
29
29
  }
30
30
  /** The one input every `report_blocker` takes, wherever it is offered. */
31
- const ReportBlockerInputSchema = z.object({ reason: z.string().min(1) });
31
+ const ReportBlockerInputSchema = z.object({
32
+ reason: z
33
+ .string()
34
+ .min(1)
35
+ .describe("What blocks the work, stated as a fact: the missing or unreadable input, or the capability that is not available, and what you tried."),
36
+ });
32
37
  /**
33
38
  * Build the `report_blocker` tool for one loop. The id, the input schema and
34
39
  * the terminal contract ("pass a reason, then stop") are fixed here; the call
@@ -48,8 +48,10 @@ export function createSkillTools(deps) {
48
48
  id: "skill_search",
49
49
  description: "Search your skills (method decision trees, API references, worked " +
50
50
  "examples) by keyword. Start here when picking a method or verifying an " +
51
- "API detail. Returns matching skill files with a line snippet — then " +
52
- "`skill_read` the file. No matches returns a data variant.",
51
+ "API detail. The query splits on whitespace into words, and each word matches as a " +
52
+ "case-insensitive substring. Returns one entry per matching skill file — its skill, its path, " +
53
+ "and the first line that holds a query word — ranked by how many distinct query words the file " +
54
+ "holds. Then `skill_read` the file. No matches returns a data variant.",
53
55
  inputSchema: z.object({
54
56
  query: z.string().min(1).describe("Keywords to match (e.g. 'PyDESeq2 contrast syntax')."),
55
57
  topK: z.number().int().min(1).max(MAX_TOP_K).optional().describe(`Max results to return. Defaults to ${DEFAULT_TOP_K}.`),
@@ -127,7 +127,10 @@ export function createStartReportSessionTool(deps) {
127
127
  "Keep the whole brief under approximately 2000 tokens. " +
128
128
  "When the conversation holds no user turn of new work after the last report session, the tool " +
129
129
  "starts nothing and it names that session. The ask that started that session is not new work. " +
130
- "Then tell the user to continue in that chat.",
130
+ "Then tell the user to continue in that chat. " +
131
+ "The result `outcome` is `started` or `existing-session` with the `threadId` of the report chat. " +
132
+ "`no_browser` means that this deployment has no browser for the report agent, thus no report session can start; tell the user. " +
133
+ "`refused`, `parent_not_found`, `parent_not_a_conversation`, and `failed` mean that nothing started, and `detail` gives the reason where there is one.",
131
134
  inputSchema: startReportSessionInput,
132
135
  executionMode: "inline",
133
136
  describeCall: "none",
@@ -116,9 +116,10 @@ export function createEditFileTool(deps) {
116
116
  "(default), old_string must occur exactly once. For a bulk pattern " +
117
117
  "edit set regex=true: old_string is a JS regex, new_string may use $1 " +
118
118
  "references, and exactly one of replace_all=true or expected_matches " +
119
- "gates the write. Returns `out_of_prefix` / `out_of_scope` / " +
120
- "`file_not_found` / `not_found` / `not_unique` / `match_count_mismatch` " +
121
- "data variants — never throws on expected outcomes.",
119
+ "gates the write. Returns `out_of_prefix` / `out_of_scope` / `symlink_denied` / " +
120
+ "`file_not_found` / `not_found` / `not_unique` / `match_count_mismatch` / " +
121
+ "`invalid_pattern` / `invalid_arguments` data variants — never throws on expected outcomes. " +
122
+ "A data variant other than `ok` writes nothing.",
122
123
  inputSchema: EditFileInputSchema,
123
124
  // The path only. The replaced text is file content, not a description.
124
125
  describeCall: ({ path }) => path,
@@ -61,11 +61,12 @@ export function createExecuteCommandTool(deps) {
61
61
  `tools don't express. stdout and stderr are each capped at ${EXEC_STREAM_BYTE_CAP / (1024 * 1024)} MiB; the end ` +
62
62
  "of a longer stream is dropped, and the result carries its truncation flag. A long result comes back as an " +
63
63
  "excerpt of its start and its end, with a reference to read the rest when the harness keeps it. " +
64
- "Failures are returned as data, " +
65
- "not thrown. stdout/stderr are EPHEMERAL and are NOT a deliverable — use " +
66
- "this to confirm a command ran, not to produce results. Do NOT compute " +
67
- "analysis results via 'python -c' / inline one-liners and read them from " +
68
- "stdout; that work is lost. Write a script and persist its outputs to output/.",
64
+ "Failures are returned as data, not thrown: `status` is `ok` whenever the call completed, so read " +
65
+ "`exitCode` for the outcome of the command. `timedOut: true` means the timeout stopped the command, and a " +
66
+ "`syntheticFailure` with `exitCode: null` means the sandbox died under the command (its reason names an " +
67
+ "out-of-memory kill). stdout and stderr are not a deliverable: they are ephemeral, " +
68
+ "so use them to confirm that a command ran. A result that you compute with 'python -c' or an inline " +
69
+ "one-liner and read from stdout is lost. Write a script and persist its outputs to output/.",
69
70
  inputSchema: ExecuteCommandInputSchema,
70
71
  // The script is what a reader recognises, and it survives a long
71
72
  // invocation whose flags would otherwise push it past the length cap.
@@ -21,7 +21,8 @@ const GrepInputSchema = z.object({
21
21
  pattern: z
22
22
  .string()
23
23
  .min(1)
24
- .describe("Regular expression to match. Anchored per line; case-sensitive unless " + "`ignoreCase` is true."),
24
+ .describe("JavaScript regular expression, tested against each line separately. It matches anywhere in the line " +
25
+ "unless you add `^` or `$`; case-sensitive unless `ignoreCase` is true."),
25
26
  path: z
26
27
  .string()
27
28
  .min(1)
@@ -31,12 +32,13 @@ const GrepInputSchema = z.object({
31
32
  export function createGrepTool(fs, workingDir) {
32
33
  return defineTool({
33
34
  id: "grep",
34
- description: "Regex search over workspace files (input data, prior runs, step " +
35
- "outputs, summaries, syntheses, data profile). Pass a file or a " +
36
- "directory path; directories are walked recursively. Results are " +
37
- `capped at ${DEFAULT_MAX_MATCHES} matches and ${DEFAULT_MAX_MATCH_BYTES} ` +
38
- "bytes per line — narrow your path or pattern if you hit the cap. " +
39
- "No matches and out-of-scope paths return data variants, not errors.",
35
+ description: "Regex search over the text files of the workspace (input data, prior runs, step " +
36
+ "outputs, summaries, syntheses). Pass a file or a " +
37
+ `directory path; directories are walked recursively, to a depth of ${MAX_DIR_DEPTH}. Results are ` +
38
+ `capped at ${DEFAULT_MAX_MATCHES} matches, and each preview at ${DEFAULT_MAX_MATCH_BYTES} characters of its line. ` +
39
+ `Only the first ${MAX_GREP_FILE_BYTES / (1024 * 1024)} MiB of each file is searched, and a file with a NUL byte ` +
40
+ "in its first KiB is skipped as binary. A `truncated` status means the match cap or the file cap cut the search; " +
41
+ "narrow your path or pattern. No matches, missing paths, and out-of-scope paths return data variants, not errors.",
40
42
  inputSchema: GrepInputSchema,
41
43
  // Both fields, because either alone loses the call: the same pattern over
42
44
  // two trees and two patterns over one tree are both ordinary sequences.
@@ -20,7 +20,7 @@ export function createListFilesTool(fs, workingDir) {
20
20
  return defineTool({
21
21
  id: "list_files",
22
22
  description: "List the entries (files and directories, with file sizes) in a directory of the analysis's " +
23
- "own workspace tree. Faster than `ls` via execute_command. Its scope is that tree and nothing " +
23
+ "own workspace tree. It reads the tree directly and runs no sandbox command. It does not recurse. Its scope is that tree and nothing " +
24
24
  "else: it never reaches a directory outside the analysis, including the host process's current " +
25
25
  "working directory. Missing and out-of-scope paths return a data variant, not an error.",
26
26
  inputSchema: ListFilesInputSchema,
@@ -57,7 +57,8 @@ export function createReadFileTool(fs, workingDir) {
57
57
  "as UTF-8 text. Output is capped at " +
58
58
  `${DEFAULT_MAX_BYTES} bytes; pass headLines or tailLines to read a ` +
59
59
  "specific window of a large file (typical for bio CSV/TSV/log files). " +
60
- "Oversize reads come back truncated with a marker. This tool is for " +
60
+ "An oversize read returns status `truncated` with the bytes that fit (the start of the file, or " +
61
+ "the end for tailLines), `totalSize`, and `returnedBytes`. This tool is for " +
61
62
  "text; a binary file (one holding NUL bytes, e.g. a .gz or image) comes " +
62
63
  "back as a `binary` data variant rather than decoded garbage. Missing " +
63
64
  "paths and paths outside the analysis tree return a data variant — not an error.",
@@ -23,7 +23,9 @@ export function createScanInputsTool(deps) {
23
23
  "the shapes its filenames form (sets of files whose names differ only at marked positions), " +
24
24
  "the distinct values each varying position takes, how those positions co-occur, value overlap " +
25
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. " +
26
+ "It also reads the header of one member file for each shape (magic bytes, a preamble, a header row, or a container index). " +
27
+ "Deterministic — no model — and the manifest it returns is bounded, so it costs the same context on three files as on three thousand. " +
28
+ "The walk stops at a ceiling of files; a manifest marked INCOMPLETE describes part of the subtree, so scan a narrower path. " +
27
29
  "Your briefing already carries a scan of the whole input tree; call this to look at a subtree more closely, " +
28
30
  "or to check a grouping you are unsure about. " +
29
31
  "It reports observations only: which files are one KIND of data, and what a varying position MEANS, are yours to decide.",
@@ -34,6 +34,8 @@ export const showFileTool = defineTool({
34
34
  "inline, or paste the bytes. Pick this tool by what you are referencing, not by how the output looks: " +
35
35
  "NOT for content you synthesized (a chart, a table, a snippet you invented — use `show_user`), NOT for a stored plan (`show_plan`). " +
36
36
  "Paths are analysis-rooted (no leading slash, no `..`); discover them with `workspace_search` or `list_files`. " +
37
+ "The tool checks the shape of each path only, not that the file exists, so a wrong path fails only when the user views the card; " +
38
+ "`shown: false` with `invalid_path` means a path has a leading slash or a `..` segment. " +
37
39
  "Up to 10 files per call — pass a related set in ONE call and they render as a gallery, rather than one call per figure. " +
38
40
  "Cards render in call order, so to interleave figures with prose alternate `show_user(markdown)` and `show_file` calls " +
39
41
  "(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
@@ -53,7 +53,9 @@ export function createWorkspaceSearchTool(pool, embedding) {
53
53
  return defineTool({
54
54
  id: "workspace_search",
55
55
  description: "Semantic search over the analysis workspace. Returns ranked entries with descriptions " +
56
- "and metadata; read a file separately to see its contents. `type` restricts results to one " +
56
+ "and metadata; read a file separately to see its contents. The nearest entries always come back, up to " +
57
+ "`limit`, with no relevance cut: `score` is the cosine similarity to the query, and a low score is a weak match, " +
58
+ "not a hit. An empty `results` list means that nothing of that type is indexed yet. `type` restricts results to one " +
57
59
  'entry type: "input-group" (a GROUP of input data the data profiler declared, carrying its ' +
58
60
  'derived count, its display pattern, and its slots), "input-dimension" (something that VARIES ' +
59
61
  "across the dataset — subject, timepoint, batch — with the observations evidencing it), " +
@@ -25,9 +25,11 @@ export function createWriteFileTool(deps) {
25
25
  // body runs unwrapped in the workflow body (see the harness-tools spec).
26
26
  executionMode: "workflow",
27
27
  description: "Write a UTF-8 text file in your working directory. Relative paths " +
28
- "resolve against it; a path outside it returns an `out_of_prefix` data " +
29
- "variant (no I/O), and one escaping the analysis tree returns " +
30
- "`out_of_scope`.",
28
+ "resolve against it. The write replaces an existing file whole, and it makes " +
29
+ "missing parent directories. On success it returns `status: ok`, the absolute " +
30
+ "'/<analysisId>/...' path, and `bytesWritten`. A path outside the working directory " +
31
+ "returns an `out_of_prefix` data variant (no I/O), one escaping the analysis tree returns " +
32
+ "`out_of_scope`, and a symbolic link on the path returns `symlink_denied`.",
31
33
  inputSchema: WriteFileInputSchema,
32
34
  // The path only. `content` is a whole file and must never ride a display
33
35
  // channel, which the emit-site length cap enforces regardless.
@@ -76,6 +76,8 @@ import type { Logger } from "../lib/logger.js";
76
76
  import type { UsageRecorder } from "../billing/usage-recorder.js";
77
77
  import type { CitationResolver } from "../citations/types.js";
78
78
  import type { SynthesisStatus, UpdateStepExecutionInput } from "../state/index.js";
79
+ import { type StepBriefing } from "../prompts/briefing.js";
80
+ import type { WorkingMemoryStore } from "../memory/working-memory.js";
79
81
  import type { AnalysisStep } from "../schemas/workflow-state.js";
80
82
  import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
81
83
  import type { BioToolKeys } from "../tools/bio/keys.js";
@@ -248,6 +250,11 @@ export interface ExecuteAnalysisDeps {
248
250
  readonly usageRecorder?: UsageRecorder;
249
251
  /** The store of the loops of the run synthesis and of their read tools. */
250
252
  readonly toolOutputStore?: ToolOutputStore;
253
+ /**
254
+ * The working memory of the analysis. The body only reads it: each step seed
255
+ * carries the goal and the constraints as they are at the dispatch of that step.
256
+ */
257
+ readonly workingMemory: WorkingMemoryStore;
251
258
  /**
252
259
  * The provenance seam of the composition. The body reads its run emit member,
253
260
  * and it reads no other member.
@@ -309,11 +316,6 @@ export declare function buildChildInput(args: {
309
316
  * rendered any earlier (at plan submission, say) cannot mention them, and the
310
317
  * agent is left to rediscover its own upstream.
311
318
  *
312
- * Everything read here is durable state written by an already-settled child
313
- * (`output/summary.md`, `cortex_artifacts`) or by the profiler
314
- * (`cortex_analysis_state`) — never live in-flight state, which would not
315
- * reproduce.
316
- *
317
319
  * REPLAY: the caller wraps this in a `DBOS.runStep`, so the composed string is
318
320
  * checkpointed and a replay returns it verbatim — the DB and disk are not read
319
321
  * again and cannot drift the seed under a recovered run. Never call it
@@ -325,6 +327,27 @@ export declare function composeStepSeed(args: {
325
327
  runId: string;
326
328
  deps: ExecuteAnalysisDeps;
327
329
  }): Promise<string>;
330
+ /**
331
+ * Do every read of one step's package and give the data its seed is rendered
332
+ * from. All reads live here, so `composeStepBriefing` stays a pure renderer.
333
+ *
334
+ * Everything read is durable state: written by an already-settled child
335
+ * (`output/summary.md`, `cortex_artifacts`), by the profiler
336
+ * (`cortex_analysis_state`), or by the conversation agent
337
+ * (`cortex_working_memory`). The working memory is the one read that can
338
+ * change while the run is live — it is read per dispatch, not once per run,
339
+ * so an edit during a run reaches each later step, and two steps of one run
340
+ * can carry different copies.
341
+ *
342
+ * A read failure throws (house rules): a swallowed error would be frozen into
343
+ * the checkpointed seed and served to every replay of this step.
344
+ */
345
+ export declare function gatherStepPackage(args: {
346
+ input: ExecuteAnalysisInput;
347
+ stepId: string;
348
+ runId: string;
349
+ deps: ExecuteAnalysisDeps;
350
+ }): Promise<StepBriefing>;
328
351
  /**
329
352
  * Project a classified synthesis outcome onto the run-phase `synthesis` ledger
330
353
  * row's terminal update (see the run-synthesis-outcome spec). The blocker keeps
@@ -82,7 +82,7 @@ import { unwrapOrThrow } from "../lib/result.js";
82
82
  import { countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, queryStepsByRun, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
83
83
  import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
84
84
  import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
85
- import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing } from "../prompts/briefing.js";
85
+ import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing, emptyStepMemory } from "../prompts/briefing.js";
86
86
  import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
87
87
  import { isChatDataPart } from "../sandbox/sandbox-step-translate.js";
88
88
  import { synthesizeRun } from "../app/synthesize-run.js";
@@ -220,17 +220,30 @@ export function buildChildInput(args) {
220
220
  * rendered any earlier (at plan submission, say) cannot mention them, and the
221
221
  * agent is left to rediscover its own upstream.
222
222
  *
223
- * Everything read here is durable state written by an already-settled child
224
- * (`output/summary.md`, `cortex_artifacts`) or by the profiler
225
- * (`cortex_analysis_state`) — never live in-flight state, which would not
226
- * reproduce.
227
- *
228
223
  * REPLAY: the caller wraps this in a `DBOS.runStep`, so the composed string is
229
224
  * checkpointed and a replay returns it verbatim — the DB and disk are not read
230
225
  * again and cannot drift the seed under a recovered run. Never call it
231
226
  * unwrapped from the workflow body.
232
227
  */
233
228
  export async function composeStepSeed(args) {
229
+ return composeStepBriefing(await gatherStepPackage(args));
230
+ }
231
+ /**
232
+ * Do every read of one step's package and give the data its seed is rendered
233
+ * from. All reads live here, so `composeStepBriefing` stays a pure renderer.
234
+ *
235
+ * Everything read is durable state: written by an already-settled child
236
+ * (`output/summary.md`, `cortex_artifacts`), by the profiler
237
+ * (`cortex_analysis_state`), or by the conversation agent
238
+ * (`cortex_working_memory`). The working memory is the one read that can
239
+ * change while the run is live — it is read per dispatch, not once per run,
240
+ * so an edit during a run reaches each later step, and two steps of one run
241
+ * can carry different copies.
242
+ *
243
+ * A read failure throws (house rules): a swallowed error would be frozen into
244
+ * the checkpointed seed and served to every replay of this step.
245
+ */
246
+ export async function gatherStepPackage(args) {
234
247
  const { input, stepId, runId, deps } = args;
235
248
  const step = input.planStepById[stepId];
236
249
  if (!step) {
@@ -246,9 +259,19 @@ export async function composeStepSeed(args) {
246
259
  analysisId,
247
260
  workingDir: toSandboxPath(workspaceRoot, analysisId, stepWritePrefix({ workspaceRoot, runId, stepId })),
248
261
  };
249
- // A DB failure here throws (house rules): a swallowed error would be frozen
250
- // into the checkpointed seed and served to every replay of this step.
251
262
  const profile = unwrapOrThrow(await loadDataProfileStatus(deps.pool, analysisId));
263
+ // A failed memory read leaves the section out: the step runs without the copy of the memory, not without a seed.
264
+ const memoryRead = await deps.workingMemory.load(analysisId);
265
+ if (memoryRead.isErr()) {
266
+ const log = (deps.logger ?? createNoopLogger()).named("executeAnalysis.seed");
267
+ log.warn("the working-memory read failed, thus the seed has no memory section", {
268
+ analysisId,
269
+ stepId,
270
+ error: describeDbError(memoryRead.error),
271
+ ...log.errorFields(memoryRead.error.cause),
272
+ });
273
+ }
274
+ const memory = memoryRead.isOk() ? memoryRead.value : emptyStepMemory();
252
275
  const upstream = await collectUpstreamHandoffs({
253
276
  analysisId,
254
277
  runId,
@@ -257,12 +280,13 @@ export async function composeStepSeed(args) {
257
280
  agentByStepId: input.agentByStepId,
258
281
  pool: deps.pool,
259
282
  });
260
- return composeStepBriefing({
283
+ return {
261
284
  step,
262
285
  workspace,
263
286
  profile: profile?.result ?? null,
264
287
  upstream,
265
- });
288
+ memory: { goal: memory.goal, constraints: memory.constraints },
289
+ };
266
290
  }
267
291
  /**
268
292
  * Gather what each completed dependency produced. Dependency order is the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inflexa-ai/harness",
3
- "version": "0.40.0",
3
+ "version": "0.41.1",
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",