@inflexa-ai/harness 0.41.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.
- 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/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +4 -0
- package/dist/runtime/assemble.d.ts +4 -2
- package/dist/runtime/assemble.js +8 -1
- package/dist/schemas/file-metadata.js +9 -9
- package/dist/schemas/plan-schemas.d.ts +3 -3
- 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 +1 -1
- 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 +2 -0
- 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 +1 -1
|
@@ -23,7 +23,8 @@ export const searchClinicalTrialsTool = defineTool({
|
|
|
23
23
|
"protocol prose.\n" +
|
|
24
24
|
"action 'details' takes ONE nctId and returns that study in full: the same trial record plus outcomes[] (each primary/secondary/other outcome " +
|
|
25
25
|
"measure with its numeric effect where the sponsor posted results) and the reported adverse events with their per-arm counts. This is how you " +
|
|
26
|
-
"read a result, as opposed to finding a trial
|
|
26
|
+
"read a result, as opposed to finding a trial. The 'details' answer is not trimmed: a large trial with posted results carries hundreds of " +
|
|
27
|
+
"adverse-event terms, each with its per-arm counts, thus one answer can run to tens of thousands of characters.\n" +
|
|
27
28
|
"ACCEPTED IDENTIFIERS: `query` is free text matched across the whole study record, so a gene symbol ('KRAS'), a drug or brand name ('imatinib', " +
|
|
28
29
|
"'Gleevec'), a condition ('melanoma') or an NCT ID ('NCT00000102') all work. `nctId` takes one NCT ID and nothing else.\n" +
|
|
29
30
|
"`status` is an optional server-side filter and `phase` narrows the returned page. Omit both to see the full landscape. totalFound reports the true " +
|
|
@@ -34,7 +34,8 @@ export const searchGeneTool = defineTool({
|
|
|
34
34
|
"biotype, assembly, description; plus resolvedFrom[] and notFound[]. Gene-level only — no transcripts or exons. Batch up to 200 identifiers per " +
|
|
35
35
|
"call.\n" +
|
|
36
36
|
"ACCEPTED IDENTIFIERS, mixed freely in one call: a HUGO gene symbol ('BRCA1'), an Ensembl gene ID ('ENSG00000012048'), an HGNC ID ('HGNC:1100'), " +
|
|
37
|
-
"a UniProt accession ('P38398') and a ChEMBL target ID ('CHEMBL5619')
|
|
37
|
+
"a six-character UniProt accession ('P38398') and a ChEMBL target ID ('CHEMBL5619'); a ten-character accession ('A0A0B4J1Y9') is read " +
|
|
38
|
+
"as a symbol and lands in notFound. An input that is not a symbol is anchored on the HGNC and UniProt " +
|
|
38
39
|
"registries first, and the approved symbol it yields is reported in resolvedFrom[] — that anchoring is human, so a non-symbol input with a " +
|
|
39
40
|
"non-human `species` looks the ortholog up by the human approved symbol.\n" +
|
|
40
41
|
"notFound is valid no-data (a deprecated, aliased or mistyped identifier, or one this species has no gene for) — report it and continue, do not " +
|
|
@@ -12,12 +12,16 @@ export const searchInteractionsTool = defineTool({
|
|
|
12
12
|
"evidence into one confidence score — for interactions and gene-set functional enrichment. " +
|
|
13
13
|
"'partners' — the one-hop interaction partners of the input proteins, score-sorted: 'what else does this protein work with?'. " +
|
|
14
14
|
"'network' — the interactions AMONG the input proteins only, no new nodes: 'is my gene set actually connected?'. " +
|
|
15
|
-
"'enrichment' — statistical over-representation of
|
|
15
|
+
"'enrichment' — statistical over-representation of terms in the input set against the whole genome, FDR-sorted, over every STRING " +
|
|
16
|
+
"category: GO Process / Function / Component, KEGG, Reactome (RCTM), Pfam, InterPro, UniProt keywords, COMPARTMENTS, DISEASES, HPO, " +
|
|
17
|
+
"WikiPathways and PubMed publications (PMID) among them, with `category` naming the source of each row. It is the enrichment TEST, as " +
|
|
16
18
|
"opposed to lookup_annotation, which just reads a vocabulary. " +
|
|
17
19
|
"ACCEPTED IDENTIFIERS: STRING resolves each one itself, so a HUGO gene symbol ('TP53'), a UniProt accession ('P04637'), an Ensembl protein ID " +
|
|
18
20
|
"('ENSP00000269305') and a full protein name all work, mixed in one call. `species` takes an NCBI Taxonomy ID (9606 = human).\n" +
|
|
19
|
-
"`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges.
|
|
20
|
-
"
|
|
21
|
+
"`limit` applies to all three actions and defaults small: a network over N proteins grows with N² edges. For 'partners', STRING applies " +
|
|
22
|
+
"it to EACH input protein, thus N inputs give up to N × limit rows. `totalEdges` / `totalTerms` give the pre-trim counts of 'network' and " +
|
|
23
|
+
"'enrichment'; 'partners' gives no total. An empty result is valid no-data (unconnected set, no enriched term, unresolvable identifiers) — " +
|
|
24
|
+
"report it and continue, do " +
|
|
21
25
|
"not retry.",
|
|
22
26
|
inputSchema: z.object({
|
|
23
27
|
identifiers: z.array(z.string()).min(1).max(100).describe("Protein/gene identifiers (e.g. ['TP53', 'BRCA1'])"),
|
|
@@ -36,7 +40,7 @@ export const searchInteractionsTool = defineTool({
|
|
|
36
40
|
.min(1)
|
|
37
41
|
.max(500)
|
|
38
42
|
.default(DEFAULT_LIMIT)
|
|
39
|
-
.describe(`Max records returned — partners, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
|
|
43
|
+
.describe(`Max records returned — partners PER INPUT PROTEIN, network edges, or enriched terms depending on action (default ${DEFAULT_LIMIT}, max 500). ` +
|
|
40
44
|
"Rows are ordered best-first (score descending for interactions, FDR ascending for enrichment), so the default keeps the strongest."),
|
|
41
45
|
}),
|
|
42
46
|
describeCall: "none",
|
|
@@ -93,7 +93,8 @@ export const targetSafetyTool = defineTool({
|
|
|
93
93
|
id: "target_safety",
|
|
94
94
|
description: "TARGET-level, mechanism-based safety liability — the curated secondary-pharmacology panel and Open Targets' curated liabilities in one call. " +
|
|
95
95
|
"Judges what engaging a target is likely to do to an organ system, before any specific molecule exists.\n" +
|
|
96
|
-
"ACCEPTED IDENTIFIERS, mixed freely in one call:
|
|
96
|
+
"ACCEPTED IDENTIFIERS, mixed freely in one call: an approved HUGO gene symbol ('KCNH2' — an alias such as 'hERG' matches no panel entry), a " +
|
|
97
|
+
"ChEMBL target ID ('CHEMBL240'), a UniProt accession " +
|
|
97
98
|
"('Q12809') and an Ensembl gene ID ('ENSG00000055118').\n" +
|
|
98
99
|
"This is the safety of the TARGET, not of a drug. For a marketed molecule's post-market adverse events use search_faers; for a chemical's " +
|
|
99
100
|
"toxicology use comptox; for what removing the gene does in vivo use gene_preclinical_profile.\n" +
|
|
@@ -41,7 +41,7 @@ const ShowUserInputSchema = z.object({
|
|
|
41
41
|
dataPath: z
|
|
42
42
|
.string()
|
|
43
43
|
.optional()
|
|
44
|
-
.describe("echart only: analysis-rooted CSV artifact path (e.g. 'runs/run-abc/step-2/output/de-summary.csv') the host loads as the ECharts `dataset.source` at render time. Author `encode`/dimensions against the CSV's column names (read only its header) and omit `dataset.source` from the spec — the rows are never pulled through the context window. If the data is not chart-ready (needs aggregation, filtering, reshaping), do that in a sandbox step that writes a chart-ready CSV. Inline `dataset.source` is only for a handful of numbers you just computed in conversation that exist nowhere as an artifact."),
|
|
44
|
+
.describe("echart only: analysis-rooted CSV artifact path (e.g. 'runs/run-abc/step-2/output/de-summary.csv') the host loads as the ECharts `dataset.source` at render time. Author `encode`/dimensions against the CSV's column names (read only its header) and omit `dataset.source` from the spec — the rows are never pulled through the context window. The tool checks the shape of the path only, not that the file exists; a wrong path fails only when the card renders. If the data is not chart-ready (needs aggregation, filtering, reshaping), do that in a sandbox step that writes a chart-ready CSV. Inline `dataset.source` is only for a handful of numbers you just computed in conversation that exist nowhere as an artifact."),
|
|
45
45
|
body: z.string().optional().describe("Markdown content to render (kind=markdown)"),
|
|
46
46
|
code: z.string().optional().describe("Code content (kind=code)"),
|
|
47
47
|
language: z.string().optional().describe("Language identifier, e.g. r, python, bash (kind=code)"),
|
|
@@ -23,9 +23,17 @@ import { callExtendAnalysisFarm } from "./sandbox/link-packages.js";
|
|
|
23
23
|
const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
|
|
24
24
|
const inputSchema = z
|
|
25
25
|
.object({
|
|
26
|
-
mode: z
|
|
27
|
-
|
|
28
|
-
|
|
26
|
+
mode: z
|
|
27
|
+
.enum(["plan", "adhoc"])
|
|
28
|
+
.describe("`plan` runs a stored plan that the user approved. `adhoc` runs one targeted step for an explicit user request."),
|
|
29
|
+
planId: planIdSchema
|
|
30
|
+
.optional()
|
|
31
|
+
.describe("mode=plan only, and required there: the `pln-<8hex>` id of the approved stored plan. Omit it for mode=adhoc."),
|
|
32
|
+
request: z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.optional()
|
|
36
|
+
.describe("mode=adhoc only, and required there: the computational request of the user, in the words of the user. Omit it for mode=plan."),
|
|
29
37
|
})
|
|
30
38
|
.superRefine((value, ctx) => {
|
|
31
39
|
if (value.mode === "plan" && (value.planId === undefined || value.request !== undefined)) {
|
|
@@ -319,7 +327,9 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
319
327
|
description: "Launch analysis computation asynchronously. Use mode=plan with the planId only after the user approved that stored plan. " +
|
|
320
328
|
"Use mode=adhoc with the user's exact targeted computational request when they explicitly asked to run/compute/test/compare it; " +
|
|
321
329
|
"that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
|
|
322
|
-
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn."
|
|
330
|
+
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn. " +
|
|
331
|
+
"A plan that already has an active run returns that run, so a second run of one plan does not start. " +
|
|
332
|
+
"The launch fails with an error, and no run starts, when the plan is not found or fails validation, when a package of the plan cannot link, or when the run is not authorized.",
|
|
323
333
|
inputSchema,
|
|
324
334
|
// The mode is what a user most needs to see here — an approved plan
|
|
325
335
|
// running is a different event from an ad hoc request being routed.
|
|
@@ -22,10 +22,15 @@ export interface Association {
|
|
|
22
22
|
somaticMutationScore: number | null;
|
|
23
23
|
literaturePmids: string[];
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* The positive tractability buckets of each modality, by their Open Targets label
|
|
27
|
+
* ('Approved Drug', 'Structure with Ligand', 'UniProt loc high conf', …). An empty
|
|
28
|
+
* list means no positive bucket. `null` means Open Targets gives no row for the modality.
|
|
29
|
+
*/
|
|
25
30
|
export interface TargetTractability {
|
|
26
|
-
smallMolecule:
|
|
27
|
-
antibody:
|
|
28
|
-
otherModalities:
|
|
31
|
+
smallMolecule: string[] | null;
|
|
32
|
+
antibody: string[] | null;
|
|
33
|
+
otherModalities: string[] | null;
|
|
29
34
|
}
|
|
30
35
|
export interface TargetInfo {
|
|
31
36
|
ensemblId: string;
|
|
@@ -277,10 +277,19 @@ export async function searchTargetAssociations(ensemblId, limit = 25) {
|
|
|
277
277
|
if (!target)
|
|
278
278
|
return null;
|
|
279
279
|
const tractabilityEntries = target.tractability ?? [];
|
|
280
|
+
// Open Targets gives one row for each bucket of a modality, from 'Approved Drug' down to a
|
|
281
|
+
// predicted pocket or a localization. The first row alone is the 'Approved Drug' bucket, thus
|
|
282
|
+
// each modality keeps the labels of all of its positive rows.
|
|
283
|
+
const positiveLabels = (modality) => {
|
|
284
|
+
const rows = tractabilityEntries.filter((t) => t.modality === modality);
|
|
285
|
+
if (rows.length === 0)
|
|
286
|
+
return null;
|
|
287
|
+
return rows.flatMap((t) => (t.value === true && t.label ? [t.label] : []));
|
|
288
|
+
};
|
|
280
289
|
const tractability = {
|
|
281
|
-
smallMolecule:
|
|
282
|
-
antibody:
|
|
283
|
-
otherModalities:
|
|
290
|
+
smallMolecule: positiveLabels("SM"),
|
|
291
|
+
antibody: positiveLabels("AB"),
|
|
292
|
+
otherModalities: positiveLabels("OC"),
|
|
284
293
|
};
|
|
285
294
|
const rows = data.associations?.associatedDiseases?.rows ?? [];
|
|
286
295
|
const associations = rows.map((row) => ({
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { ok } from "neverthrow";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
8
|
-
import { pairSafeIndex, READ_TOOL_OUTPUT_TOOL_ID, TOOL_RESULT_CAP } from "../loop/tool-output.js";
|
|
8
|
+
import { pairSafeIndex, READ_TOOL_OUTPUT_TOOL_ID, TOOL_OUTPUT_KEEP_MAX, TOOL_RESULT_CAP } from "../loop/tool-output.js";
|
|
9
9
|
import { defineTool } from "./define-tool.js";
|
|
10
10
|
const DEFAULT_PAGE_CHARS = 8 * 1024;
|
|
11
11
|
const MAX_PAGE_CHARS = 16 * 1024;
|
|
@@ -105,9 +105,12 @@ function search(ref, text, start, limit, pattern) {
|
|
|
105
105
|
export function createReadToolOutputTool(store) {
|
|
106
106
|
return defineTool({
|
|
107
107
|
id: READ_TOOL_OUTPUT_TOOL_ID,
|
|
108
|
-
description: "Read the
|
|
109
|
-
|
|
110
|
-
|
|
108
|
+
description: "Read the kept text of a tool result that came back as an excerpt. A tool result longer than " +
|
|
109
|
+
`${TOOL_RESULT_CAP} characters comes back as its start and its end, and the excerpt gives a reference when the harness kept the text. ` +
|
|
110
|
+
`Pass that reference as \`ref\`. The harness keeps at most ${TOOL_OUTPUT_KEEP_MAX} characters. Of a longer text, it keeps the first ` +
|
|
111
|
+
"half and the last half of that bound, with a line between them that gives the count of characters not kept. " +
|
|
112
|
+
"A reference that the harness did not keep gives the status `not_found`, and the rest of that result cannot be read. " +
|
|
113
|
+
"Read a window with `offset` and `limit`: an offset counts the characters " +
|
|
111
114
|
`(UTF-16 code units) of the kept text from 0, and a page has at most ${MAX_PAGE_CHARS} characters, ` +
|
|
112
115
|
`${DEFAULT_PAGE_CHARS} by default. Or give \`pattern\`, a JavaScript regular expression, to get at most ` +
|
|
113
116
|
`${MAX_MATCHES} matches, each with its offset and the text around it. When a result gives \`more: true\`, ` +
|
|
@@ -54,13 +54,20 @@ const blockPayload = z.union([AuthoringBlockSchema, z.unknown()]);
|
|
|
54
54
|
* present. Under `.optional()` each of those nulls fails the input parse, and the block never lands.
|
|
55
55
|
*/
|
|
56
56
|
const absentable = (schema) => schema.nullish();
|
|
57
|
+
/** The meaning of each flat destination field. The add and the move share it. */
|
|
58
|
+
const DESTINATION_NOTES = {
|
|
59
|
+
parentId: "The id of the section that holds the destination. Leave it out for the root. An anchor in `before` or `after` implies its parent.",
|
|
60
|
+
place: "Put the block at the `start` or the `end` of the parent. The default is `end`. Give a place or an anchor, not both.",
|
|
61
|
+
before: "The id of a sibling block. The block lands directly before it.",
|
|
62
|
+
after: "The id of a sibling block. The block lands directly after it.",
|
|
63
|
+
};
|
|
57
64
|
/** The flat destination fields. An anchor is `before` or `after`, and a place is `start` or `end`. */
|
|
58
65
|
const addBlockInput = z.object({
|
|
59
66
|
block: blockPayload,
|
|
60
|
-
parentId: absentable(z.string()),
|
|
61
|
-
place: absentable(z.enum(["start", "end"])),
|
|
62
|
-
before: absentable(z.string()),
|
|
63
|
-
after: absentable(z.string()),
|
|
67
|
+
parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
|
|
68
|
+
place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
|
|
69
|
+
before: absentable(z.string()).describe(DESTINATION_NOTES.before),
|
|
70
|
+
after: absentable(z.string()).describe(DESTINATION_NOTES.after),
|
|
64
71
|
});
|
|
65
72
|
/**
|
|
66
73
|
* The change payload is a title for a section target, or a block for an atom target, and exactly one of
|
|
@@ -68,27 +75,27 @@ const addBlockInput = z.object({
|
|
|
68
75
|
* `z.unknown()` as a required key, thus an optional marker is what makes the retitle call representable.
|
|
69
76
|
*/
|
|
70
77
|
const changeBlockInput = z.object({
|
|
71
|
-
targetId: z.string(),
|
|
72
|
-
title: absentable(z.string()),
|
|
73
|
-
block: blockPayload.optional(),
|
|
78
|
+
targetId: z.string().describe("The id of the block to change, as the outline gives it."),
|
|
79
|
+
title: absentable(z.string()).describe("The new title of a section target. Give a title or a block, not both."),
|
|
80
|
+
block: blockPayload.optional().describe("The replacement of an atom target. Give a title or a block, not both."),
|
|
74
81
|
});
|
|
75
82
|
const removeBlockInput = z.object({
|
|
76
|
-
targetId: z.string(),
|
|
83
|
+
targetId: z.string().describe("The id of the block to remove, as the outline gives it."),
|
|
77
84
|
});
|
|
78
85
|
const moveBlockInput = z.object({
|
|
79
|
-
targetId: z.string(),
|
|
80
|
-
parentId: absentable(z.string()),
|
|
81
|
-
place: absentable(z.enum(["start", "end"])),
|
|
82
|
-
before: absentable(z.string()),
|
|
83
|
-
after: absentable(z.string()),
|
|
86
|
+
targetId: z.string().describe("The id of the block to move, as the outline gives it."),
|
|
87
|
+
parentId: absentable(z.string()).describe(DESTINATION_NOTES.parentId),
|
|
88
|
+
place: absentable(z.enum(["start", "end"])).describe(DESTINATION_NOTES.place),
|
|
89
|
+
before: absentable(z.string()).describe(DESTINATION_NOTES.before),
|
|
90
|
+
after: absentable(z.string()).describe(DESTINATION_NOTES.after),
|
|
84
91
|
});
|
|
85
92
|
const readOutlineInput = z.object({});
|
|
86
93
|
/** The read names its target with the same field name as each mutation, thus one id key serves them all. */
|
|
87
94
|
const readBlockInput = z.object({
|
|
88
|
-
targetId: z.string(),
|
|
95
|
+
targetId: z.string().describe("The id of the block to read, as the outline gives it."),
|
|
89
96
|
});
|
|
90
97
|
const setTitleInput = z.object({
|
|
91
|
-
title: z.string().min(1),
|
|
98
|
+
title: z.string().min(1).describe("The title of the whole report."),
|
|
92
99
|
});
|
|
93
100
|
const finishDraftInput = z.object({});
|
|
94
101
|
/**
|
|
@@ -144,7 +144,11 @@ export function createDeriveTableTool(deps) {
|
|
|
144
144
|
"That directory is the one place that the script can write: the analysis mounts read-only, and the container has no network. " +
|
|
145
145
|
"The standard output carries the logs of the script alone, thus a table that it prints there reaches nobody. " +
|
|
146
146
|
"The derived table joins the pinned evidence of this session, thus a block binds its path the same way as a pinned artifact. " +
|
|
147
|
-
"A name that this session already derived is refused, thus a second derivation of one table takes a new name."
|
|
147
|
+
"A name that this session already derived is refused, thus a second derivation of one table takes a new name. " +
|
|
148
|
+
`Limits: the script holds at most ${String(SCRIPT_CAP_BYTES / 1024)} KiB, inputs holds at most ${String(INPUT_CAP)} paths, ` +
|
|
149
|
+
`and the run stops after ${String(DERIVATION_DEADLINE_MS / 1000)} seconds. ` +
|
|
150
|
+
"A failed script gives back exec-failed with the tail of its standard error. " +
|
|
151
|
+
"The outcome unavailable means that this host cannot run a derivation, and a repeat gives the same outcome.",
|
|
148
152
|
inputSchema: deriveTableInput,
|
|
149
153
|
executionMode: "inline",
|
|
150
154
|
describeCall: (input) => `derive ${input.output}`,
|
|
@@ -226,13 +226,14 @@ export function createExaminePageTool(deps) {
|
|
|
226
226
|
const releaseMs = deps.deadlines?.releaseMs ?? RELEASE_DEADLINE_MS;
|
|
227
227
|
return defineTool({
|
|
228
228
|
id: "examine_page",
|
|
229
|
-
description: "Open the rendered report page in a real headless browser, and
|
|
230
|
-
"
|
|
229
|
+
description: "Open the rendered report page in a real headless browser, and give back what a reviewer sees: " +
|
|
230
|
+
"one or more screenshots, the coverage of the look, the console errors, and the failed requests. " +
|
|
231
231
|
"A tall page arrives as consecutive top-to-bottom slices in document order, and the tiles list names the rows of each slice. " +
|
|
232
232
|
"The coverage says whether the pictures hold the whole page, a truncated top portion of it, or the top window alone. " +
|
|
233
233
|
"Run it after the preview to look at the current page, and to confirm that the layout and the charts read clean. " +
|
|
234
|
-
"
|
|
235
|
-
"
|
|
234
|
+
"A look at the page of the current draft is what lets record_report_version record. " +
|
|
235
|
+
"The outcomes no-page and missed-stamp mean that no preview rendered the current page: run preview_report, then look again. " +
|
|
236
|
+
"The outcome no-browser means that this host gives no browser, and a repeat gives the same outcome.",
|
|
236
237
|
inputSchema: examinePageInput,
|
|
237
238
|
executionMode: "inline",
|
|
238
239
|
describeCall: "none",
|
|
@@ -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
|