@inflexa-ai/harness 0.14.2 → 0.15.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/README.md +2 -1
- package/dist/agents/conversation-agent.d.ts +7 -0
- package/dist/agents/conversation-agent.js +5 -3
- package/dist/agents/sandbox/drug-repurposing-agent.js +5 -1
- package/dist/agents/sandbox/immune-profiling-agent.js +4 -1
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +2 -1
- package/dist/billing/noop-usage-recorder.d.ts +7 -0
- package/dist/billing/noop-usage-recorder.js +10 -0
- package/dist/billing/usage-recorder.d.ts +63 -0
- package/dist/billing/usage-recorder.js +15 -0
- package/dist/contracts/chat-events.d.ts +44 -3
- package/dist/contracts/chat-parts.d.ts +38 -2
- package/dist/contracts/chat-parts.js +1 -1
- package/dist/contracts/data-profile.d.ts +26 -0
- package/dist/contracts/data-profile.js +26 -0
- package/dist/contracts/index.d.ts +3 -1
- package/dist/contracts/index.js +1 -0
- package/dist/contracts/message.d.ts +21 -1
- package/dist/contracts/part-registry.js +1 -0
- package/dist/contracts/schemas/chat-events.d.ts +48 -2
- package/dist/contracts/schemas/chat-events.js +8 -1
- package/dist/contracts/schemas/chat-parts.d.ts +46 -3
- package/dist/contracts/schemas/chat-parts.js +13 -0
- package/dist/contracts/schemas/index.d.ts +2 -1
- package/dist/contracts/schemas/index.js +2 -1
- package/dist/contracts/schemas/usage.d.ts +11 -0
- package/dist/contracts/schemas/usage.js +18 -0
- package/dist/contracts/usage.d.ts +51 -0
- package/dist/contracts/usage.js +41 -0
- package/dist/execution/artifact-metadata.d.ts +3 -0
- package/dist/execution/artifact-metadata.js +1 -0
- package/dist/execution/dbos-workflow-purger.d.ts +30 -0
- package/dist/execution/dbos-workflow-purger.js +193 -0
- package/dist/execution/post-step-pipeline.d.ts +3 -0
- package/dist/execution/post-step-pipeline.js +2 -0
- package/dist/execution/reconcile-manifest.js +28 -15
- package/dist/execution/report-runner.d.ts +16 -0
- package/dist/execution/report-runner.js +4 -0
- package/dist/execution/run-synthesis.d.ts +3 -0
- package/dist/execution/run-synthesis.js +2 -0
- package/dist/execution/step-summary.d.ts +3 -0
- package/dist/execution/step-summary.js +1 -0
- package/dist/execution/workflow-purger.d.ts +38 -0
- package/dist/execution/workflow-purger.js +24 -0
- package/dist/index.d.ts +24 -2
- package/dist/index.js +27 -0
- package/dist/lib/metrics.js +2 -1
- package/dist/loop/metrics.d.ts +18 -0
- package/dist/loop/metrics.js +18 -1
- package/dist/loop/run-agent.d.ts +35 -0
- package/dist/loop/run-agent.js +149 -23
- package/dist/loop/run-to-terminal.d.ts +5 -2
- package/dist/loop/run-to-terminal.js +36 -2
- package/dist/loop/tool-detail.d.ts +54 -0
- package/dist/loop/tool-detail.js +105 -0
- package/dist/loop/tool-outcome.d.ts +21 -0
- package/dist/loop/tool-outcome.js +25 -0
- package/dist/loop/types.d.ts +15 -1
- package/dist/memory/content-to-cortex.d.ts +28 -4
- package/dist/memory/content-to-cortex.js +70 -14
- package/dist/memory/thread-history.d.ts +21 -1
- package/dist/memory/thread-history.js +29 -7
- package/dist/memory/thread-store.d.ts +65 -8
- package/dist/memory/thread-store.js +81 -17
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.d.ts +1 -1
- package/dist/prompts/sandbox/bulk-transcriptomics-agent.js +17 -13
- package/dist/prompts/sandbox/chromatin-agent.d.ts +1 -1
- package/dist/prompts/sandbox/chromatin-agent.js +45 -54
- package/dist/prompts/sandbox/genomic-variant-agent.d.ts +1 -1
- package/dist/prompts/sandbox/genomic-variant-agent.js +39 -37
- package/dist/prompts/sandbox-standards.d.ts +1 -1
- package/dist/prompts/sandbox-standards.js +21 -2
- package/dist/provenance/collector.d.ts +5 -3
- package/dist/provenance/collector.js +5 -3
- package/dist/providers/ai-sdk.js +91 -16
- package/dist/providers/streaming-chat.js +5 -0
- package/dist/providers/types.d.ts +23 -0
- package/dist/providers/types.js +1 -0
- package/dist/reference-data/catalog.js +536 -0
- package/dist/runtime/assemble.d.ts +25 -8
- package/dist/runtime/assemble.js +7 -4
- package/dist/sandbox/sandbox-step-translate.d.ts +22 -1
- package/dist/sandbox/sandbox-step-translate.js +23 -36
- package/dist/schemas/run-synthesis.d.ts +2 -2
- package/dist/state/index.d.ts +2 -0
- package/dist/state/index.js +3 -0
- package/dist/state/init.js +28 -0
- package/dist/state/purge-analysis.d.ts +74 -0
- package/dist/state/purge-analysis.js +205 -0
- package/dist/state/vector-store.d.ts +6 -0
- package/dist/state/vector-store.js +22 -2
- package/dist/tasks/data-profile-activity.d.ts +14 -2
- package/dist/tasks/data-profile-activity.js +1 -1
- package/dist/tasks/data-profile.d.ts +3 -0
- package/dist/tasks/data-profile.js +9 -3
- package/dist/tools/bio/pubmed.js +15 -0
- package/dist/tools/bio/search-gene.js +12 -0
- package/dist/tools/define-tool.d.ts +41 -3
- package/dist/tools/define-tool.js +4 -1
- package/dist/tools/detail-resolver.d.ts +35 -0
- package/dist/tools/detail-resolver.js +39 -0
- package/dist/tools/execute-analysis.js +3 -0
- package/dist/tools/iterate-report.d.ts +3 -0
- package/dist/tools/iterate-report.js +3 -0
- package/dist/tools/memory/update-working-memory.js +8 -0
- package/dist/tools/research/generate-analogy-report.d.ts +3 -0
- package/dist/tools/research/generate-analogy-report.js +6 -0
- package/dist/tools/research/generate-plan.d.ts +3 -0
- package/dist/tools/research/generate-plan.js +6 -0
- package/dist/tools/research/inspect-run.js +4 -0
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +6 -0
- package/dist/tools/workspace/edit-file.js +2 -0
- package/dist/tools/workspace/execute-command.js +25 -0
- package/dist/tools/workspace/grep.js +3 -0
- package/dist/tools/workspace/list-files.js +2 -0
- package/dist/tools/workspace/read-file.js +7 -0
- package/dist/tools/workspace/workspace-search.js +1 -0
- package/dist/tools/workspace/write-file.js +3 -0
- package/dist/workflows/execute-analysis.d.ts +7 -0
- package/dist/workflows/execute-analysis.js +31 -2
- package/dist/workflows/execute-target-assessment.d.ts +3 -0
- package/dist/workflows/sandbox-step.d.ts +23 -0
- package/dist/workflows/sandbox-step.js +33 -2
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.d.ts +3 -0
- package/dist/workflows/target-assessment/lib/clinical-consequence-annotator.js +1 -0
- package/package.json +1 -1
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
* no terminal at all.
|
|
21
21
|
*/
|
|
22
22
|
import type { Logger } from "../lib/logger.js";
|
|
23
|
+
import type { ToolDetailResolver } from "../tools/detail-resolver.js";
|
|
23
24
|
/**
|
|
24
25
|
* Writes one part to the run-event stream. Injected rather than imported so this
|
|
25
26
|
* module stays free of the durability engine and so a test can record what a
|
|
@@ -51,8 +52,19 @@ export interface ProfileActivityEmitter {
|
|
|
51
52
|
sandboxInit(): Promise<void>;
|
|
52
53
|
/** Reported once the sandbox is ready, covering the gap before the agent's first tool call. */
|
|
53
54
|
agentStarting(): Promise<void>;
|
|
54
|
-
/**
|
|
55
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Reported per tool call the profiler agent makes, phrased by the shared
|
|
57
|
+
* translator.
|
|
58
|
+
*
|
|
59
|
+
* `resolveDetail` is built over the profiler agent's own tool list, so the
|
|
60
|
+
* phrase comes from the called tool's `describeCall` hook. It is passed per
|
|
61
|
+
* call rather than held by the emitter because the emitter exists before the
|
|
62
|
+
* agent does — `sandboxInit` is reported before the sandbox the agent's tools
|
|
63
|
+
* are bound to. The call site supplies a RESOLVER, never a phrase: the
|
|
64
|
+
* phrases are this capability's observable contract, and a call site that
|
|
65
|
+
* composed its own is a call site that can drift from it.
|
|
66
|
+
*/
|
|
67
|
+
forTool(name: string, input: unknown, resolveDetail?: ToolDetailResolver): Promise<void>;
|
|
56
68
|
/** Reported for the vector-store pass that indexes per-file descriptions. */
|
|
57
69
|
indexing(): Promise<void>;
|
|
58
70
|
/** The success terminal. Emitted only after the terminal ledger write has landed. */
|
|
@@ -48,7 +48,7 @@ export function createProfileActivityEmitter(write, frame, logger) {
|
|
|
48
48
|
return {
|
|
49
49
|
sandboxInit: () => emit("sandbox-init", "Starting sandbox"),
|
|
50
50
|
agentStarting: () => emit("executing", "Running data-profiler"),
|
|
51
|
-
forTool: (name, input) => emit("executing", activityForTool(name, input)),
|
|
51
|
+
forTool: (name, input, resolveDetail) => emit("executing", activityForTool(name, input, resolveDetail)),
|
|
52
52
|
indexing: () => emit("indexing", "Indexing input descriptions for search"),
|
|
53
53
|
complete: () => emit("complete", "Profile complete"),
|
|
54
54
|
failed: (reason) => emit("failed", reason),
|
|
@@ -23,6 +23,7 @@ import type { RunAuthorization, RunAuthorizer } from "../execution/run-authorize
|
|
|
23
23
|
import type { StagedInput } from "../execution/staged-input.js";
|
|
24
24
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
25
25
|
import type { Logger } from "../lib/logger.js";
|
|
26
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
26
27
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
27
28
|
import type { SandboxClient } from "../sandbox/client.js";
|
|
28
29
|
import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
|
|
@@ -53,6 +54,8 @@ export interface DataProfileDeps extends EnvironmentStorePaths {
|
|
|
53
54
|
readonly embedding: EmbeddingProvider;
|
|
54
55
|
/** Absolute path to the skills tree (one subdirectory per skill). */
|
|
55
56
|
readonly skillsDir: string;
|
|
57
|
+
/** LLM usage-accounting seam for the profiler agent loop; omitted falls back to the no-op recorder. */
|
|
58
|
+
readonly usageRecorder?: UsageRecorder;
|
|
56
59
|
}
|
|
57
60
|
/**
|
|
58
61
|
* Workflow input. JSON-serialisable — DBOS persists it as the workflow's
|
|
@@ -26,6 +26,7 @@ import { durableStep } from "../loop/run-step.js";
|
|
|
26
26
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
27
27
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
28
28
|
import { defineTool } from "../tools/define-tool.js";
|
|
29
|
+
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
29
30
|
import { renderWorkspace } from "../prompts/briefing.js";
|
|
30
31
|
import { toSandboxPath } from "../workspace/paths.js";
|
|
31
32
|
import { estimateDataProfileResources } from "../sandbox/estimate-data-profile-resources.js";
|
|
@@ -36,8 +37,9 @@ import { ProfilerOutputSchema } from "../schemas/data-profile-schemas.js";
|
|
|
36
37
|
import { completeDataProfile, failDataProfile, loadDataProfileStatus, loadSeedInputFileIds, recordDataProfileWorkflowId, tryRerunDataProfile, tryStartDataProfile, upsertArtifacts, } from "../state/index.js";
|
|
37
38
|
import { createProfileActivityEmitter } from "./data-profile-activity.js";
|
|
38
39
|
import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.js";
|
|
39
|
-
|
|
40
|
-
|
|
40
|
+
// The run literal is declared in `contracts/` — a consumer reads it back off recorded usage, and this
|
|
41
|
+
// module is unimportable to one that only wants the string (see the constant's own doc).
|
|
42
|
+
import { DATA_PROFILE_RUN_LITERAL } from "../contracts/data-profile.js";
|
|
41
43
|
const DATA_PROFILE_STEP_LITERAL = "profile";
|
|
42
44
|
const DATA_PROFILE_AGENT_ID = "data-profiler";
|
|
43
45
|
/** Sandbox-server exec budget for the profile run. */
|
|
@@ -277,6 +279,9 @@ export async function runDataProfileBody(input, deps) {
|
|
|
277
279
|
});
|
|
278
280
|
const baseAgent = createDataProfilerAgent(sandboxAgentDeps);
|
|
279
281
|
const agentDef = { ...baseAgent, tools: [...baseAgent.tools, submitProfileTool] };
|
|
282
|
+
// The profiler's own roster, `submit_profile` included, so every call it
|
|
283
|
+
// makes is phrased by the tool that was called.
|
|
284
|
+
const resolveDetail = createDetailResolver(agentDef.tools, logger);
|
|
280
285
|
const signal = new AbortController().signal;
|
|
281
286
|
// Covers the gap between a ready sandbox and the agent's first tool call, which would
|
|
282
287
|
// otherwise still read as `Starting sandbox` long after the container was up. The run
|
|
@@ -294,10 +299,11 @@ export async function runDataProfileBody(input, deps) {
|
|
|
294
299
|
// counter and desynchronise the recorded sequence on replay.
|
|
295
300
|
emit: async (event) => {
|
|
296
301
|
if (event.type === "tool-started")
|
|
297
|
-
await activity.forTool(event.name, event.input);
|
|
302
|
+
await activity.forTool(event.name, event.input, resolveDetail);
|
|
298
303
|
},
|
|
299
304
|
runStep: durableStep,
|
|
300
305
|
resolved: () => capturedProfile !== null,
|
|
306
|
+
usageRecorder: deps.usageRecorder,
|
|
301
307
|
}, {
|
|
302
308
|
tools: [submitProfileTool],
|
|
303
309
|
nudge: "You stopped without calling submit_profile, so no profile was " +
|
package/dist/tools/bio/pubmed.js
CHANGED
|
@@ -125,6 +125,21 @@ export function createPubMedTool(deps) {
|
|
|
125
125
|
"`outline` from the first call and re-request the specific `sections` you need rather than raising maxChars. " +
|
|
126
126
|
"available: false is an expected outcome, not an error — do not retry it.",
|
|
127
127
|
inputSchema,
|
|
128
|
+
// One tool, three jobs. The action alone would still render three
|
|
129
|
+
// different searches identically, so each action names its own subject:
|
|
130
|
+
// the query, how many articles, or which article.
|
|
131
|
+
describeCall: (input) => {
|
|
132
|
+
switch (input.action) {
|
|
133
|
+
case "search":
|
|
134
|
+
return `search ${input.query ?? ""}`;
|
|
135
|
+
case "details": {
|
|
136
|
+
const count = input.pmids?.length ?? 0;
|
|
137
|
+
return `details for ${count} ${count === 1 ? "article" : "articles"}`;
|
|
138
|
+
}
|
|
139
|
+
case "fulltext":
|
|
140
|
+
return `fulltext ${input.pmcId ?? ""}`;
|
|
141
|
+
}
|
|
142
|
+
},
|
|
128
143
|
execute: async (input) => {
|
|
129
144
|
switch (input.action) {
|
|
130
145
|
case "search":
|
|
@@ -5,6 +5,12 @@ import { ok } from "neverthrow";
|
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { defineTool } from "../define-tool.js";
|
|
7
7
|
import { lookupGenes } from "../lib/ensembl-client.js";
|
|
8
|
+
/**
|
|
9
|
+
* How many symbols a detail names before it switches to a count plus a sample.
|
|
10
|
+
* Eight symbols of typical length stay well inside the emit-site cap, so the
|
|
11
|
+
* listed form is never the thing that trips it.
|
|
12
|
+
*/
|
|
13
|
+
const SYMBOLS_LISTED_IN_FULL = 8;
|
|
8
14
|
export const searchGeneTool = defineTool({
|
|
9
15
|
id: "search_gene",
|
|
10
16
|
description: "Resolve gene symbols against Ensembl — the identifier step other tools depend on. Per symbol: Ensembl gene ID (ENSG…), coordinates, strand, " +
|
|
@@ -14,5 +20,11 @@ export const searchGeneTool = defineTool({
|
|
|
14
20
|
symbols: z.array(z.string()).min(1).max(200).describe("Gene symbols to look up (e.g. ['BRCA1', 'TP53'])"),
|
|
15
21
|
species: z.string().default("homo_sapiens").describe("Species name (e.g. 'homo_sapiens', 'mus_musculus')"),
|
|
16
22
|
}),
|
|
23
|
+
// The symbols are the call. A large batch is summarized here rather than
|
|
24
|
+
// left to the emit-site cap: that cap cuts at a character, so a 200-symbol
|
|
25
|
+
// join arrives ending in `TP5` — a fragment that reads as a real symbol and
|
|
26
|
+
// says nothing about how many were dropped. The count leads, so a batch is
|
|
27
|
+
// legible as a batch even when the sample after it is itself trimmed.
|
|
28
|
+
describeCall: ({ symbols }) => symbols.length <= SYMBOLS_LISTED_IN_FULL ? symbols.join(", ") : `${symbols.length} genes: ${symbols.slice(0, SYMBOLS_LISTED_IN_FULL).join(", ")}, …`,
|
|
17
29
|
execute: async ({ symbols, species }) => ok(await lookupGenes(symbols, { species })),
|
|
18
30
|
});
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`, `turnUsage`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import type { Result } from "neverthrow";
|
|
19
19
|
import { z } from "zod";
|
|
20
20
|
import type { AgentSession } from "../auth/types.js";
|
|
21
|
+
import type { AgentRunUsage } from "../loop/metrics.js";
|
|
21
22
|
import type { EmitFn, RunStep } from "../loop/types.js";
|
|
22
23
|
import type { AskApproval, AskRequest } from "./approval/contract.js";
|
|
23
24
|
export type { EmitFn };
|
|
@@ -42,7 +43,8 @@ export declare function isToolError(value: unknown): value is ToolError;
|
|
|
42
43
|
* dependencies (see the harness-durable-runtime spec) — invocation identity,
|
|
43
44
|
* `session`, `signal`, `emit`, the `runStep`
|
|
44
45
|
* durability seam (`passthroughStep` in chat, `DBOS.runStep` in workflows) a
|
|
45
|
-
* tool uses to wrap its own durable work,
|
|
46
|
+
* tool uses to wrap its own durable work, the `ask` user-approval seam, and the
|
|
47
|
+
* turn's usage accumulator a sub-agent-running tool hands to its child loop.
|
|
46
48
|
*/
|
|
47
49
|
export interface ToolContext {
|
|
48
50
|
/** Stable identity of this AI SDK tool call. Redelivery preserves it; a new
|
|
@@ -64,10 +66,18 @@ export interface ToolContext {
|
|
|
64
66
|
* waiting on a surface that cannot answer.
|
|
65
67
|
*/
|
|
66
68
|
readonly ask: (request: AskRequest) => Promise<AskApproval>;
|
|
69
|
+
/**
|
|
70
|
+
* The turn's usage accumulator — the mutable total every loop under this
|
|
71
|
+
* turn folds its LLM calls into. A tool that drives a child `runAgent`
|
|
72
|
+
* passes it straight into that run's options, which is what puts the
|
|
73
|
+
* child's tokens in the turn total its root loop reports. A tool that runs
|
|
74
|
+
* no child agent ignores it.
|
|
75
|
+
*/
|
|
76
|
+
readonly turnUsage?: AgentRunUsage;
|
|
67
77
|
}
|
|
68
78
|
/**
|
|
69
79
|
* A packaged tool: identity, the Zod input contract, the emitted AI SDK input
|
|
70
|
-
* schema, execution mode, and the executor.
|
|
80
|
+
* schema, execution mode, the optional call-description hook, and the executor.
|
|
71
81
|
*/
|
|
72
82
|
export interface Tool<Input = unknown, Output = unknown> {
|
|
73
83
|
readonly id: string;
|
|
@@ -75,6 +85,8 @@ export interface Tool<Input = unknown, Output = unknown> {
|
|
|
75
85
|
readonly inputSchema: z.ZodType;
|
|
76
86
|
readonly jsonSchema: Record<string, unknown>;
|
|
77
87
|
readonly executionMode: ToolExecutionMode;
|
|
88
|
+
/** See {@link ToolDefinition.describeCall}. Absent when the tool declares none. */
|
|
89
|
+
describeCall?(input: Input): string;
|
|
78
90
|
execute(input: Input, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
79
91
|
}
|
|
80
92
|
export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
@@ -82,6 +94,32 @@ export interface ToolDefinition<Schema extends z.ZodType, Output> {
|
|
|
82
94
|
readonly description: string;
|
|
83
95
|
readonly inputSchema: Schema;
|
|
84
96
|
readonly executionMode?: ToolExecutionMode;
|
|
97
|
+
/**
|
|
98
|
+
* One line naming what THIS call is doing — the call-time counterpart of
|
|
99
|
+
* `description`. `description` self-describes the tool at attach time, so an
|
|
100
|
+
* agent knows what it holds; `describeCall` self-describes one invocation, so
|
|
101
|
+
* a surface can render four `update_working_memory` calls as four distinct
|
|
102
|
+
* lines instead of four identical chips.
|
|
103
|
+
*
|
|
104
|
+
* It lives here, beside `inputSchema`, because that is the one place the
|
|
105
|
+
* compiler checks the two against each other. A host-side formatter reading
|
|
106
|
+
* `input.path` by string key breaks silently the day the schema moves; this
|
|
107
|
+
* one fails to build. The tool author writes it because the tool author knows
|
|
108
|
+
* which field matters.
|
|
109
|
+
*
|
|
110
|
+
* Synchronous and pure — no I/O, no ambient state. Dispatch is what the user
|
|
111
|
+
* waits on, and a description must never be able to fail a call: the loop
|
|
112
|
+
* validates the input against `inputSchema` first, guards the call, and drops
|
|
113
|
+
* the detail on any failure (see the tool-call-detail capability).
|
|
114
|
+
*
|
|
115
|
+
* Return whatever reads best. Normalization — one line, control characters
|
|
116
|
+
* removed, secrets redacted, length capped — happens once at the emit site,
|
|
117
|
+
* never here.
|
|
118
|
+
*
|
|
119
|
+
* OPTIONAL, and absence is a normal state: a tool without it is observable
|
|
120
|
+
* exactly as it is today. The hook never reaches the model.
|
|
121
|
+
*/
|
|
122
|
+
describeCall?(input: z.infer<Schema>): string;
|
|
85
123
|
execute(input: z.infer<Schema>, ctx: ToolContext): Promise<Result<Output, ToolError>>;
|
|
86
124
|
}
|
|
87
125
|
/**
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* captures its deps and calls `defineTool`.
|
|
9
9
|
*
|
|
10
10
|
* `ToolContext` carries only request-scoped values every tool may need
|
|
11
|
-
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`) — no pool, no sandbox, no
|
|
11
|
+
* (`invocationId`, `session`, `signal`, `emit`, `runStep`, `ask`, `turnUsage`) — no pool, no sandbox, no
|
|
12
12
|
* logger. The error contract: an expected
|
|
13
13
|
* outcome ("not found", "no results") stays in the ok channel as a data
|
|
14
14
|
* variant (`ok({ found: false })`); an unexpected failure is an `err(ToolError)`
|
|
@@ -44,6 +44,9 @@ export function defineTool(def) {
|
|
|
44
44
|
inputSchema: def.inputSchema,
|
|
45
45
|
jsonSchema,
|
|
46
46
|
executionMode,
|
|
47
|
+
// Spread rather than assigned: a tool that declares no hook carries no
|
|
48
|
+
// `describeCall` key at all, so "has a hook" is one property check.
|
|
49
|
+
...(def.describeCall === undefined ? {} : { describeCall: def.describeCall }),
|
|
47
50
|
execute: def.execute,
|
|
48
51
|
};
|
|
49
52
|
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The name-plus-input detail resolver.
|
|
3
|
+
*
|
|
4
|
+
* The live loop holds the resolved `Tool` at dispatch and needs nothing. Every
|
|
5
|
+
* other surface — the reload converter, a workflow's live-activity line — has a
|
|
6
|
+
* tool NAME and a persisted input, so it needs a way back to the tool. This is
|
|
7
|
+
* that way, and it is deliberately built over a tool list the CALLER supplies:
|
|
8
|
+
*
|
|
9
|
+
* - A name map internal to the harness could never see an embedder's tools. A
|
|
10
|
+
* host contributes tools through the host-tools seam, and those calls are as
|
|
11
|
+
* ambiguous in a transcript as any other.
|
|
12
|
+
* - Two harness tools legitimately share one id (`write_file` exists in both the
|
|
13
|
+
* workspace surface and the report version store). A resolver built from one
|
|
14
|
+
* agent's list cannot hold both, because a tool list rejects a duplicate id at
|
|
15
|
+
* registry construction.
|
|
16
|
+
*
|
|
17
|
+
* It resolves through the same `computeDetail` the loop uses, so a reloaded
|
|
18
|
+
* thread cannot show a different detail from the one the live turn showed.
|
|
19
|
+
* Nothing is persisted: storage stays the pure model transcript.
|
|
20
|
+
*/
|
|
21
|
+
import { type ToolCallDetail } from "../loop/tool-detail.js";
|
|
22
|
+
import type { Logger } from "../lib/logger.js";
|
|
23
|
+
import type { Tool } from "./define-tool.js";
|
|
24
|
+
/**
|
|
25
|
+
* Maps a tool name plus a call's input to that call's detail. Yields
|
|
26
|
+
* `undefined` for an unknown tool, a tool with no hook, and an input the tool's
|
|
27
|
+
* schema rejects.
|
|
28
|
+
*/
|
|
29
|
+
export type ToolDetailResolver = (toolName: string, input: unknown) => ToolCallDetail | undefined;
|
|
30
|
+
/**
|
|
31
|
+
* Build a resolver over `tools`. A duplicate id resolves to the first tool
|
|
32
|
+
* declaring it; callers pass one agent's composed list, which a registry has
|
|
33
|
+
* already rejected duplicates from.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createDetailResolver(tools: readonly Tool[], logger?: Logger): ToolDetailResolver;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The name-plus-input detail resolver.
|
|
3
|
+
*
|
|
4
|
+
* The live loop holds the resolved `Tool` at dispatch and needs nothing. Every
|
|
5
|
+
* other surface — the reload converter, a workflow's live-activity line — has a
|
|
6
|
+
* tool NAME and a persisted input, so it needs a way back to the tool. This is
|
|
7
|
+
* that way, and it is deliberately built over a tool list the CALLER supplies:
|
|
8
|
+
*
|
|
9
|
+
* - A name map internal to the harness could never see an embedder's tools. A
|
|
10
|
+
* host contributes tools through the host-tools seam, and those calls are as
|
|
11
|
+
* ambiguous in a transcript as any other.
|
|
12
|
+
* - Two harness tools legitimately share one id (`write_file` exists in both the
|
|
13
|
+
* workspace surface and the report version store). A resolver built from one
|
|
14
|
+
* agent's list cannot hold both, because a tool list rejects a duplicate id at
|
|
15
|
+
* registry construction.
|
|
16
|
+
*
|
|
17
|
+
* It resolves through the same `computeDetail` the loop uses, so a reloaded
|
|
18
|
+
* thread cannot show a different detail from the one the live turn showed.
|
|
19
|
+
* Nothing is persisted: storage stays the pure model transcript.
|
|
20
|
+
*/
|
|
21
|
+
import { computeDetail } from "../loop/tool-detail.js";
|
|
22
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
23
|
+
/**
|
|
24
|
+
* Build a resolver over `tools`. A duplicate id resolves to the first tool
|
|
25
|
+
* declaring it; callers pass one agent's composed list, which a registry has
|
|
26
|
+
* already rejected duplicates from.
|
|
27
|
+
*/
|
|
28
|
+
export function createDetailResolver(tools, logger) {
|
|
29
|
+
const log = (logger ?? createNoopLogger()).named("tool-detail");
|
|
30
|
+
const byId = new Map();
|
|
31
|
+
for (const tool of tools) {
|
|
32
|
+
if (!byId.has(tool.id))
|
|
33
|
+
byId.set(tool.id, tool);
|
|
34
|
+
}
|
|
35
|
+
return (toolName, input) => {
|
|
36
|
+
const tool = byId.get(toolName);
|
|
37
|
+
return tool === undefined ? undefined : computeDetail(tool, input, log);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -152,6 +152,9 @@ export function createExecuteAnalysisTool(deps) {
|
|
|
152
152
|
"that explicit request is consent and needs no synthetic-plan approval. If computation is merely your suggestion, ask first. " +
|
|
153
153
|
"You never choose the sandbox specialist: ad hoc routing does that automatically. Returns runId with status=in_progress; inspect results on a later turn.",
|
|
154
154
|
inputSchema,
|
|
155
|
+
// The mode is what a user most needs to see here — an approved plan
|
|
156
|
+
// running is a different event from an ad hoc request being routed.
|
|
157
|
+
describeCall: (input) => (input.mode === "plan" ? `plan ${input.planId ?? ""}` : `ad hoc: ${input.request ?? ""}`),
|
|
155
158
|
execute: async (input, ctx) => {
|
|
156
159
|
if (ctx.session.scope.kind !== "analysis")
|
|
157
160
|
throw new Error("execute_analysis requires an analysis-scoped session");
|
|
@@ -48,6 +48,7 @@ import { type StagedAsset } from "./lib/report-preflight.js";
|
|
|
48
48
|
import type { PreviewPublisher } from "./report/preview-publisher.js";
|
|
49
49
|
import type { ChromeConfig } from "../lib/chrome.js";
|
|
50
50
|
import type { Logger } from "../lib/logger.js";
|
|
51
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
51
52
|
/**
|
|
52
53
|
* The report brief the conversation agent composes: title/audience + sources +
|
|
53
54
|
* the section union. This is the ~12k-char schema that `plan_report` hands back
|
|
@@ -209,6 +210,8 @@ export interface SubmitReportDeps {
|
|
|
209
210
|
previewId: string;
|
|
210
211
|
ttlSeconds: number;
|
|
211
212
|
}) => Promise<PreviewPublisher>;
|
|
213
|
+
/** LLM usage-accounting seam for the report-builder loop; omitted falls back to the no-op recorder. */
|
|
214
|
+
readonly usageRecorder?: UsageRecorder;
|
|
212
215
|
}
|
|
213
216
|
/**
|
|
214
217
|
* `submit_report` — takes the composed brief and drives the report iteration.
|
|
@@ -654,6 +654,7 @@ export function createReportSubmitTool(deps) {
|
|
|
654
654
|
templatesDir: deps.templatesDir,
|
|
655
655
|
skillsDir: deps.skillsDir,
|
|
656
656
|
chrome: deps.chrome,
|
|
657
|
+
usageRecorder: deps.usageRecorder,
|
|
657
658
|
}, {
|
|
658
659
|
resourceId,
|
|
659
660
|
workspaceRoot: analysisRoot,
|
|
@@ -665,6 +666,8 @@ export function createReportSubmitTool(deps) {
|
|
|
665
666
|
session: ctx.session,
|
|
666
667
|
signal: ctx.signal,
|
|
667
668
|
emit: ctx.emit,
|
|
669
|
+
turnUsage: ctx.turnUsage,
|
|
670
|
+
invocationId: ctx.invocationId,
|
|
668
671
|
});
|
|
669
672
|
if (!result.ok) {
|
|
670
673
|
await ctx.emit({
|
|
@@ -149,6 +149,14 @@ export function createUpdateWorkingMemoryTool(workingMemory, pool) {
|
|
|
149
149
|
"user's last message, run contents (tables, gene lists, file paths), or anything you can retrieve " +
|
|
150
150
|
"again with inspect_run, read_file, or search.",
|
|
151
151
|
inputSchema,
|
|
152
|
+
// Four writes in one turn are four identical chips without this. The
|
|
153
|
+
// entry id rides along on revise/retire because it is the only thing
|
|
154
|
+
// separating two retirements from the same section.
|
|
155
|
+
describeCall: (input) => {
|
|
156
|
+
const operation = input.section === "goal" ? "set" : (input.operation ?? "add");
|
|
157
|
+
const target = input.id === undefined ? "" : ` ${input.id}`;
|
|
158
|
+
return `${operation} ${input.section}${target}`;
|
|
159
|
+
},
|
|
152
160
|
execute: async (input, ctx) => {
|
|
153
161
|
const analysisId = scopeResource(ctx.session.scope).resourceId;
|
|
154
162
|
switch (input.section) {
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { z } from "zod";
|
|
16
16
|
import { type AnalogicalReasonerOutput } from "@inflexa-ai/harness/contracts/analogy-report.js";
|
|
17
17
|
import type { ChatProvider } from "../../providers/types.js";
|
|
18
|
+
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
18
19
|
import { type Tool } from "../define-tool.js";
|
|
19
20
|
import { type BioToolKeys } from "../bio/keys.js";
|
|
20
21
|
export declare const generateAnalogyReportInputSchema: z.ZodObject<{
|
|
@@ -39,6 +40,8 @@ export interface GenerateAnalogyReportDeps {
|
|
|
39
40
|
readonly model: string;
|
|
40
41
|
/** API keys for the bio/chem + GitHub data sources the reasoner searches. */
|
|
41
42
|
readonly bioKeys: BioToolKeys;
|
|
43
|
+
/** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
|
|
44
|
+
readonly usageRecorder?: UsageRecorder;
|
|
42
45
|
}
|
|
43
46
|
/** Build the `generate_analogy_report` delegation tool bound to its provider. */
|
|
44
47
|
export declare function createGenerateAnalogyReportTool(deps: GenerateAnalogyReportDeps): Tool;
|
|
@@ -203,6 +203,12 @@ export function createGenerateAnalogyReportTool(deps) {
|
|
|
203
203
|
signal: ctx.signal,
|
|
204
204
|
emit: ctx.emit,
|
|
205
205
|
runStep: passthroughStep,
|
|
206
|
+
usageRecorder: deps.usageRecorder,
|
|
207
|
+
// Fold the child's calls into the turn total the root loop reports.
|
|
208
|
+
turnUsage: ctx.turnUsage,
|
|
209
|
+
// Keeps the usage record keys of two parallel dispatches disjoint —
|
|
210
|
+
// same frame, same call path, same loop-local step names.
|
|
211
|
+
invocationId: ctx.invocationId,
|
|
206
212
|
});
|
|
207
213
|
rawText = finalText(transcript);
|
|
208
214
|
}
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
*/
|
|
26
26
|
import type { Pool } from "pg";
|
|
27
27
|
import { type ChatProvider } from "../../providers/types.js";
|
|
28
|
+
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
28
29
|
import { type Tool } from "../define-tool.js";
|
|
29
30
|
import type { EnvironmentStorePaths } from "../../config/environment-stores.js";
|
|
30
31
|
import { type ResourcePolicy } from "../../config/resource-limits.js";
|
|
@@ -49,6 +50,8 @@ export interface GeneratePlanDeps extends EnvironmentStorePaths {
|
|
|
49
50
|
readonly resourcePolicy?: ResourcePolicy;
|
|
50
51
|
/** Diagnostic sink. Absent, the tool runs silently — see `RunAgentOptions.logger`. */
|
|
51
52
|
readonly logger?: Logger;
|
|
53
|
+
/** LLM usage-accounting seam for the planner loop; omitted falls back to the no-op recorder. */
|
|
54
|
+
readonly usageRecorder?: UsageRecorder;
|
|
52
55
|
}
|
|
53
56
|
/** Build the `generate_plan` tool bound to its provider and pool. */
|
|
54
57
|
export declare function createGeneratePlanTool(deps: GeneratePlanDeps): Tool;
|
|
@@ -830,6 +830,12 @@ export function createGeneratePlanTool(deps) {
|
|
|
830
830
|
// as soon as one is recorded.
|
|
831
831
|
toolChoice: "required",
|
|
832
832
|
logger,
|
|
833
|
+
usageRecorder: deps.usageRecorder,
|
|
834
|
+
// Fold the planner's calls into the turn total the root loop reports.
|
|
835
|
+
turnUsage: ctx.turnUsage,
|
|
836
|
+
// Keeps the usage record keys of two parallel dispatches disjoint —
|
|
837
|
+
// same frame, same call path, same loop-local step names.
|
|
838
|
+
invocationId: ctx.invocationId,
|
|
833
839
|
}, {
|
|
834
840
|
tools: innerTools.terminal,
|
|
835
841
|
nudge: "You ended without a terminal outcome. Call submit_plan with " +
|
|
@@ -102,6 +102,10 @@ export function createInspectRunTool(pool, clock = systemClock) {
|
|
|
102
102
|
"For a user-directed check, waitForTerminalSeconds can wait once for 1-30 seconds; if the cutoff returns in_progress, stop polling for that turn. " +
|
|
103
103
|
"Default response is lightweight — pass includeDiagnostics:true to add error/duration/retry telemetry.",
|
|
104
104
|
inputSchema,
|
|
105
|
+
// The two calls do different work: one inspects a named run, the other
|
|
106
|
+
// pages the list. Naming the run id is what separates repeated polls of
|
|
107
|
+
// the same run from polls of different ones.
|
|
108
|
+
describeCall: (input) => (input.runId === undefined ? `run list (page ${input.page ?? 1})` : input.runId),
|
|
105
109
|
execute: async (input, ctx) => {
|
|
106
110
|
const resourceId = scopeResource(ctx.session.scope).resourceId;
|
|
107
111
|
const verbose = input.includeDiagnostics === true;
|
|
@@ -18,6 +18,7 @@ import { type ChatProvider } from "../../providers/types.js";
|
|
|
18
18
|
import { type Tool } from "../define-tool.js";
|
|
19
19
|
import { type BioToolKeys } from "../bio/keys.js";
|
|
20
20
|
import type { Logger } from "../../lib/logger.js";
|
|
21
|
+
import type { UsageRecorder } from "../../billing/usage-recorder.js";
|
|
21
22
|
export interface LiteratureReviewerDeps {
|
|
22
23
|
/** Operational logging seam; omitted falls back to no-op. */
|
|
23
24
|
readonly logger?: Logger;
|
|
@@ -27,6 +28,8 @@ export interface LiteratureReviewerDeps {
|
|
|
27
28
|
readonly model: string;
|
|
28
29
|
/** API keys for the bio/chem data sources the reviewer searches. */
|
|
29
30
|
readonly bioKeys: BioToolKeys;
|
|
31
|
+
/** LLM usage-accounting seam for the child loop; omitted falls back to the no-op recorder. */
|
|
32
|
+
readonly usageRecorder?: UsageRecorder;
|
|
30
33
|
}
|
|
31
34
|
/** Build the `literature_reviewer` delegation tool bound to its provider. */
|
|
32
35
|
export declare function createLiteratureReviewerTool(deps: LiteratureReviewerDeps): Tool;
|
|
@@ -72,6 +72,12 @@ export function createLiteratureReviewerTool(deps) {
|
|
|
72
72
|
signal: ctx.signal,
|
|
73
73
|
emit: ctx.emit,
|
|
74
74
|
runStep: passthroughStep,
|
|
75
|
+
usageRecorder: deps.usageRecorder,
|
|
76
|
+
// Fold the child's calls into the turn total the root loop reports.
|
|
77
|
+
turnUsage: ctx.turnUsage,
|
|
78
|
+
// Keeps the usage record keys of two parallel reviews disjoint —
|
|
79
|
+
// same frame, same call path, same loop-local step names.
|
|
80
|
+
invocationId: ctx.invocationId,
|
|
75
81
|
});
|
|
76
82
|
return ok({ report: finalText(transcript) });
|
|
77
83
|
},
|
|
@@ -65,6 +65,8 @@ export function createEditFileTool(deps) {
|
|
|
65
65
|
"/ `out_of_scope` / `file_not_found` / `not_found` / `not_unique` data " +
|
|
66
66
|
"variants — never throws on expected outcomes.",
|
|
67
67
|
inputSchema: EditFileInputSchema,
|
|
68
|
+
// The path only. The replaced text is file content, not a description.
|
|
69
|
+
describeCall: ({ path }) => path,
|
|
68
70
|
execute: async ({ path, old_string, new_string, replace_all }, ctx) => {
|
|
69
71
|
const read = unwrapOrThrow(await deps.workspaceFilesystem.readFile({
|
|
70
72
|
session: ctx.session,
|
|
@@ -42,6 +42,12 @@ const ExecuteCommandInputSchema = z.object({
|
|
|
42
42
|
env: z.record(z.string(), z.string()).optional().describe("Extra environment variables for this command."),
|
|
43
43
|
timeoutSeconds: z.number().int().positive().optional().describe("Per-command timeout in seconds; capped by the step deadline."),
|
|
44
44
|
});
|
|
45
|
+
/** Extensions that mark an argv token as the thing being run, not a flag or a data path. */
|
|
46
|
+
const SCRIPT_EXTENSIONS = /\.(py|R|r|sh|js|ts|ipynb)$/;
|
|
47
|
+
/** The first script-like token in an argv, or `undefined` when none looks like one. */
|
|
48
|
+
function scriptToken(command) {
|
|
49
|
+
return command.find((arg) => SCRIPT_EXTENSIONS.test(arg));
|
|
50
|
+
}
|
|
45
51
|
export function createExecuteCommandTool(deps) {
|
|
46
52
|
const { sandboxClient, sandbox, workflowId, stepId, nextFunctionId, deadlineMs, defaultCwd, markExecActive, lineageCollector, mountRoot } = deps;
|
|
47
53
|
return defineTool({
|
|
@@ -58,6 +64,25 @@ export function createExecuteCommandTool(deps) {
|
|
|
58
64
|
"analysis results via 'python -c' / inline one-liners and read them from " +
|
|
59
65
|
"stdout; that work is lost. Write a script and persist its outputs to output/.",
|
|
60
66
|
inputSchema: ExecuteCommandInputSchema,
|
|
67
|
+
// The script is what a reader recognises, and it survives a long
|
|
68
|
+
// invocation whose flags would otherwise push it past the length cap.
|
|
69
|
+
// When nothing in the argv looks like a script, the joined argv is the
|
|
70
|
+
// best account of the call there is.
|
|
71
|
+
//
|
|
72
|
+
// `env` never rides along. It is the channel this tool documents for
|
|
73
|
+
// secrets, so it is the one field guaranteed to hold them. An argv can
|
|
74
|
+
// hold one too (`--password …` is a real habit), and the emit-site
|
|
75
|
+
// redaction only matches the structured, prefixed formats — so the join
|
|
76
|
+
// is a deliberate trade, not a claim of safety.
|
|
77
|
+
//
|
|
78
|
+
// Know what the trade costs. This tool is sandbox-gated, so the only
|
|
79
|
+
// surface that reads its detail is a workflow's live-activity line — and
|
|
80
|
+
// that line goes to the run-event stream through `DBOS.writeStream`. The
|
|
81
|
+
// detail is therefore DURABLE, and every history read replays it. It
|
|
82
|
+
// stays inside the session's own analysis and never reaches the model,
|
|
83
|
+
// but display-only does not mean transient. A secret typed into an argv
|
|
84
|
+
// outlives the turn that ran it.
|
|
85
|
+
describeCall: ({ command }) => scriptToken(command) ?? command.join(" "),
|
|
61
86
|
execute: async ({ command, cwd, env, timeoutSeconds }, ctx) => {
|
|
62
87
|
const execId = `${workflowId}:${stepId}:${nextFunctionId()}`;
|
|
63
88
|
const effectiveCwd = cwd === undefined ? defaultCwd : cwd.startsWith("/") ? cwd : posixPath.join(defaultCwd, cwd);
|
|
@@ -38,6 +38,9 @@ export function createGrepTool(fs, workingDir) {
|
|
|
38
38
|
"bytes per line — narrow your path or pattern if you hit the cap. " +
|
|
39
39
|
"No matches and out-of-scope paths return data variants, not errors.",
|
|
40
40
|
inputSchema: GrepInputSchema,
|
|
41
|
+
// Both fields, because either alone loses the call: the same pattern over
|
|
42
|
+
// two trees and two patterns over one tree are both ordinary sequences.
|
|
43
|
+
describeCall: ({ pattern, path }) => `${pattern} in ${path}`,
|
|
41
44
|
execute: async ({ pattern, path, ignoreCase }, ctx) => {
|
|
42
45
|
let regex;
|
|
43
46
|
try {
|
|
@@ -24,6 +24,8 @@ export function createListFilesTool(fs, workingDir) {
|
|
|
24
24
|
"else: it never reaches a directory outside the analysis, including the host process's current " +
|
|
25
25
|
"working directory. Missing and out-of-scope paths return a data variant, not an error.",
|
|
26
26
|
inputSchema: ListFilesInputSchema,
|
|
27
|
+
// The directory IS the call — two listings differ by nothing else.
|
|
28
|
+
describeCall: ({ path }) => path,
|
|
27
29
|
execute: async ({ path }, ctx) => {
|
|
28
30
|
const result = unwrapOrThrow(await fs.list({ session: ctx.session, path, workingDir }));
|
|
29
31
|
switch (result.kind) {
|
|
@@ -62,6 +62,13 @@ export function createReadFileTool(fs, workingDir) {
|
|
|
62
62
|
"back as a `binary` data variant rather than decoded garbage. Missing " +
|
|
63
63
|
"paths and paths outside the analysis tree return a data variant — not an error.",
|
|
64
64
|
inputSchema: ReadFileInputSchema,
|
|
65
|
+
describeCall: ({ path, headLines, tailLines }) => {
|
|
66
|
+
if (headLines !== undefined)
|
|
67
|
+
return `${path} (first ${headLines} lines)`;
|
|
68
|
+
if (tailLines !== undefined)
|
|
69
|
+
return `${path} (last ${tailLines} lines)`;
|
|
70
|
+
return path;
|
|
71
|
+
},
|
|
65
72
|
execute: async ({ path, headLines, tailLines }, ctx) => {
|
|
66
73
|
if (headLines !== undefined && tailLines !== undefined) {
|
|
67
74
|
return ok({
|
|
@@ -47,6 +47,7 @@ export function createWorkspaceSearchTool(pool, embedding) {
|
|
|
47
47
|
.describe('Restrict results to one entry type: "input", "output", "summary", or "synthesis"'),
|
|
48
48
|
limit: z.number().int().min(1).max(50).default(8).describe("Maximum number of results to return"),
|
|
49
49
|
}),
|
|
50
|
+
describeCall: ({ query, type }) => (type === undefined ? query : `${query} (${type})`),
|
|
50
51
|
execute: async ({ query, type, limit }, ctx) => {
|
|
51
52
|
const [vector] = unwrapOrThrow(await embedding.embed([query], ctx.session));
|
|
52
53
|
if (!vector)
|
|
@@ -28,6 +28,9 @@ export function createWriteFileTool(deps) {
|
|
|
28
28
|
"variant (no I/O), and one escaping the analysis tree returns " +
|
|
29
29
|
"`out_of_scope`.",
|
|
30
30
|
inputSchema: WriteFileInputSchema,
|
|
31
|
+
// The path only. `content` is a whole file and must never ride a display
|
|
32
|
+
// channel, which the emit-site length cap enforces regardless.
|
|
33
|
+
describeCall: ({ path }) => path,
|
|
31
34
|
execute: async ({ path, content }, ctx) => ok(await deps.mutator.writeFile({ path, content, toolName: "write_file", emit: ctx.emit })),
|
|
32
35
|
});
|
|
33
36
|
}
|
|
@@ -63,6 +63,7 @@ import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
|
|
|
63
63
|
import type { RunSession } from "../auth/types.js";
|
|
64
64
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
65
65
|
import type { Logger } from "../lib/logger.js";
|
|
66
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
66
67
|
import type { SynthesisStatus, UpdateStepExecutionInput } from "../state/index.js";
|
|
67
68
|
import type { AnalysisStep } from "../schemas/workflow-state.js";
|
|
68
69
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
@@ -280,6 +281,12 @@ export interface ExecuteAnalysisDeps {
|
|
|
280
281
|
readonly runCharge: RunCharge;
|
|
281
282
|
/** Run-authorization seam — the terminal path revokes through `revoke`. */
|
|
282
283
|
readonly runAuthorizer: RunAuthorizer;
|
|
284
|
+
/**
|
|
285
|
+
* LLM usage-accounting seam for the in-body synthesizer loop (and the
|
|
286
|
+
* literature reviewer it delegates to); omitted falls back to the no-op
|
|
287
|
+
* recorder.
|
|
288
|
+
*/
|
|
289
|
+
readonly usageRecorder?: UsageRecorder;
|
|
283
290
|
/**
|
|
284
291
|
* Optional, fire-and-forget provenance observation of the run lifecycle.
|
|
285
292
|
* Deliberately invoked directly in the workflow body (NOT wrapped in
|