@inflexa-ai/harness 0.39.2 → 0.40.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -3
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +13 -2
- package/dist/agents/report-session-agent.d.ts +3 -0
- package/dist/agents/report-session-agent.js +2 -0
- package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
- package/dist/agents/sandbox/shared.d.ts +11 -2
- package/dist/agents/sandbox/shared.js +4 -0
- package/dist/app/chat-turn.d.ts +75 -12
- package/dist/app/chat-turn.js +149 -21
- package/dist/app/message-assembly.d.ts +16 -52
- package/dist/app/message-assembly.js +41 -68
- package/dist/app/run-activity.d.ts +4 -6
- package/dist/app/run-activity.js +7 -27
- package/dist/app/spawn-report-session.d.ts +6 -11
- package/dist/app/spawn-report-session.js +5 -6
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +1 -0
- package/dist/billing/usage-recorder.d.ts +3 -4
- package/dist/billing/usage-recorder.js +3 -4
- package/dist/contracts/chat-parts.d.ts +11 -1
- package/dist/contracts/chat-parts.js +1 -0
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/report-blocks.d.ts +8 -8
- package/dist/contracts/schemas/chat-parts.d.ts +32 -9
- package/dist/contracts/schemas/chat-parts.js +10 -0
- package/dist/execution/artifact-metadata.d.ts +26 -33
- package/dist/execution/artifact-metadata.js +44 -106
- package/dist/execution/post-step-pipeline.d.ts +23 -16
- package/dist/execution/post-step-pipeline.js +19 -21
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +11 -1
- package/dist/execution/step-summary.d.ts +21 -30
- package/dist/execution/step-summary.js +30 -57
- package/dist/index.d.ts +9 -8
- package/dist/index.js +15 -34
- package/dist/lib/zod-issues.js +2 -4
- package/dist/loop/compaction.d.ts +20 -0
- package/dist/loop/compaction.js +2 -0
- package/dist/loop/continue-agent.d.ts +40 -0
- package/dist/loop/continue-agent.js +38 -0
- package/dist/loop/metrics.d.ts +30 -22
- package/dist/loop/metrics.js +51 -22
- package/dist/loop/run-agent.d.ts +111 -13
- package/dist/loop/run-agent.js +467 -220
- package/dist/loop/run-to-terminal.d.ts +16 -17
- package/dist/loop/run-to-terminal.js +24 -35
- package/dist/loop/tool-mask.d.ts +23 -0
- package/dist/loop/tool-mask.js +46 -0
- package/dist/loop/tool-output.d.ts +48 -0
- package/dist/loop/tool-output.js +79 -0
- package/dist/loop/types.d.ts +6 -3
- package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
- package/dist/memory/ai-sdk-message-storage.js +78 -2
- package/dist/memory/conversation-display-recorder.d.ts +6 -0
- package/dist/memory/conversation-display-recorder.js +64 -22
- package/dist/memory/conversation-display-replay.d.ts +5 -14
- package/dist/memory/conversation-display-replay.js +66 -32
- package/dist/memory/conversation-display-storage.d.ts +2 -1
- package/dist/memory/conversation-display-storage.js +2 -1
- package/dist/memory/conversation-view.d.ts +53 -0
- package/dist/memory/conversation-view.js +130 -0
- package/dist/memory/count-tokens.d.ts +5 -7
- package/dist/memory/count-tokens.js +7 -9
- package/dist/memory/thread-history.d.ts +74 -102
- package/dist/memory/thread-history.js +181 -246
- package/dist/memory/thread-store.d.ts +3 -3
- package/dist/memory/thread-store.js +7 -2
- package/dist/memory/tool-call-integrity.d.ts +15 -21
- package/dist/memory/tool-call-integrity.js +31 -38
- package/dist/memory/working-memory.d.ts +2 -5
- package/dist/memory/working-memory.js +2 -5
- package/dist/prompts/SOUL.d.ts +2 -2
- package/dist/prompts/SOUL.js +2 -7
- package/dist/prompts/analogical-reasoner.d.ts +1 -1
- package/dist/prompts/analogical-reasoner.js +16 -32
- package/dist/prompts/briefing.js +1 -1
- package/dist/prompts/compaction.d.ts +4 -0
- package/dist/prompts/compaction.js +24 -0
- package/dist/prompts/conversation.d.ts +1 -1
- package/dist/prompts/conversation.js +26 -35
- package/dist/prompts/execute-analysis/step-summary.js +7 -5
- package/dist/prompts/literature-reviewer.d.ts +1 -1
- package/dist/prompts/literature-reviewer.js +29 -34
- package/dist/prompts/planner.js +15 -29
- package/dist/prompts/report-session.d.ts +1 -1
- package/dist/prompts/report-session.js +2 -33
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
- package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +1 -2
- package/dist/prompts/sandbox/data-profiler.js +6 -3
- package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
- package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
- package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
- package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
- package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
- package/dist/prompts/sandbox/enrichment-agent.js +3 -2
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
- package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
- package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
- package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
- package/dist/prompts/sandbox/microbiome-agent.js +14 -21
- package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
- package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
- package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
- package/dist/prompts/sandbox/network-agent.d.ts +1 -1
- package/dist/prompts/sandbox/network-agent.js +7 -8
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
- package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
- package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/proteomics-agent.js +3 -4
- package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
- package/dist/prompts/sandbox/scientific-executor.js +7 -10
- package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
- package/dist/prompts/sandbox/single-cell-agent.js +2 -4
- package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
- package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
- package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
- package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
- package/dist/prompts/sandbox-standards.d.ts +7 -7
- package/dist/prompts/sandbox-standards.js +74 -76
- package/dist/prompts/synthesis-agent.d.ts +1 -1
- package/dist/prompts/synthesis-agent.js +16 -42
- package/dist/providers/ai-sdk.d.ts +39 -6
- package/dist/providers/ai-sdk.js +117 -18
- package/dist/providers/errors.d.ts +2 -0
- package/dist/providers/errors.js +5 -0
- package/dist/providers/prompt-cache.d.ts +43 -41
- package/dist/providers/prompt-cache.js +47 -41
- package/dist/providers/reasoning.d.ts +10 -21
- package/dist/providers/reasoning.js +10 -21
- package/dist/providers/types.d.ts +21 -5
- package/dist/report-model/authoring-grammar.d.ts +2 -2
- package/dist/runtime/assemble.d.ts +9 -7
- package/dist/runtime/assemble.js +7 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/submit-exec.d.ts +1 -1
- package/dist/schemas/artifact-manifest.d.ts +4 -4
- package/dist/state/init.js +39 -4
- package/dist/state/purge-analysis.js +4 -0
- package/dist/state/tool-outputs.d.ts +4 -0
- package/dist/state/tool-outputs.js +30 -0
- package/dist/tasks/data-profile.d.ts +3 -0
- package/dist/tasks/data-profile.js +2 -0
- package/dist/tools/ad-hoc-router.d.ts +8 -0
- package/dist/tools/ad-hoc-router.js +21 -2
- package/dist/tools/display/show-user.d.ts +1 -1
- package/dist/tools/execute-analysis.d.ts +3 -0
- package/dist/tools/execute-analysis.js +9 -1
- package/dist/tools/read-tool-output.d.ts +7 -0
- package/dist/tools/read-tool-output.js +130 -0
- package/dist/tools/research/generate-analogy-report.d.ts +10 -26
- package/dist/tools/research/generate-analogy-report.js +76 -163
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +4 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +3 -0
- package/dist/tools/sandbox/report-blocker.d.ts +18 -5
- package/dist/tools/sandbox/report-blocker.js +54 -6
- package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
- package/dist/tools/sandbox/submit-file-metadata.js +67 -0
- package/dist/tools/start-report-session.d.ts +1 -3
- package/dist/tools/start-report-session.js +0 -2
- package/dist/tools/workspace/execute-command.js +7 -4
- package/dist/tools/workspace/result-bounds.d.ts +4 -4
- package/dist/tools/workspace/result-bounds.js +6 -5
- package/dist/tools/workspace/workspace-search.d.ts +1 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +1 -0
- package/dist/workflows/sandbox-step.d.ts +30 -9
- package/dist/workflows/sandbox-step.js +37 -8
- package/package.json +9 -9
- package/dist/providers/anthropic.d.ts +0 -25
- package/dist/providers/anthropic.js +0 -28
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `read_tool_output` — reads the kept text of a tool result that the loop cut, by a window or by a pattern.
|
|
3
|
+
* Each result stays under the cap of the loop, thus the loop never cuts a result of this tool.
|
|
4
|
+
*/
|
|
5
|
+
import { ok } from "neverthrow";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { unwrapOrThrow } from "../lib/result.js";
|
|
8
|
+
import { pairSafeIndex, READ_TOOL_OUTPUT_TOOL_ID, TOOL_RESULT_CAP } from "../loop/tool-output.js";
|
|
9
|
+
import { defineTool } from "./define-tool.js";
|
|
10
|
+
const DEFAULT_PAGE_CHARS = 8 * 1024;
|
|
11
|
+
const MAX_PAGE_CHARS = 16 * 1024;
|
|
12
|
+
const MAX_MATCHES = 20;
|
|
13
|
+
const MATCH_CHARS_BEFORE = 150;
|
|
14
|
+
const MATCH_CHARS_AFTER = 250;
|
|
15
|
+
/** The loop JSON-escapes the text of a result, and an escape can make a text two to six times longer. */
|
|
16
|
+
const RESULT_BUDGET = TOOL_RESULT_CAP - 512;
|
|
17
|
+
const ReadToolOutputInputSchema = z.object({
|
|
18
|
+
ref: z.string().min(1).max(64).describe("The reference that the excerpt of a cut tool result gives, for example to_3f9a2c41b8d605e7a1c0."),
|
|
19
|
+
offset: z.number().int().min(0).optional().describe("The index of the first character of the window, from 0. Defaults to 0."),
|
|
20
|
+
limit: z
|
|
21
|
+
.number()
|
|
22
|
+
.int()
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(MAX_PAGE_CHARS)
|
|
25
|
+
.optional()
|
|
26
|
+
.describe(`The count of characters of the window. A page defaults to ${DEFAULT_PAGE_CHARS}. A search with no limit reaches the end of the text.`),
|
|
27
|
+
pattern: z
|
|
28
|
+
.string()
|
|
29
|
+
.min(1)
|
|
30
|
+
.max(1024)
|
|
31
|
+
.optional()
|
|
32
|
+
.describe("A JavaScript regular expression. The result gives each match in the window with its offset and the text around it."),
|
|
33
|
+
});
|
|
34
|
+
/** The end of a window of `length` characters from `start`, off the middle of a surrogate pair, and past `start`. */
|
|
35
|
+
function windowEnd(text, start, length) {
|
|
36
|
+
const end = Math.min(text.length, start + length);
|
|
37
|
+
const safe = pairSafeIndex(text, end, -1);
|
|
38
|
+
return safe > start ? safe : pairSafeIndex(text, end, 1);
|
|
39
|
+
}
|
|
40
|
+
function fits(result) {
|
|
41
|
+
return JSON.stringify(result).length <= RESULT_BUDGET;
|
|
42
|
+
}
|
|
43
|
+
function page(ref, text, start, limit) {
|
|
44
|
+
const at = (stoppedAt) => ({
|
|
45
|
+
status: "ok",
|
|
46
|
+
ref,
|
|
47
|
+
offset: start,
|
|
48
|
+
end: stoppedAt,
|
|
49
|
+
keptLength: text.length,
|
|
50
|
+
more: stoppedAt < text.length,
|
|
51
|
+
text: text.slice(start, stoppedAt),
|
|
52
|
+
});
|
|
53
|
+
let end = windowEnd(text, start, limit);
|
|
54
|
+
let result = at(end);
|
|
55
|
+
while (!fits(result)) {
|
|
56
|
+
end = windowEnd(text, start, Math.floor(((end - start) * 3) / 4));
|
|
57
|
+
result = at(end);
|
|
58
|
+
}
|
|
59
|
+
return result;
|
|
60
|
+
}
|
|
61
|
+
function search(ref, text, start, limit, pattern) {
|
|
62
|
+
let regex;
|
|
63
|
+
try {
|
|
64
|
+
regex = new RegExp(pattern, "g");
|
|
65
|
+
}
|
|
66
|
+
catch (err) {
|
|
67
|
+
return { status: "invalid_pattern", pattern, error: err instanceof Error ? err.message : String(err) };
|
|
68
|
+
}
|
|
69
|
+
const stop = limit === undefined ? text.length : windowEnd(text, start, limit);
|
|
70
|
+
const window = text.slice(start, stop);
|
|
71
|
+
const matches = [];
|
|
72
|
+
let end = stop;
|
|
73
|
+
for (let found = regex.exec(window); found !== null; found = regex.exec(window)) {
|
|
74
|
+
const offset = start + found.index;
|
|
75
|
+
// A match past the last one that the result can hold is where the next call continues.
|
|
76
|
+
if (matches.length === MAX_MATCHES) {
|
|
77
|
+
end = offset;
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
const from = pairSafeIndex(text, Math.max(0, offset - MATCH_CHARS_BEFORE), 1);
|
|
81
|
+
const to = pairSafeIndex(text, Math.min(text.length, offset + MATCH_CHARS_AFTER), -1);
|
|
82
|
+
matches.push({ offset, text: text.slice(from, to) });
|
|
83
|
+
if (found[0].length === 0)
|
|
84
|
+
regex.lastIndex++;
|
|
85
|
+
}
|
|
86
|
+
if (matches.length === 0)
|
|
87
|
+
return { status: "no_matches", ref, pattern, offset: start, end };
|
|
88
|
+
const at = (stoppedAt) => ({
|
|
89
|
+
status: "matches",
|
|
90
|
+
ref,
|
|
91
|
+
pattern,
|
|
92
|
+
offset: start,
|
|
93
|
+
end: stoppedAt,
|
|
94
|
+
keptLength: text.length,
|
|
95
|
+
more: stoppedAt < text.length,
|
|
96
|
+
matches: [...matches],
|
|
97
|
+
});
|
|
98
|
+
let result = at(end);
|
|
99
|
+
while (!fits(result) && matches.length > 1) {
|
|
100
|
+
end = matches.pop().offset;
|
|
101
|
+
result = at(end);
|
|
102
|
+
}
|
|
103
|
+
return result;
|
|
104
|
+
}
|
|
105
|
+
export function createReadToolOutputTool(store) {
|
|
106
|
+
return defineTool({
|
|
107
|
+
id: READ_TOOL_OUTPUT_TOOL_ID,
|
|
108
|
+
description: "Read the full text of a tool result that came back as an excerpt. A result longer than the limit of the context " +
|
|
109
|
+
"comes back as its start and its end, and the excerpt gives a reference when the harness kept the whole text. " +
|
|
110
|
+
"Pass that reference as `ref`. Read a window with `offset` and `limit`: an offset counts the characters " +
|
|
111
|
+
`(UTF-16 code units) of the kept text from 0, and a page has at most ${MAX_PAGE_CHARS} characters, ` +
|
|
112
|
+
`${DEFAULT_PAGE_CHARS} by default. Or give \`pattern\`, a JavaScript regular expression, to get at most ` +
|
|
113
|
+
`${MAX_MATCHES} matches, each with its offset and the text around it. When a result gives \`more: true\`, ` +
|
|
114
|
+
"call again with `offset` set to its `end`.",
|
|
115
|
+
inputSchema: ReadToolOutputInputSchema,
|
|
116
|
+
// The reference alone repeats across the pages of one text, and the pattern is what tells two searches apart.
|
|
117
|
+
describeCall: ({ ref, pattern }) => (pattern === undefined ? ref : `${pattern} in ${ref}`),
|
|
118
|
+
execute: async ({ ref, offset = 0, limit, pattern }, ctx) => {
|
|
119
|
+
// The analysis of the session scopes the read, thus a reference cannot reach a text of a different analysis.
|
|
120
|
+
const kept = unwrapOrThrow(await store.get(ctx.session.scope.analysisId, ref));
|
|
121
|
+
if (kept === null)
|
|
122
|
+
return ok({ status: "not_found", ref });
|
|
123
|
+
const text = kept.content;
|
|
124
|
+
if (offset >= text.length)
|
|
125
|
+
return ok({ status: "out_of_range", ref, offset, keptLength: text.length });
|
|
126
|
+
const start = pairSafeIndex(text, offset, -1);
|
|
127
|
+
return ok(pattern === undefined ? page(ref, text, start, limit ?? DEFAULT_PAGE_CHARS) : search(ref, text, start, limit, pattern));
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
@@ -2,18 +2,14 @@
|
|
|
2
2
|
* generateAnalogyReport — cross-domain analogy report as a sub-agent tool.
|
|
3
3
|
*
|
|
4
4
|
* An inline harness `AgentDefinition` (mirroring `createLiteratureReviewerTool`)
|
|
5
|
-
* driven by `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* error envelope surfaces — the frontend never has to render raw prose.
|
|
10
|
-
*
|
|
11
|
-
* One provider serves both the research agent and the conversion retry; the
|
|
12
|
-
* wrapper's parse + validate + error-envelope cascade is the correctness
|
|
13
|
-
* boundary.
|
|
5
|
+
* driven by `runToTerminal`. The result reaches the wrapper through one of two
|
|
6
|
+
* terminal tools because the output envelope is a union and `defineTool` takes
|
|
7
|
+
* only a top-level object. A run with no outcome gets one salvage continuation,
|
|
8
|
+
* then an `extraction-failed` envelope, so the frontend never renders raw prose.
|
|
14
9
|
*/
|
|
15
10
|
import { z } from "zod";
|
|
16
|
-
import {
|
|
11
|
+
import type { Logger } from "../../lib/logger.js";
|
|
12
|
+
import type { ToolOutputStore } from "../../loop/tool-output.js";
|
|
17
13
|
import type { ChatProvider } from "../../providers/types.js";
|
|
18
14
|
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
19
15
|
import { type Tool } from "../define-tool.js";
|
|
@@ -42,22 +38,10 @@ export interface GenerateAnalogyReportDeps {
|
|
|
42
38
|
readonly bioKeys: BioToolKeys;
|
|
43
39
|
/** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
|
|
44
40
|
readonly usageRecorder?: UsageRecorder;
|
|
41
|
+
/** The store of the child loop and of its read tool. */
|
|
42
|
+
readonly toolOutputStore?: ToolOutputStore;
|
|
43
|
+
/** Logging seam; omitted falls back to no-op. Logs the salvage warning and an accounting error of the reasoner loop. */
|
|
44
|
+
readonly logger?: Logger;
|
|
45
45
|
}
|
|
46
46
|
/** Build the `generate_analogy_report` delegation tool bound to its provider. */
|
|
47
47
|
export declare function createGenerateAnalogyReportTool(deps: GenerateAnalogyReportDeps): Tool;
|
|
48
|
-
interface ParseSuccess {
|
|
49
|
-
ok: true;
|
|
50
|
-
value: AnalogicalReasonerOutput;
|
|
51
|
-
}
|
|
52
|
-
interface ParseFailure {
|
|
53
|
-
ok: false;
|
|
54
|
-
reason: "not-json" | "schema-mismatch";
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Trim leading whitespace and strip a single wrapping ```json fence before
|
|
58
|
-
* parsing — the inner agent's prompt forbids code fences but a relaxed
|
|
59
|
-
* parse here avoids burning a conversion retry on trivial slips. Beyond
|
|
60
|
-
* that, no salvaging. Exported for unit testing.
|
|
61
|
-
*/
|
|
62
|
-
export declare function tryParseEnvelope(raw: string): ParseSuccess | ParseFailure;
|
|
63
|
-
export {};
|
|
@@ -2,26 +2,23 @@
|
|
|
2
2
|
* generateAnalogyReport — cross-domain analogy report as a sub-agent tool.
|
|
3
3
|
*
|
|
4
4
|
* An inline harness `AgentDefinition` (mirroring `createLiteratureReviewerTool`)
|
|
5
|
-
* driven by `
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* error envelope surfaces — the frontend never has to render raw prose.
|
|
10
|
-
*
|
|
11
|
-
* One provider serves both the research agent and the conversion retry; the
|
|
12
|
-
* wrapper's parse + validate + error-envelope cascade is the correctness
|
|
13
|
-
* boundary.
|
|
5
|
+
* driven by `runToTerminal`. The result reaches the wrapper through one of two
|
|
6
|
+
* terminal tools because the output envelope is a union and `defineTool` takes
|
|
7
|
+
* only a top-level object. A run with no outcome gets one salvage continuation,
|
|
8
|
+
* then an `extraction-failed` envelope, so the frontend never renders raw prose.
|
|
14
9
|
*/
|
|
15
10
|
import { ok } from "neverthrow";
|
|
16
11
|
import { z } from "zod";
|
|
17
|
-
import {
|
|
12
|
+
import { AnalogyReportSchema } from "@inflexa-ai/harness/contracts/analogy-report.js";
|
|
18
13
|
import { analogicalReasonerPrompt } from "../../prompts/analogical-reasoner.js";
|
|
19
14
|
import { composeSystemPrompt } from "../../agents/system-prompt.js";
|
|
20
15
|
import { forSubAgent } from "../../auth/types.js";
|
|
21
|
-
import {
|
|
22
|
-
import { finalText, runAgent } from "../../loop/run-agent.js";
|
|
16
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
23
17
|
import { passthroughStep } from "../../loop/run-step.js";
|
|
18
|
+
import { runToTerminal } from "../../loop/run-to-terminal.js";
|
|
24
19
|
import { defineTool } from "../define-tool.js";
|
|
20
|
+
import { createReadToolOutputTool } from "../read-tool-output.js";
|
|
21
|
+
import { createReportBlockerToolFor } from "../sandbox/report-blocker.js";
|
|
25
22
|
// Cross-domain search tools the analogical-reasoner uses.
|
|
26
23
|
import { createSearchSemanticScholarTool } from "./search-semantic-scholar.js";
|
|
27
24
|
import { searchArxivTool } from "./search-arxiv.js";
|
|
@@ -32,6 +29,41 @@ import { createNcbiTools } from "../bio/keys.js";
|
|
|
32
29
|
const AGENT_ID = "analogical-reasoner";
|
|
33
30
|
/** Tool-call budget for the inner research agent. */
|
|
34
31
|
const RESEARCH_MAX_ITERATIONS = 40;
|
|
32
|
+
const ANALOGY_SALVAGE_NUDGE = "You ended without a terminal outcome. Call submit_analogy_report with " +
|
|
33
|
+
"your report now, or report_blocker if phase 1 cannot run. Do not reply " +
|
|
34
|
+
"with prose.";
|
|
35
|
+
function createSubmitAnalogyReportTool(cell) {
|
|
36
|
+
return defineTool({
|
|
37
|
+
id: "submit_analogy_report",
|
|
38
|
+
description: "Terminal. Submit the analogy report: the problem summary, objects, " +
|
|
39
|
+
"relations, and key terms of phase 1, and each analogy with its " +
|
|
40
|
+
"coverage and its cited solutions from phase 2. Call it one time with " +
|
|
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.",
|
|
43
|
+
inputSchema: AnalogyReportSchema,
|
|
44
|
+
describeCall: "none",
|
|
45
|
+
execute: async (report) => {
|
|
46
|
+
// A report replaces a blocker, thus the report wins when one round records both.
|
|
47
|
+
cell.outcome = { kind: "report", report };
|
|
48
|
+
return ok({
|
|
49
|
+
recorded: true,
|
|
50
|
+
message: "Report recorded. You are done — stop now and do not take further actions.",
|
|
51
|
+
});
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
function createAnalogyBlockerTool(cell) {
|
|
56
|
+
return createReportBlockerToolFor({
|
|
57
|
+
// A blocker never replaces a recorded outcome, thus it never replaces a report.
|
|
58
|
+
record: (outcome) => {
|
|
59
|
+
if (cell.outcome === null)
|
|
60
|
+
cell.outcome = outcome;
|
|
61
|
+
},
|
|
62
|
+
blockedWhen: "Ends the analogy research with no report. Call it only when the " +
|
|
63
|
+
"problem is empty or incoherent, thus phase 1 cannot extract its " +
|
|
64
|
+
"objects and relations.",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
35
67
|
export const generateAnalogyReportInputSchema = z.object({
|
|
36
68
|
problem: z
|
|
37
69
|
.string()
|
|
@@ -86,88 +118,18 @@ export function buildResearchPrompt(input) {
|
|
|
86
118
|
}
|
|
87
119
|
return lines.join("\n");
|
|
88
120
|
}
|
|
89
|
-
/**
|
|
90
|
-
* Conversion prompt the retry uses when the inner research agent's output
|
|
91
|
-
* is not valid JSON. Quotes the raw output verbatim and asks for a strict
|
|
92
|
-
* AnalogyReportSchema-shaped object back. No tools — single shot.
|
|
93
|
-
*/
|
|
94
|
-
function buildConversionPrompt(rawOutput) {
|
|
95
|
-
return [
|
|
96
|
-
"Convert the analogical-reasoner output below into a single JSON",
|
|
97
|
-
"object matching the AnalogyReport schema. Return ONLY raw JSON — no",
|
|
98
|
-
"prose, no markdown fences, no commentary, no apology. The first",
|
|
99
|
-
"character of your response must be `{`. The final character must be `}`.",
|
|
100
|
-
"",
|
|
101
|
-
"## Required shape (AnalogyReportSchema)",
|
|
102
|
-
"",
|
|
103
|
-
"```json",
|
|
104
|
-
"{",
|
|
105
|
-
' "schemaVersion": "1",',
|
|
106
|
-
' "problemSummary": "1-2 sentence summary of the source problem.",',
|
|
107
|
-
' "problemObjects": [{ "name": "...", "role": "..." }],',
|
|
108
|
-
' "problemRelations": ["short bullet phrase", "..."],',
|
|
109
|
-
' "keyTerms": ["term1", "term2"],',
|
|
110
|
-
' "analogies": [',
|
|
111
|
-
" {",
|
|
112
|
-
' "targetDomain": "control_theory",',
|
|
113
|
-
' "analogyTitle": "Adaptive control of an unseen plant",',
|
|
114
|
-
' "objectMappings": [{ "source": "...", "target": "...", "rationale": "..." }],',
|
|
115
|
-
' "sharedRelations": "...",',
|
|
116
|
-
' "coverage": "available",',
|
|
117
|
-
' "solutions": [',
|
|
118
|
-
" {",
|
|
119
|
-
' "title": "Method or paper title (verbatim from input)",',
|
|
120
|
-
' "sourceDomain": "control_theory",',
|
|
121
|
-
' "description": "2-3 sentences.",',
|
|
122
|
-
' "keyConcepts": ["..."],',
|
|
123
|
-
' "relevance": "How this transfers back to the source problem.",',
|
|
124
|
-
' "sources": [{ "url": "https://...", "title": "Exact paper title" }],',
|
|
125
|
-
' "githubRepos": []',
|
|
126
|
-
" }",
|
|
127
|
-
" ]",
|
|
128
|
-
" }",
|
|
129
|
-
" ]",
|
|
130
|
-
"}",
|
|
131
|
-
"```",
|
|
132
|
-
"",
|
|
133
|
-
"## Field rules",
|
|
134
|
-
"",
|
|
135
|
-
"- Preserve every analogy, object mapping, shared relation, solution, and",
|
|
136
|
-
" citation that appears in the input. Do not invent content.",
|
|
137
|
-
"- Each analogy needs `coverage`:",
|
|
138
|
-
" - `available` when the input lists concrete solutions for it,",
|
|
139
|
-
" - `queried_no_data` when the input says searches returned no usable results,",
|
|
140
|
-
" - `search_failed` when the input says the search tool errored out,",
|
|
141
|
-
" - `not_loaded` when the input says the analogy was skipped (budget).",
|
|
142
|
-
" When `coverage` is not `available`, `solutions` must be `[]`.",
|
|
143
|
-
"- Each `solutions[].sources[]` entry needs a valid `url` (http/https)",
|
|
144
|
-
" AND a `title`. If a paper is mentioned without a URL, omit that source.",
|
|
145
|
-
"- `githubRepos` must always be an array (use `[]` when not mentioned).",
|
|
146
|
-
"- If the input has no extractable analogy content at all (pure apology,",
|
|
147
|
-
" empty, off-topic), return the envelope with `analogies: []`. The",
|
|
148
|
-
" wrapper translates that into an error envelope.",
|
|
149
|
-
"",
|
|
150
|
-
"--- INPUT ---",
|
|
151
|
-
rawOutput,
|
|
152
|
-
"--- END INPUT ---",
|
|
153
|
-
].join("\n");
|
|
154
|
-
}
|
|
155
121
|
/** Build the `generate_analogy_report` delegation tool bound to its provider. */
|
|
156
122
|
export function createGenerateAnalogyReportTool(deps) {
|
|
123
|
+
const logger = (deps.logger ?? createNoopLogger()).named("generate-analogy-report");
|
|
157
124
|
const ncbi = createNcbiTools(deps.bioKeys);
|
|
158
|
-
const
|
|
125
|
+
const searchTools = [
|
|
159
126
|
createSearchSemanticScholarTool({ ...(deps.bioKeys.semanticScholar === undefined ? {} : { apiKey: deps.bioKeys.semanticScholar }) }),
|
|
160
127
|
searchArxivTool,
|
|
161
128
|
createSearchGithubReposTool({ githubToken: deps.bioKeys.github }),
|
|
162
129
|
ncbi.pubmed,
|
|
130
|
+
...(deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : []),
|
|
163
131
|
];
|
|
164
|
-
const
|
|
165
|
-
id: AGENT_ID,
|
|
166
|
-
systemPrompt: composeSystemPrompt(analogicalReasonerPrompt),
|
|
167
|
-
model: deps.model,
|
|
168
|
-
tools: reasonerTools,
|
|
169
|
-
maxIterations: RESEARCH_MAX_ITERATIONS,
|
|
170
|
-
};
|
|
132
|
+
const systemPrompt = composeSystemPrompt(analogicalReasonerPrompt);
|
|
171
133
|
return defineTool({
|
|
172
134
|
id: "generate_analogy_report",
|
|
173
135
|
description: "Idea-generation engine. Extracts structural analogies for an open-ended " +
|
|
@@ -189,29 +151,41 @@ export function createGenerateAnalogyReportTool(deps) {
|
|
|
189
151
|
"`solutions` array — informational, NOT an error. A top-level `error` " +
|
|
190
152
|
"field means no report was produced: on `error.kind === " +
|
|
191
153
|
'"extraction-failed"` do NOT retry with the same or a similar problem ' +
|
|
192
|
-
"statement (the wrapper already
|
|
193
|
-
"latency on the same failure). Surface the message and ask the
|
|
194
|
-
"narrow the 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.",
|
|
195
157
|
inputSchema: generateAnalogyReportInputSchema,
|
|
196
158
|
describeCall: "none",
|
|
197
159
|
execute: async (input, ctx) => {
|
|
198
160
|
const childSession = forSubAgent(ctx.session, AGENT_ID);
|
|
199
|
-
//
|
|
200
|
-
|
|
161
|
+
// Each call builds fresh tool closures over its own cell. Their order and
|
|
162
|
+
// definitions stay fixed across calls so the cache-keyed prefix holds.
|
|
163
|
+
const cell = { outcome: null };
|
|
164
|
+
const submitReportTool = createSubmitAnalogyReportTool(cell);
|
|
165
|
+
const blockerTool = createAnalogyBlockerTool(cell);
|
|
166
|
+
const agent = {
|
|
167
|
+
id: AGENT_ID,
|
|
168
|
+
systemPrompt,
|
|
169
|
+
model: deps.model,
|
|
170
|
+
tools: [...searchTools, submitReportTool, blockerTool],
|
|
171
|
+
maxIterations: RESEARCH_MAX_ITERATIONS,
|
|
172
|
+
};
|
|
201
173
|
try {
|
|
202
|
-
|
|
174
|
+
await runToTerminal(agent, [{ role: "user", content: buildResearchPrompt(input) }], childSession, {
|
|
203
175
|
provider: deps.provider,
|
|
204
176
|
signal: ctx.signal,
|
|
205
177
|
emit: ctx.emit,
|
|
206
178
|
runStep: passthroughStep,
|
|
179
|
+
resolved: () => cell.outcome !== null,
|
|
180
|
+
logger,
|
|
207
181
|
usageRecorder: deps.usageRecorder,
|
|
182
|
+
toolOutputStore: deps.toolOutputStore,
|
|
208
183
|
// Fold the child's calls into the turn total the root loop reports.
|
|
209
184
|
turnUsage: ctx.turnUsage,
|
|
210
185
|
// Keeps the usage record keys of two parallel dispatches disjoint —
|
|
211
186
|
// same frame, same call path, same loop-local step names.
|
|
212
187
|
invocationId: ctx.invocationId,
|
|
213
|
-
});
|
|
214
|
-
rawText = finalText(transcript);
|
|
188
|
+
}, { tools: [submitReportTool, blockerTool], nudge: ANALOGY_SALVAGE_NUDGE });
|
|
215
189
|
}
|
|
216
190
|
catch (err) {
|
|
217
191
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -223,84 +197,23 @@ export function createGenerateAnalogyReportTool(deps) {
|
|
|
223
197
|
},
|
|
224
198
|
});
|
|
225
199
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
// raw output into a valid envelope. The parse+validate+envelope
|
|
232
|
-
// cascade below is the safety net.
|
|
233
|
-
try {
|
|
234
|
-
const reply = unwrapOrThrow(await deps.provider.chat({
|
|
235
|
-
tools: {},
|
|
236
|
-
toolChoice: "none",
|
|
237
|
-
system: "You convert an analogical-reasoner's free-text output " +
|
|
238
|
-
"into a strict AnalogyReportSchema JSON envelope. You " +
|
|
239
|
-
"preserve information faithfully and never invent content. " +
|
|
240
|
-
"You return ONLY raw JSON — no prose, no markdown fences, " +
|
|
241
|
-
"no commentary.",
|
|
242
|
-
messages: [{ role: "user", content: buildConversionPrompt(rawText) }],
|
|
243
|
-
}, childSession, ctx.signal));
|
|
244
|
-
const content = reply.message.content;
|
|
245
|
-
const convertedText = typeof content === "string"
|
|
246
|
-
? content
|
|
247
|
-
: content
|
|
248
|
-
.filter((b) => b.type === "text")
|
|
249
|
-
.map((b) => b.text)
|
|
250
|
-
.join("");
|
|
251
|
-
const parse = tryParseEnvelope(convertedText);
|
|
252
|
-
if (!parse.ok)
|
|
253
|
-
return ok(buildExtractionFailedEnvelope());
|
|
254
|
-
// If the conversion model produced an error envelope verbatim,
|
|
255
|
-
// surface it (it knows something we don't about the input).
|
|
256
|
-
if ("error" in parse.value)
|
|
257
|
-
return ok(parse.value);
|
|
258
|
-
// Empty `analogies: []` means the input had no extractable content
|
|
259
|
-
// (apology, off-topic) — surface as extraction-failed.
|
|
260
|
-
if (parse.value.analogies.length === 0) {
|
|
261
|
-
return ok(buildExtractionFailedEnvelope());
|
|
262
|
-
}
|
|
263
|
-
return ok(parse.value);
|
|
264
|
-
}
|
|
265
|
-
catch {
|
|
266
|
-
return ok(buildExtractionFailedEnvelope());
|
|
200
|
+
const outcome = cell.outcome;
|
|
201
|
+
if (outcome?.kind === "report")
|
|
202
|
+
return ok(outcome.report);
|
|
203
|
+
if (outcome?.kind === "blocker") {
|
|
204
|
+
return ok({ schemaVersion: "1", error: { kind: "extraction-failed", message: outcome.reason } });
|
|
267
205
|
}
|
|
206
|
+
return ok(buildExtractionFailedEnvelope());
|
|
268
207
|
},
|
|
269
208
|
});
|
|
270
209
|
}
|
|
271
|
-
/**
|
|
210
|
+
/** The envelope of a run that recorded no outcome, also after its salvage. */
|
|
272
211
|
function buildExtractionFailedEnvelope() {
|
|
273
212
|
return {
|
|
274
213
|
schemaVersion: "1",
|
|
275
214
|
error: {
|
|
276
215
|
kind: "extraction-failed",
|
|
277
|
-
message: "The analogical reasoner
|
|
216
|
+
message: "The analogical reasoner submitted no report and no blocker, also after a corrective request. Try narrowing the problem statement.",
|
|
278
217
|
},
|
|
279
218
|
};
|
|
280
219
|
}
|
|
281
|
-
/**
|
|
282
|
-
* Trim leading whitespace and strip a single wrapping ```json fence before
|
|
283
|
-
* parsing — the inner agent's prompt forbids code fences but a relaxed
|
|
284
|
-
* parse here avoids burning a conversion retry on trivial slips. Beyond
|
|
285
|
-
* that, no salvaging. Exported for unit testing.
|
|
286
|
-
*/
|
|
287
|
-
export function tryParseEnvelope(raw) {
|
|
288
|
-
const stripped = stripFence(raw);
|
|
289
|
-
let candidate;
|
|
290
|
-
try {
|
|
291
|
-
candidate = JSON.parse(stripped);
|
|
292
|
-
}
|
|
293
|
-
catch {
|
|
294
|
-
return { ok: false, reason: "not-json" };
|
|
295
|
-
}
|
|
296
|
-
const result = AnalogicalReasonerOutputSchema.safeParse(candidate);
|
|
297
|
-
if (!result.success)
|
|
298
|
-
return { ok: false, reason: "schema-mismatch" };
|
|
299
|
-
return { ok: true, value: result.data };
|
|
300
|
-
}
|
|
301
|
-
function stripFence(raw) {
|
|
302
|
-
const trimmed = raw.trim();
|
|
303
|
-
if (!trimmed.startsWith("```"))
|
|
304
|
-
return trimmed;
|
|
305
|
-
return trimmed.replace(/^```(?:json)?\s*\n/i, "").replace(/\n```\s*$/i, "");
|
|
306
|
-
}
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* terminal-tool surface above.
|
|
25
25
|
*/
|
|
26
26
|
import type { Pool } from "pg";
|
|
27
|
+
import type { ToolOutputStore } from "../../loop/tool-output.js";
|
|
27
28
|
import { type ChatProvider } from "../../providers/types.js";
|
|
28
29
|
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
29
30
|
import { type Tool } from "../define-tool.js";
|
|
@@ -53,6 +54,8 @@ export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
|
53
54
|
readonly logger?: Logger;
|
|
54
55
|
/** LLM usage-accounting seam for the planner loop; omitted falls back to the no-op recorder. */
|
|
55
56
|
readonly usageRecorder?: UsageRecorder;
|
|
57
|
+
/** The store of the planner loop and of its read tool. */
|
|
58
|
+
readonly toolOutputStore?: ToolOutputStore;
|
|
56
59
|
/** API keys for the search tools the planner uses to ground a plan. */
|
|
57
60
|
readonly bioKeys: BioToolKeys;
|
|
58
61
|
}
|
|
@@ -32,6 +32,7 @@ import { passthroughStep } from "../../loop/run-step.js";
|
|
|
32
32
|
import { forSubAgent, scopeResource } from "../../auth/types.js";
|
|
33
33
|
import { effectiveDeadlineMs } from "../../providers/types.js";
|
|
34
34
|
import { defineTool } from "../define-tool.js";
|
|
35
|
+
import { createReadToolOutputTool } from "../read-tool-output.js";
|
|
35
36
|
import { createListAvailablePackagesTool } from "../sandbox/list-available-packages.js";
|
|
36
37
|
import { createListAvailableRefsTool } from "../sandbox/list-available-refs.js";
|
|
37
38
|
import { createReportBlockerToolFor } from "../sandbox/report-blocker.js";
|
|
@@ -872,6 +873,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
872
873
|
// definitions are identical across invocations, thus the request prefix
|
|
873
874
|
// that the cache keys on does not move.
|
|
874
875
|
const searchTools = buildPlannerSearchTools(deps);
|
|
876
|
+
const readTools = deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : [];
|
|
875
877
|
const planner = {
|
|
876
878
|
id: PLANNER_AGENT_ID,
|
|
877
879
|
systemPrompt: composeSystemPrompt(plannerInstructions(deps.resourcePolicy)),
|
|
@@ -879,7 +881,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
879
881
|
// The search tools come first, and the terminal tools come last.
|
|
880
882
|
// The order is the order of the prompt, and it is stable across
|
|
881
883
|
// every invocation, thus the cached request prefix holds.
|
|
882
|
-
tools: [...searchTools, ...innerTools.terminal],
|
|
884
|
+
tools: [...searchTools, ...readTools, ...innerTools.terminal],
|
|
883
885
|
maxIterations: PLANNER_MAX_ITERATIONS,
|
|
884
886
|
};
|
|
885
887
|
// Merge the outer abort signal with a wall-clock timeout — either
|
|
@@ -899,6 +901,7 @@ export function createGeneratePlanTool(deps) {
|
|
|
899
901
|
// turn below recovers a run that ends on prose.
|
|
900
902
|
logger,
|
|
901
903
|
usageRecorder: deps.usageRecorder,
|
|
904
|
+
toolOutputStore: deps.toolOutputStore,
|
|
902
905
|
// Fold the planner's calls into the turn total the root loop reports.
|
|
903
906
|
turnUsage: ctx.turnUsage,
|
|
904
907
|
// Keeps the usage record keys of two parallel dispatches disjoint —
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* entire tool call is one step in the parent loop (PR #3 caches it whole,
|
|
15
15
|
* so the child never replays).
|
|
16
16
|
*/
|
|
17
|
+
import type { ToolOutputStore } from "../../loop/tool-output.js";
|
|
17
18
|
import { type ChatProvider } from "../../providers/types.js";
|
|
18
19
|
import { type Tool } from "../define-tool.js";
|
|
19
20
|
import { type BioToolKeys } from "../bio/keys.js";
|
|
@@ -31,6 +32,8 @@ export interface LiteratureReviewerDeps {
|
|
|
31
32
|
readonly bioKeys: BioToolKeys;
|
|
32
33
|
/** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
|
|
33
34
|
readonly usageRecorder?: UsageRecorder;
|
|
35
|
+
/** The store of the child loop and of its read tool. */
|
|
36
|
+
readonly toolOutputStore?: ToolOutputStore;
|
|
34
37
|
/** Shared bibliographic verification service; all agent surfaces share its pacing/cache. */
|
|
35
38
|
readonly citationResolver: CitationResolver;
|
|
36
39
|
}
|
|
@@ -22,6 +22,7 @@ import { literatureReviewerPrompt } from "../../prompts/literature-reviewer.js";
|
|
|
22
22
|
import { composeSystemPrompt } from "../../agents/system-prompt.js";
|
|
23
23
|
import { forSubAgent } from "../../auth/types.js";
|
|
24
24
|
import { defineTool } from "../define-tool.js";
|
|
25
|
+
import { createReadToolOutputTool } from "../read-tool-output.js";
|
|
25
26
|
import { createChemDbTools, createNcbiTools } from "../bio/keys.js";
|
|
26
27
|
import { genePreclinicalProfileTool } from "../bio/gene-preclinical-profile.js";
|
|
27
28
|
import { lookupAnnotationTool } from "../bio/lookup-annotation.js";
|
|
@@ -46,6 +47,7 @@ export function createLiteratureReviewerTool(deps) {
|
|
|
46
47
|
createResolveCitationTool(deps.citationResolver),
|
|
47
48
|
chemDb.drugGeneInteractions,
|
|
48
49
|
genePreclinicalProfileTool,
|
|
50
|
+
...(deps.toolOutputStore ? [createReadToolOutputTool(deps.toolOutputStore)] : []),
|
|
49
51
|
];
|
|
50
52
|
const agent = {
|
|
51
53
|
id: AGENT_ID,
|
|
@@ -78,6 +80,7 @@ export function createLiteratureReviewerTool(deps) {
|
|
|
78
80
|
emit: ctx.emit,
|
|
79
81
|
runStep: passthroughStep,
|
|
80
82
|
usageRecorder: deps.usageRecorder,
|
|
83
|
+
toolOutputStore: deps.toolOutputStore,
|
|
81
84
|
// Fold the child's calls into the turn total the root loop reports.
|
|
82
85
|
turnUsage: ctx.turnUsage,
|
|
83
86
|
// Keeps the usage record keys of two parallel reviews disjoint —
|
|
@@ -6,16 +6,21 @@
|
|
|
6
6
|
* `report_blocker` is the alternative to improvising an inline result the
|
|
7
7
|
* harness would launder into a green run.
|
|
8
8
|
*
|
|
9
|
-
* This module is the canonical home of the tool.
|
|
10
|
-
* sandbox step agent, the planner (`tools/research/generate-plan.ts`)
|
|
11
|
-
* run synthesizer (`execution/run-synthesis.ts`)
|
|
9
|
+
* This module is the canonical home of the tool. Four loops offer it — the
|
|
10
|
+
* sandbox step agent, the planner (`tools/research/generate-plan.ts`), the
|
|
11
|
+
* run synthesizer (`execution/run-synthesis.ts`) and the analogical reasoner
|
|
12
|
+
* (`tools/research/generate-analogy-report.ts`) — and all four share one id,
|
|
12
13
|
* one input schema and one terminal contract via `createReportBlockerToolFor`.
|
|
13
14
|
* What differs per loop is only what a blocker *means* there, so each call site
|
|
14
15
|
* injects its own `blockedWhen` prose and its own capture closure. The tool
|
|
15
|
-
* records the reason and tells the agent to stop; the driving body reads
|
|
16
|
-
*
|
|
16
|
+
* records the reason and tells the agent to stop; the driving body reads the
|
|
17
|
+
* outcome after the loop and decides the terminal status. The sandbox step body
|
|
18
|
+
* runs in durable steps, so it reads the blocker back from the transcript
|
|
19
|
+
* (`recordedBlocker`) on replay, not only from its cell.
|
|
17
20
|
*/
|
|
21
|
+
import type { ModelMessage } from "ai";
|
|
18
22
|
import { type Tool } from "../define-tool.js";
|
|
23
|
+
export declare const REPORT_BLOCKER_TOOL_ID = "report_blocker";
|
|
19
24
|
export interface BlockerOutcome {
|
|
20
25
|
readonly kind: "blocker";
|
|
21
26
|
readonly reason: string;
|
|
@@ -46,6 +51,14 @@ export interface ReportBlockerDeps {
|
|
|
46
51
|
* site supplies only the per-loop meaning and the capture closure.
|
|
47
52
|
*/
|
|
48
53
|
export declare function createReportBlockerToolFor(deps: ReportBlockerDeps): Tool;
|
|
54
|
+
/**
|
|
55
|
+
* The blocker that a run recorded, read from its transcript: the reason of the
|
|
56
|
+
* first `report_blocker` call, in call order, whose result is ok.
|
|
57
|
+
*
|
|
58
|
+
* A replay returns each tool step's cached result without re-running
|
|
59
|
+
* `execute`, so the in-memory cell is empty on replay; the transcript is not.
|
|
60
|
+
*/
|
|
61
|
+
export declare function recordedBlocker(messages: readonly ModelMessage[]): BlockerOutcome | null;
|
|
49
62
|
/**
|
|
50
63
|
* The sandbox step agent's `report_blocker`, bound to the step body's holder.
|
|
51
64
|
* A blocker here ends the step with no deliverables and a blocked reason.
|