@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.
Files changed (184) hide show
  1. package/README.md +2 -3
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +13 -2
  4. package/dist/agents/report-session-agent.d.ts +3 -0
  5. package/dist/agents/report-session-agent.js +2 -0
  6. package/dist/agents/sandbox/cheminformatics-agent.js +0 -3
  7. package/dist/agents/sandbox/shared.d.ts +11 -2
  8. package/dist/agents/sandbox/shared.js +4 -0
  9. package/dist/app/chat-turn.d.ts +75 -12
  10. package/dist/app/chat-turn.js +149 -21
  11. package/dist/app/message-assembly.d.ts +16 -52
  12. package/dist/app/message-assembly.js +41 -68
  13. package/dist/app/run-activity.d.ts +4 -6
  14. package/dist/app/run-activity.js +7 -27
  15. package/dist/app/spawn-report-session.d.ts +6 -11
  16. package/dist/app/spawn-report-session.js +5 -6
  17. package/dist/app/synthesize-run.d.ts +3 -0
  18. package/dist/app/synthesize-run.js +1 -0
  19. package/dist/billing/usage-recorder.d.ts +3 -4
  20. package/dist/billing/usage-recorder.js +3 -4
  21. package/dist/contracts/chat-parts.d.ts +11 -1
  22. package/dist/contracts/chat-parts.js +1 -0
  23. package/dist/contracts/index.d.ts +1 -1
  24. package/dist/contracts/part-registry.js +1 -0
  25. package/dist/contracts/report-blocks.d.ts +8 -8
  26. package/dist/contracts/schemas/chat-parts.d.ts +32 -9
  27. package/dist/contracts/schemas/chat-parts.js +10 -0
  28. package/dist/execution/artifact-metadata.d.ts +26 -33
  29. package/dist/execution/artifact-metadata.js +44 -106
  30. package/dist/execution/post-step-pipeline.d.ts +23 -16
  31. package/dist/execution/post-step-pipeline.js +19 -21
  32. package/dist/execution/run-synthesis.d.ts +3 -0
  33. package/dist/execution/run-synthesis.js +11 -1
  34. package/dist/execution/step-summary.d.ts +21 -30
  35. package/dist/execution/step-summary.js +30 -57
  36. package/dist/index.d.ts +9 -8
  37. package/dist/index.js +15 -34
  38. package/dist/lib/zod-issues.js +2 -4
  39. package/dist/loop/compaction.d.ts +20 -0
  40. package/dist/loop/compaction.js +2 -0
  41. package/dist/loop/continue-agent.d.ts +40 -0
  42. package/dist/loop/continue-agent.js +38 -0
  43. package/dist/loop/metrics.d.ts +30 -22
  44. package/dist/loop/metrics.js +51 -22
  45. package/dist/loop/run-agent.d.ts +111 -13
  46. package/dist/loop/run-agent.js +467 -220
  47. package/dist/loop/run-to-terminal.d.ts +16 -17
  48. package/dist/loop/run-to-terminal.js +24 -35
  49. package/dist/loop/tool-mask.d.ts +23 -0
  50. package/dist/loop/tool-mask.js +46 -0
  51. package/dist/loop/tool-output.d.ts +48 -0
  52. package/dist/loop/tool-output.js +79 -0
  53. package/dist/loop/types.d.ts +6 -3
  54. package/dist/memory/ai-sdk-message-storage.d.ts +49 -2
  55. package/dist/memory/ai-sdk-message-storage.js +78 -2
  56. package/dist/memory/conversation-display-recorder.d.ts +6 -0
  57. package/dist/memory/conversation-display-recorder.js +64 -22
  58. package/dist/memory/conversation-display-replay.d.ts +5 -14
  59. package/dist/memory/conversation-display-replay.js +66 -32
  60. package/dist/memory/conversation-display-storage.d.ts +2 -1
  61. package/dist/memory/conversation-display-storage.js +2 -1
  62. package/dist/memory/conversation-view.d.ts +53 -0
  63. package/dist/memory/conversation-view.js +130 -0
  64. package/dist/memory/count-tokens.d.ts +5 -7
  65. package/dist/memory/count-tokens.js +7 -9
  66. package/dist/memory/thread-history.d.ts +74 -102
  67. package/dist/memory/thread-history.js +181 -246
  68. package/dist/memory/thread-store.d.ts +3 -3
  69. package/dist/memory/thread-store.js +7 -2
  70. package/dist/memory/tool-call-integrity.d.ts +15 -21
  71. package/dist/memory/tool-call-integrity.js +31 -38
  72. package/dist/memory/working-memory.d.ts +2 -5
  73. package/dist/memory/working-memory.js +2 -5
  74. package/dist/prompts/SOUL.d.ts +2 -2
  75. package/dist/prompts/SOUL.js +2 -7
  76. package/dist/prompts/analogical-reasoner.d.ts +1 -1
  77. package/dist/prompts/analogical-reasoner.js +16 -32
  78. package/dist/prompts/briefing.js +1 -1
  79. package/dist/prompts/compaction.d.ts +4 -0
  80. package/dist/prompts/compaction.js +24 -0
  81. package/dist/prompts/conversation.d.ts +1 -1
  82. package/dist/prompts/conversation.js +26 -35
  83. package/dist/prompts/execute-analysis/step-summary.js +7 -5
  84. package/dist/prompts/literature-reviewer.d.ts +1 -1
  85. package/dist/prompts/literature-reviewer.js +29 -34
  86. package/dist/prompts/planner.js +15 -29
  87. package/dist/prompts/report-session.d.ts +1 -1
  88. package/dist/prompts/report-session.js +2 -33
  89. package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
  90. package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +1 -2
  91. package/dist/prompts/sandbox/cheminformatics-agent.d.ts +1 -1
  92. package/dist/prompts/sandbox/cheminformatics-agent.js +10 -29
  93. package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
  94. package/dist/prompts/sandbox/chromatin-agent.js +1 -2
  95. package/dist/prompts/sandbox/data-profiler.js +6 -3
  96. package/dist/prompts/sandbox/dna-methylation-agent.d.ts +1 -1
  97. package/dist/prompts/sandbox/dna-methylation-agent.js +2 -7
  98. package/dist/prompts/sandbox/drug-repurposing-agent.d.ts +1 -1
  99. package/dist/prompts/sandbox/drug-repurposing-agent.js +20 -24
  100. package/dist/prompts/sandbox/enrichment-agent.d.ts +1 -1
  101. package/dist/prompts/sandbox/enrichment-agent.js +3 -2
  102. package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
  103. package/dist/prompts/sandbox/genomic-variant-agent.js +0 -2
  104. package/dist/prompts/sandbox/immune-profiling-agent.d.ts +1 -1
  105. package/dist/prompts/sandbox/immune-profiling-agent.js +7 -14
  106. package/dist/prompts/sandbox/metabolomics-agent.d.ts +1 -1
  107. package/dist/prompts/sandbox/metabolomics-agent.js +0 -2
  108. package/dist/prompts/sandbox/microbiome-agent.d.ts +1 -1
  109. package/dist/prompts/sandbox/microbiome-agent.js +14 -21
  110. package/dist/prompts/sandbox/multi-omics-integration-agent.d.ts +1 -1
  111. package/dist/prompts/sandbox/multi-omics-integration-agent.js +9 -19
  112. package/dist/prompts/sandbox/multimodal-sc-agent.d.ts +1 -1
  113. package/dist/prompts/sandbox/multimodal-sc-agent.js +4 -5
  114. package/dist/prompts/sandbox/network-agent.d.ts +1 -1
  115. package/dist/prompts/sandbox/network-agent.js +7 -8
  116. package/dist/prompts/sandbox/pkpd-clinical-response-agent.d.ts +1 -1
  117. package/dist/prompts/sandbox/pkpd-clinical-response-agent.js +19 -19
  118. package/dist/prompts/sandbox/proteomics-agent.d.ts +1 -1
  119. package/dist/prompts/sandbox/proteomics-agent.js +3 -4
  120. package/dist/prompts/sandbox/scientific-executor.d.ts +1 -1
  121. package/dist/prompts/sandbox/scientific-executor.js +7 -10
  122. package/dist/prompts/sandbox/single-cell-agent.d.ts +1 -1
  123. package/dist/prompts/sandbox/single-cell-agent.js +2 -4
  124. package/dist/prompts/sandbox/spatial-omics-agent.d.ts +1 -1
  125. package/dist/prompts/sandbox/spatial-omics-agent.js +3 -5
  126. package/dist/prompts/sandbox/statistical-modeling-agent.d.ts +1 -1
  127. package/dist/prompts/sandbox/statistical-modeling-agent.js +1 -1
  128. package/dist/prompts/sandbox/translational-safety-agent.d.ts +1 -1
  129. package/dist/prompts/sandbox/translational-safety-agent.js +10 -19
  130. package/dist/prompts/sandbox-standards.d.ts +7 -7
  131. package/dist/prompts/sandbox-standards.js +74 -76
  132. package/dist/prompts/synthesis-agent.d.ts +1 -1
  133. package/dist/prompts/synthesis-agent.js +16 -42
  134. package/dist/providers/ai-sdk.d.ts +39 -6
  135. package/dist/providers/ai-sdk.js +117 -18
  136. package/dist/providers/errors.d.ts +2 -0
  137. package/dist/providers/errors.js +5 -0
  138. package/dist/providers/prompt-cache.d.ts +43 -41
  139. package/dist/providers/prompt-cache.js +47 -41
  140. package/dist/providers/reasoning.d.ts +10 -21
  141. package/dist/providers/reasoning.js +10 -21
  142. package/dist/providers/types.d.ts +21 -5
  143. package/dist/report-model/authoring-grammar.d.ts +2 -2
  144. package/dist/runtime/assemble.d.ts +9 -7
  145. package/dist/runtime/assemble.js +7 -3
  146. package/dist/sandbox/create-sandbox.d.ts +2 -2
  147. package/dist/sandbox/submit-exec.d.ts +1 -1
  148. package/dist/schemas/artifact-manifest.d.ts +4 -4
  149. package/dist/state/init.js +39 -4
  150. package/dist/state/purge-analysis.js +4 -0
  151. package/dist/state/tool-outputs.d.ts +4 -0
  152. package/dist/state/tool-outputs.js +30 -0
  153. package/dist/tasks/data-profile.d.ts +3 -0
  154. package/dist/tasks/data-profile.js +2 -0
  155. package/dist/tools/ad-hoc-router.d.ts +8 -0
  156. package/dist/tools/ad-hoc-router.js +21 -2
  157. package/dist/tools/display/show-user.d.ts +1 -1
  158. package/dist/tools/execute-analysis.d.ts +3 -0
  159. package/dist/tools/execute-analysis.js +9 -1
  160. package/dist/tools/read-tool-output.d.ts +7 -0
  161. package/dist/tools/read-tool-output.js +130 -0
  162. package/dist/tools/research/generate-analogy-report.d.ts +10 -26
  163. package/dist/tools/research/generate-analogy-report.js +76 -163
  164. package/dist/tools/research/generate-plan.d.ts +3 -0
  165. package/dist/tools/research/generate-plan.js +4 -1
  166. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  167. package/dist/tools/research/literature-reviewer.js +3 -0
  168. package/dist/tools/sandbox/report-blocker.d.ts +18 -5
  169. package/dist/tools/sandbox/report-blocker.js +54 -6
  170. package/dist/tools/sandbox/submit-file-metadata.d.ts +30 -0
  171. package/dist/tools/sandbox/submit-file-metadata.js +67 -0
  172. package/dist/tools/start-report-session.d.ts +1 -3
  173. package/dist/tools/start-report-session.js +0 -2
  174. package/dist/tools/workspace/execute-command.js +7 -4
  175. package/dist/tools/workspace/result-bounds.d.ts +4 -4
  176. package/dist/tools/workspace/result-bounds.js +6 -5
  177. package/dist/tools/workspace/workspace-search.d.ts +1 -1
  178. package/dist/workflows/execute-analysis.d.ts +3 -0
  179. package/dist/workflows/execute-analysis.js +1 -0
  180. package/dist/workflows/sandbox-step.d.ts +30 -9
  181. package/dist/workflows/sandbox-step.js +37 -8
  182. package/package.json +9 -9
  183. package/dist/providers/anthropic.d.ts +0 -25
  184. 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 `runAgent`. The inner research agent runs Phase 1 extraction +
