@inflexa-ai/harness 0.41.0 → 0.41.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/contracts/report-blocks.d.ts +10 -10
- package/dist/contracts/report-reference.d.ts +4 -4
- package/dist/memory/message-backfill.d.ts +8 -2
- package/dist/prompts/briefing.d.ts +28 -1
- package/dist/prompts/briefing.js +44 -1
- package/dist/prompts/compaction.d.ts +2 -2
- package/dist/prompts/compaction.js +9 -5
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +12 -0
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +4 -0
- package/dist/report-model/authoring-grammar.d.ts +4 -4
- package/dist/runtime/assemble.d.ts +4 -2
- package/dist/runtime/assemble.js +8 -1
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +3 -3
- package/dist/schemas/file-metadata.js +9 -9
- package/dist/schemas/plan-schemas.d.ts +3 -3
- package/dist/state/init.d.ts +7 -6
- package/dist/state/init.js +51 -672
- package/dist/state/migrations/20260927120000_baseline.d.ts +11 -0
- package/dist/state/migrations/20260927120000_baseline.js +675 -0
- package/dist/state/migrations/index.d.ts +10 -0
- package/dist/state/migrations/index.js +13 -0
- package/dist/tools/ad-hoc-router.js +9 -4
- package/dist/tools/bio/chembl.js +8 -5
- package/dist/tools/bio/comptox.js +4 -4
- package/dist/tools/bio/drug-gene-interactions.js +5 -4
- package/dist/tools/bio/gene-disease-evidence.js +4 -4
- package/dist/tools/bio/lookup-annotation.js +7 -3
- package/dist/tools/bio/opentargets.js +8 -6
- package/dist/tools/bio/pubchem.js +4 -2
- package/dist/tools/bio/pubmed.d.ts +1 -1
- package/dist/tools/bio/pubmed.js +10 -6
- package/dist/tools/bio/search-clinical-trials.js +2 -1
- package/dist/tools/bio/search-gene.js +2 -1
- package/dist/tools/bio/search-interactions.js +8 -4
- package/dist/tools/bio/target-safety.js +2 -1
- package/dist/tools/display/show-user.js +7 -5
- package/dist/tools/execute-analysis.js +14 -4
- package/dist/tools/lib/opentargets-client.d.ts +8 -3
- package/dist/tools/lib/opentargets-client.js +12 -3
- package/dist/tools/read-tool-output.js +7 -4
- package/dist/tools/report-authoring/authoring-tools.js +22 -15
- package/dist/tools/report-session/derive-table.js +5 -1
- package/dist/tools/report-session/examine-page.js +5 -4
- package/dist/tools/report-session/preview-report.js +6 -1
- package/dist/tools/report-session/record-version.js +2 -1
- package/dist/tools/research/context7-docs.js +8 -2
- package/dist/tools/research/generate-analogy-report.js +15 -15
- package/dist/tools/research/generate-plan.js +12 -7
- package/dist/tools/research/inspect-data-profile.js +7 -5
- package/dist/tools/research/inspect-run.js +6 -2
- package/dist/tools/research/literature-reviewer.js +11 -6
- package/dist/tools/research/search-arxiv.js +2 -1
- package/dist/tools/research/search-github-repos.js +9 -3
- package/dist/tools/sandbox/list-available-refs.js +3 -2
- package/dist/tools/sandbox/report-blocker.js +6 -1
- package/dist/tools/sandbox/skills.js +4 -2
- package/dist/tools/start-report-session.js +4 -1
- package/dist/tools/workspace/edit-file.js +4 -3
- package/dist/tools/workspace/execute-command.js +6 -5
- package/dist/tools/workspace/grep.js +9 -7
- package/dist/tools/workspace/list-files.js +1 -1
- package/dist/tools/workspace/read-file.js +2 -1
- package/dist/tools/workspace/scan-inputs.js +3 -1
- package/dist/tools/workspace/show-file.js +6 -3
- package/dist/tools/workspace/workspace-search.js +3 -1
- package/dist/tools/workspace/write-file.js +5 -3
- package/dist/workflows/execute-analysis.d.ts +28 -5
- package/dist/workflows/execute-analysis.js +34 -10
- package/package.json +2 -1
|
@@ -393,9 +393,14 @@ export function createPreviewReportTool(deps) {
|
|
|
393
393
|
const resolvePageAsset = deps.resolvePageAsset ?? resolvePageAssetFromInstallation;
|
|
394
394
|
return defineTool({
|
|
395
395
|
id: "preview_report",
|
|
396
|
-
description: "Render the current draft to
|
|
396
|
+
description: "Render the current draft to an HTML page in the session directory, and give back the page path. " +
|
|
397
|
+
"The page loads its figures, table data, and libraries from an assets directory that the tool stages beside it. " +
|
|
397
398
|
"The tool finishes the draft first: an incomplete draft gives back the gap list, and no page renders. " +
|
|
398
399
|
"On a pass it resolves each reference, stages each bound image beside the page, and writes the page. " +
|
|
400
|
+
"An unresolved reference gives back the block and the binding to repair. " +
|
|
401
|
+
"The outcome resolver-unavailable means that this host gives no reference resolver, and a repeat gives the same outcome. " +
|
|
402
|
+
"A rendered page carries pagePath, and access when the host serves the page: a URL with its expiry, or the reason that no URL was granted. " +
|
|
403
|
+
"The render marks the page that examine_page looks at next. " +
|
|
399
404
|
"Use it to see the report, and to confirm that each reference resolves.",
|
|
400
405
|
inputSchema: previewReportInput,
|
|
401
406
|
executionMode: "inline",
|
|
@@ -128,7 +128,8 @@ export function createRecordVersionTool(deps) {
|
|
|
128
128
|
id: "record_report_version",
|
|
129
129
|
description: "Record the current draft as one report version. The tool runs the whole gate first: it finishes the draft, " +
|
|
130
130
|
"resolves each reference, matches each chart encoding, and matches each assert. An incomplete draft gives back the gap list, " +
|
|
131
|
-
"and a failed reference gives back the block that broke. The tool records a version only after
|
|
131
|
+
"and a failed reference gives back the block that broke. The tool records a version only after examine_page looked at the page of the current draft: " +
|
|
132
|
+
"with no look it gives back never-seen, and after an edit since the look it gives back stale-look. " +
|
|
132
133
|
"A thread holds one version, thus a later record replaces it whole. Amend the draft, look at the page again, and record again. " +
|
|
133
134
|
"After the version lands, the tool removes the file of each derived table that no block of the recorded report binds.",
|
|
134
135
|
inputSchema: recordVersionInput,
|
|
@@ -43,7 +43,10 @@ function renderSnippets(snippets) {
|
|
|
43
43
|
}
|
|
44
44
|
export const resolveLibraryIdTool = defineTool({
|
|
45
45
|
id: "resolve_library_id",
|
|
46
|
-
description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to
|
|
46
|
+
description: "Resolve a package name (e.g., 'scanpy', 'DESeq2', 'scikit-learn') to the Context7 library ID that `query_docs` needs. " +
|
|
47
|
+
"Returns the single best match as {found: true, libraryId, name, description}, or {found: false} when Context7 holds no library for the name. " +
|
|
48
|
+
"`query` ranks the candidates and only the top one comes back, thus make sure that `name` is the package you meant. " +
|
|
49
|
+
"A failed Context7 request returns an error result.",
|
|
47
50
|
inputSchema: z.object({
|
|
48
51
|
libraryName: z.string().describe("Package or library name to search for (e.g., 'scanpy', 'pysam', 'lifelines')."),
|
|
49
52
|
query: z.string().describe("What you need help with — used to rank results by relevance (e.g., 'how to run differential expression')."),
|
|
@@ -69,7 +72,10 @@ export const resolveLibraryIdTool = defineTool({
|
|
|
69
72
|
});
|
|
70
73
|
export const queryDocsTool = defineTool({
|
|
71
74
|
id: "query_docs",
|
|
72
|
-
description: "Query up-to-date documentation and code examples for a library.
|
|
75
|
+
description: "Query up-to-date documentation and code examples for a library. The libraryId comes from `resolve_library_id`. " +
|
|
76
|
+
"Returns {found: true, documentation}: Markdown snippets, each with a title, a source id, a description, and code blocks. " +
|
|
77
|
+
"Returns {found: false} when Context7 holds no library at that ID or no snippet for the query. A failed Context7 request returns an error result. " +
|
|
78
|
+
"Use this to verify function signatures, parameters, and usage patterns before writing code.",
|
|
73
79
|
inputSchema: z.object({
|
|
74
80
|
libraryId: z.string().describe("Context7 library ID exactly as `resolve_library_id` gave it — an owner and a library, for example '/scverse/scanpy'."),
|
|
75
81
|
query: z.string().describe("Specific question about the library (e.g., 'rank_genes_groups function parameters and usage')."),
|
|
@@ -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.
|
|
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.
|
|
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:
|
|
139
|
-
"
|
|
140
|
-
"
|
|
141
|
-
"
|
|
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
|
|
147
|
-
"it is slow
|
|
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 —
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
"
|
|
155
|
-
"
|
|
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}
|
|
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.
|
|
425
|
-
"
|
|
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
|
-
"
|
|
702
|
-
"the
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
101
|
-
"
|
|
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
|
|
62
|
-
"genes or pathways, validating findings against published
|
|
63
|
-
"or building evidence profiles.
|
|
64
|
-
"
|
|
65
|
-
"
|
|
66
|
-
"
|
|
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"]).
|
|
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.
|
|
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
|
|
60
|
-
"
|
|
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(
|
|
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
|
-
'`
|
|
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({
|
|
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.
|
|
52
|
-
"
|
|
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
|
-
"
|
|
66
|
-
"
|
|
67
|
-
"
|
|
68
|
-
"
|
|
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("
|
|
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
|
|
35
|
-
"outputs, summaries, syntheses
|
|
36
|
-
|
|
37
|
-
`capped at ${DEFAULT_MAX_MATCHES} matches and ${DEFAULT_MAX_MATCH_BYTES} ` +
|
|
38
|
-
|
|
39
|
-
"
|
|
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.
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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.",
|
|
@@ -25,7 +25,7 @@ const ShowFileInputSchema = z.object({
|
|
|
25
25
|
}))
|
|
26
26
|
.min(1)
|
|
27
27
|
.max(MAX_FILES)
|
|
28
|
-
.describe(`The
|
|
28
|
+
.describe(`The files to show together, 1 to ${MAX_FILES} per call; multiple render as ONE gallery card. Pass all the figures that one prose section discusses in a single call. Figures for different sections go in separate calls.`),
|
|
29
29
|
});
|
|
30
30
|
export const showFileTool = defineTool({
|
|
31
31
|
id: "show_file",
|
|
@@ -34,8 +34,11 @@ 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
|
-
"
|
|
38
|
-
"
|
|
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. " +
|
|
39
|
+
"Up to 10 files per call, rendered as one gallery — pass all the figures that one prose section discusses in ONE call. " +
|
|
40
|
+
"Cards render in call order, so to put figures next to their prose, emit `show_user(markdown)` sections and `show_file` " +
|
|
41
|
+
"calls in one batch, in reading order: a section, then the `show_file` for its figures, then the next section " +
|
|
39
42
|
"(markdown image syntax pointing at a workspace file does NOT render — this tool is the only way to show one).",
|
|
40
43
|
inputSchema: ShowFileInputSchema,
|
|
41
44
|
// `.min(1)` on `files` makes index 0 total after the parse.
|
|
@@ -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.
|
|
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
|
|
29
|
-
"
|
|
30
|
-
"`
|
|
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
|