6
- * Phase 2 cross-domain search; the wrapper parses + schema-validates the text
7
- * output; on parse failure a single-shot conversion call (no tools) transforms
8
- * raw markdown into a valid envelope; on hard failure an `extraction-failed`
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 { type AnalogicalReasonerOutput } from "@inflexa-ai/harness/contracts/analogy-report.js";
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 `runAgent`. The inner research agent runs Phase 1 extraction +
6
- * Phase 2 cross-domain search; the wrapper parses + schema-validates the text
7
- * output; on parse failure a single-shot conversion call (no tools) transforms
8
- * raw markdown into a valid envelope; on hard failure an `extraction-failed`
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 { AnalogicalReasonerOutputSchema } from "@inflexa-ai/harness/contracts/analogy-report.js";
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 { unwrapOrThrow } from "../../lib/result.js";
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 reasonerTools = [
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 agent = {
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 retried internally — you would burn " +
193
- "latency on the same failure). Surface the message and ask the user to " +
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
- // Phase 1+2: drive the research agent over the cross-domain toolset.
200
- let rawText;
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
- const { messages: transcript } = await runAgent(agent, [{ role: "user", content: buildResearchPrompt(input) }], childSession, {
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
- // Fast path: the agent emitted valid JSON matching the schema.
227
- const directParse = tryParseEnvelope(rawText);
228
- if (directParse.ok)
229
- return ok(directParse.value);
230
- // Slow path: a single-shot conversion call (no tools) transforms the
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
- /** Single source of truth for the wrapper's terminal "could not recover" envelope. */
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 returned malformed output that could not " + "be recovered. Try narrowing the problem statement.",
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. Three loops offer it — the
10
- * sandbox step agent, the planner (`tools/research/generate-plan.ts`) and the
11
- * run synthesizer (`execution/run-synthesis.ts`) — and all three share one id,
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 its
16
- * own outcome cell after the loop and decides the terminal status.
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.