@inflexa-ai/harness 0.14.2 → 0.15.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 -1
- package/dist/agents/conversation-agent.d.ts +7 -0
- package/dist/agents/conversation-agent.js +5 -3
- 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/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/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
|
@@ -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
|
|
@@ -66,6 +66,7 @@ import { createNoopLogger } from "../lib/console-logger.js";
|
|
|
66
66
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
67
67
|
import { RunDedupCollisionError, countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
|
|
68
68
|
import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
69
|
+
import { addChatUsage, hasReportedUsage } from "../loop/metrics.js";
|
|
69
70
|
import { SYNTHESIS_AGENT_ID, loadStepSummariesFromDisk } from "../execution/run-synthesis.js";
|
|
70
71
|
import { MAX_UPSTREAM_ARTIFACTS, composeStepBriefing } from "../prompts/briefing.js";
|
|
71
72
|
import { SYNTHESIS_STEP_ID, runDir, runStepDir, stepSubdir, stepWritePrefix, toSandboxPath } from "../workspace/paths.js";
|
|
@@ -319,11 +320,22 @@ export function synthesizeFindings(args) {
|
|
|
319
320
|
resolveWorkspaceRoot: deps.resolveWorkspaceRoot,
|
|
320
321
|
synthesisModel: deps.synthesisModel,
|
|
321
322
|
bioKeys: deps.bioKeys,
|
|
323
|
+
usageRecorder: deps.usageRecorder,
|
|
322
324
|
}, {
|
|
323
325
|
analysisId,
|
|
324
326
|
runId,
|
|
325
327
|
completedSteps,
|
|
326
|
-
|
|
328
|
+
// Stepped, exactly as a sandbox step's child input is. Synthesis owns a real row in the
|
|
329
|
+
// step-execution ledger (seeded and updated under `SYNTHESIS_STEP_ID` in the body below),
|
|
330
|
+
// so it lists as a step everywhere a run's steps are shown. Handing it the bare run session
|
|
331
|
+
// left every call it makes — the synthesizer loop and each sub-agent it dispatches —
|
|
332
|
+
// recorded against the run carrying no `stepId`, which made the one step in the list unable
|
|
333
|
+
// to report a figure the one that had just spent the most.
|
|
334
|
+
//
|
|
335
|
+
// It also puts the step id into the replay-stable `recordKey`, which is what it is for:
|
|
336
|
+
// synthesis and a plan step of the same run can no longer mint the same key from the same
|
|
337
|
+
// call path.
|
|
338
|
+
session: forStep(session, SYNTHESIS_STEP_ID),
|
|
327
339
|
emit,
|
|
328
340
|
onProgress: (phase, activity, extra = {}) => DBOS.writeStream("events", {
|
|
329
341
|
type: "data-synthesis-progress",
|
|
@@ -530,6 +542,7 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
530
542
|
failed: final.failed,
|
|
531
543
|
canceled: final.canceled,
|
|
532
544
|
budgetExceeded: final.budgetExceeded,
|
|
545
|
+
...(final.usage ? { usage: final.usage } : {}),
|
|
533
546
|
failureReason: synthesisError
|
|
534
547
|
? synthesisError instanceof Error
|
|
535
548
|
? `synthesis-failed: ${synthesisError.message}`
|
|
@@ -601,6 +614,12 @@ async function runSchedulerLoop(args) {
|
|
|
601
614
|
const canceled = new Set();
|
|
602
615
|
const inFlight = new Map();
|
|
603
616
|
const budgetExceededChildIds = new Set();
|
|
617
|
+
// Run-level usage aggregate, folded from the children's own durable results.
|
|
618
|
+
// `getResult` IS the replay-safe source: a recovered parent reads back the
|
|
619
|
+
// identical value from each child's workflow record, so the sum reproduces
|
|
620
|
+
// exactly without a second channel. A child that settled by throwing carries
|
|
621
|
+
// no result and therefore contributes nothing — same rule as its duration.
|
|
622
|
+
const runUsage = {};
|
|
604
623
|
// Child workflow ids the parent itself cancelled (budget / neverFits halt cascade). Their `getResult`
|
|
605
624
|
// rejects with a DBOS cancellation error and lands in the settlement error branch — but the step was
|
|
606
625
|
// CANCELED by us, not failed on its own merits. Keyed here (deterministic: `cancelInFlight` runs the
|
|
@@ -798,6 +817,7 @@ async function runSchedulerLoop(args) {
|
|
|
798
817
|
let stepDurationMs;
|
|
799
818
|
if (settled.kind === "result") {
|
|
800
819
|
const r = settled.result;
|
|
820
|
+
addChatUsage(runUsage, r.usage);
|
|
801
821
|
if (r.status === "complete") {
|
|
802
822
|
completed.add(settled.stepId);
|
|
803
823
|
stepRuntime.set(settled.stepId, {
|
|
@@ -912,7 +932,15 @@ async function runSchedulerLoop(args) {
|
|
|
912
932
|
});
|
|
913
933
|
}
|
|
914
934
|
await drainBudgetExceededNotifications(budgetExceededChildIds);
|
|
915
|
-
return {
|
|
935
|
+
return {
|
|
936
|
+
completed,
|
|
937
|
+
failed,
|
|
938
|
+
canceled,
|
|
939
|
+
budgetExceeded,
|
|
940
|
+
failureReason,
|
|
941
|
+
stepStates: stepRuntime,
|
|
942
|
+
...(hasReportedUsage(runUsage) ? { usage: { ...runUsage } } : {}),
|
|
943
|
+
};
|
|
916
944
|
}
|
|
917
945
|
async function drainBudgetExceededNotifications(childIds) {
|
|
918
946
|
while (true) {
|
|
@@ -1068,6 +1096,7 @@ async function collectAndComplete(args) {
|
|
|
1068
1096
|
note: `${completed.size} of ${input.steps.length} steps completed; results are partial.`,
|
|
1069
1097
|
}
|
|
1070
1098
|
: {}),
|
|
1099
|
+
...(args.usage ? { usage: args.usage } : {}),
|
|
1071
1100
|
});
|
|
1072
1101
|
}
|
|
1073
1102
|
else {
|
|
@@ -35,6 +35,7 @@ import { z } from "zod";
|
|
|
35
35
|
import type { RunSession } from "../auth/types.js";
|
|
36
36
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
37
37
|
import type { Logger } from "../lib/logger.js";
|
|
38
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
38
39
|
import type { AgentChat } from "../providers/types.js";
|
|
39
40
|
import { TargetAssessmentInputSchema } from "./target-assessment/schemas.js";
|
|
40
41
|
import { type ProgressPhase } from "./target-assessment/progress.js";
|
|
@@ -88,6 +89,8 @@ export interface ExecuteTargetAssessmentDeps {
|
|
|
88
89
|
readonly decisionModel: string;
|
|
89
90
|
/** Billing-gateway model for synthesis agents — typically `env.TARGET_ASSESSMENT_SYNTHESIS_MODEL`. */
|
|
90
91
|
readonly synthesisModel: string;
|
|
92
|
+
/** LLM usage-accounting seam for the assessment's agent loops; omitted falls back to the no-op recorder. */
|
|
93
|
+
readonly usageRecorder?: UsageRecorder;
|
|
91
94
|
}
|
|
92
95
|
/**
|
|
93
96
|
* Register the executeTargetAssessment workflow with DBOS. Caller is
|
|
@@ -26,8 +26,10 @@
|
|
|
26
26
|
* parent (`executeAnalysis`). This child only owns its own teardown and
|
|
27
27
|
* its own self-cancel on 402.
|
|
28
28
|
*/
|
|
29
|
+
import type { TokenUsageRollup } from "@inflexa-ai/harness/contracts/usage.js";
|
|
29
30
|
import type { Pool } from "pg";
|
|
30
31
|
import type { Logger } from "../lib/logger.js";
|
|
32
|
+
import type { UsageRecorder } from "../billing/usage-recorder.js";
|
|
31
33
|
import type { AgentDefinition, LoopMessage } from "../loop/types.js";
|
|
32
34
|
import type { AgentChat, EmbeddingProvider } from "../providers/types.js";
|
|
33
35
|
import { type RunSession } from "../auth/types.js";
|
|
@@ -125,6 +127,17 @@ export interface SandboxStepResult {
|
|
|
125
127
|
readonly durationMs: number;
|
|
126
128
|
readonly finishReason: string | null;
|
|
127
129
|
readonly error: string | null;
|
|
130
|
+
/**
|
|
131
|
+
* What this step's agent loop reported spending. Absent when it reported
|
|
132
|
+
* nothing — never an all-zero figure.
|
|
133
|
+
*
|
|
134
|
+
* Durable by virtue of being the child's workflow result: the parent folds
|
|
135
|
+
* the run's aggregate from these, and a recovered parent reads back the same
|
|
136
|
+
* values, so the aggregate needs no side channel to survive replay. Optional
|
|
137
|
+
* because this is a durable shape — a child that settled under an older
|
|
138
|
+
* result shape recovers without it.
|
|
139
|
+
*/
|
|
140
|
+
readonly usage?: TokenUsageRollup;
|
|
128
141
|
}
|
|
129
142
|
/** One file in the step's terminal file tree / output listing. */
|
|
130
143
|
export interface StepFileEntry {
|
|
@@ -204,6 +217,11 @@ export interface SandboxStepDeps {
|
|
|
204
217
|
* account of why a step died.
|
|
205
218
|
*/
|
|
206
219
|
readonly logger?: Logger;
|
|
220
|
+
/**
|
|
221
|
+
* LLM usage-accounting seam. Covers this step's agent loop and the
|
|
222
|
+
* post-step sub-agent loops alike; omitted falls back to the no-op recorder.
|
|
223
|
+
*/
|
|
224
|
+
readonly usageRecorder?: UsageRecorder;
|
|
207
225
|
/** Non-streaming chat — drives the agent loop + the post-step sub-agents. */
|
|
208
226
|
readonly provider: AgentChat;
|
|
209
227
|
/** Write-side embedder for the post-step vector index. */
|
|
@@ -275,3 +293,8 @@ export declare function createLineageCollector(input: Pick<SandboxStepInput, "st
|
|
|
275
293
|
* `dbos.application_versions` without per-workflow config.
|
|
276
294
|
*/
|
|
277
295
|
export declare function registerSandboxStep(deps: SandboxStepDeps): (input: SandboxStepInput) => Promise<SandboxStepResult>;
|
|
296
|
+
/**
|
|
297
|
+
* Body extracted so tests can drive it without registering a workflow
|
|
298
|
+
* (the DBOS calls inside still rely on a workflow context being present).
|
|
299
|
+
*/
|
|
300
|
+
export declare function runSandboxStepBody(input: SandboxStepInput, deps: SandboxStepDeps): Promise<SandboxStepResult>;
|
|
@@ -34,6 +34,7 @@ import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
|
34
34
|
import { runAgent } from "../loop/run-agent.js";
|
|
35
35
|
import { durableStep } from "../loop/run-step.js";
|
|
36
36
|
import { activityForTool, applyTreeDelta, isChatDataPart, sandboxTreeDelta, stepPartId } from "../sandbox/sandbox-step-translate.js";
|
|
37
|
+
import { createDetailResolver } from "../tools/detail-resolver.js";
|
|
37
38
|
import { forSubAgent } from "../auth/types.js";
|
|
38
39
|
import { walkStepArtifacts } from "../execution/post-step.js";
|
|
39
40
|
import { collectStepOutputs, generateStepFileMetadata, generateStepSummaryAndWrite, reconcileAndRegisterStepArtifacts, vectorIndexStepOutputs, } from "../execution/post-step-pipeline.js";
|
|
@@ -110,7 +111,7 @@ export function registerSandboxStep(deps) {
|
|
|
110
111
|
* Body extracted so tests can drive it without registering a workflow
|
|
111
112
|
* (the DBOS calls inside still rely on a workflow context being present).
|
|
112
113
|
*/
|
|
113
|
-
async function runSandboxStepBody(input, deps) {
|
|
114
|
+
export async function runSandboxStepBody(input, deps) {
|
|
114
115
|
// Step coordinates bind once — every record below carries them without each
|
|
115
116
|
// call site restating them. REPLAY: a recovered workflow re-executes this body
|
|
116
117
|
// and re-emits its records, so a reader dedups by (runId, stepId), not by count.
|
|
@@ -193,6 +194,9 @@ async function runSandboxStepBody(input, deps) {
|
|
|
193
194
|
lineageCollector,
|
|
194
195
|
blockerHolder,
|
|
195
196
|
});
|
|
197
|
+
// Built over THIS agent's tools, not a shared table: a sandbox agent's roster
|
|
198
|
+
// differs per agent type, and each tool describes its own calls.
|
|
199
|
+
const resolveDetail = createDetailResolver(agent.tools, logger);
|
|
196
200
|
// Forward every loop event onto the child's own DBOS `"events"` stream
|
|
197
201
|
// (the route fans-in parent + every active child stream — Pattern A).
|
|
198
202
|
// Pre-shaped chat data parts (e.g. `data-sandbox-event` from `run-exec.ts`)
|
|
@@ -301,7 +305,7 @@ async function runSandboxStepBody(input, deps) {
|
|
|
301
305
|
// envelope, and surface each tool call as live step activity.
|
|
302
306
|
await safeEmit({ type: "data-loop-event", data: { stepId: input.stepId, event } });
|
|
303
307
|
if (event.type === "tool-started") {
|
|
304
|
-
await emitActivity("executing", activityForTool(event.name, event.input));
|
|
308
|
+
await emitActivity("executing", activityForTool(event.name, event.input, resolveDetail));
|
|
305
309
|
}
|
|
306
310
|
};
|
|
307
311
|
const initial = [{ role: "user", content: input.prompt }];
|
|
@@ -312,12 +316,14 @@ async function runSandboxStepBody(input, deps) {
|
|
|
312
316
|
let transcript;
|
|
313
317
|
let finishReason;
|
|
314
318
|
let hitMaxSteps;
|
|
319
|
+
let stepUsage;
|
|
315
320
|
try {
|
|
316
321
|
const agentResult = await runAgent(agent, initial, session, {
|
|
317
322
|
provider: deps.provider,
|
|
318
323
|
signal: neverAbort,
|
|
319
324
|
emit,
|
|
320
325
|
runStep: durableStep,
|
|
326
|
+
usageRecorder: deps.usageRecorder,
|
|
321
327
|
formatStepName: {
|
|
322
328
|
llm: (iteration) => `llm:${iteration}`,
|
|
323
329
|
tool: (toolName, toolUseId) => `tool:${toolName}:${toolUseId}`,
|
|
@@ -327,6 +333,7 @@ async function runSandboxStepBody(input, deps) {
|
|
|
327
333
|
transcript = agentResult.messages;
|
|
328
334
|
finishReason = agentResult.finish.reason;
|
|
329
335
|
hitMaxSteps = agentResult.finish.cappedOut;
|
|
336
|
+
stepUsage = agentResult.finish.usage;
|
|
330
337
|
}
|
|
331
338
|
catch (err) {
|
|
332
339
|
if (isBudgetExceeded(err)) {
|
|
@@ -380,6 +387,28 @@ async function runSandboxStepBody(input, deps) {
|
|
|
380
387
|
// `failStep` always throws; the outer `throw` is for control-flow analysis.
|
|
381
388
|
throw await failStep("agent_loop", err);
|
|
382
389
|
}
|
|
390
|
+
// What the step's loop spent, emitted the moment the loop is done and before
|
|
391
|
+
// any branch that can end the step — so a step that later blocks or fails its
|
|
392
|
+
// post-step pipeline has still reported what it cost.
|
|
393
|
+
//
|
|
394
|
+
// No rollup means NO PART: the part's contract is to carry a step's rollup,
|
|
395
|
+
// and a loop that reported nothing has none to carry. A part naming the step
|
|
396
|
+
// with an absent usage field would be a claim about a figure nobody made.
|
|
397
|
+
//
|
|
398
|
+
// Stable id per (runId, stepId), like every other step part here, so a
|
|
399
|
+
// DBOS-replayed body's re-emission folds latest-wins onto the one part
|
|
400
|
+
// instead of appearing twice on the run's stream.
|
|
401
|
+
if (stepUsage) {
|
|
402
|
+
await safeEmit({
|
|
403
|
+
type: "data-step-usage",
|
|
404
|
+
id: stepPartId("step-usage", input.runId, input.stepId),
|
|
405
|
+
runId: input.runId,
|
|
406
|
+
stepId: input.stepId,
|
|
407
|
+
agentId: input.agentId,
|
|
408
|
+
modelId: agent.model,
|
|
409
|
+
usage: stepUsage,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
383
412
|
// Blocker (see the harness-sandbox-agents spec): the agent declared it cannot fulfil the step. A
|
|
384
413
|
// blocked step has no deliverables — skip the artifact post-step pipeline,
|
|
385
414
|
// mark `blocked` (carrying the reason), emit the blocked part, and return a
|
|
@@ -414,6 +443,7 @@ async function runSandboxStepBody(input, deps) {
|
|
|
414
443
|
durationMs,
|
|
415
444
|
finishReason,
|
|
416
445
|
error: blockerOutcome.reason,
|
|
446
|
+
...(stepUsage ? { usage: stepUsage } : {}),
|
|
417
447
|
};
|
|
418
448
|
}
|
|
419
449
|
// (4-8) post-step — the body is the explicit composition: walk the artifact
|
|
@@ -547,6 +577,7 @@ async function runSandboxStepBody(input, deps) {
|
|
|
547
577
|
durationMs,
|
|
548
578
|
finishReason,
|
|
549
579
|
error: null,
|
|
580
|
+
...(stepUsage ? { usage: stepUsage } : {}),
|
|
550
581
|
};
|
|
551
582
|
}
|
|
552
583
|
/**
|
|
@@ -19,6 +19,7 @@ import { z } from "zod";
|
|
|
19
19
|
import type { Pool } from "pg";
|
|
20
20
|
import type { AgentSession } from "../../../auth/types.js";
|
|
21
21
|
import type { ChatProvider } from "../../../providers/types.js";
|
|
22
|
+
import type { UsageRecorder } from "../../../billing/usage-recorder.js";
|
|
22
23
|
export declare const OffTargetAnnotationSchema: z.ZodObject<{
|
|
23
24
|
clinical_consequence: z.ZodString;
|
|
24
25
|
provenance: z.ZodString;
|
|
@@ -28,6 +29,8 @@ export interface ClinicalConsequenceAnnotatorDeps {
|
|
|
28
29
|
readonly provider: ChatProvider;
|
|
29
30
|
readonly session: AgentSession;
|
|
30
31
|
readonly model: string;
|
|
32
|
+
/** LLM usage-accounting seam for the annotator loop; omitted falls back to the no-op recorder. */
|
|
33
|
+
readonly usageRecorder?: UsageRecorder;
|
|
31
34
|
}
|
|
32
35
|
export interface AnnotationInput {
|
|
33
36
|
primaryTargetGene: string;
|
|
@@ -126,6 +126,7 @@ export async function annotateClinicalConsequence(pool, input, deps) {
|
|
|
126
126
|
signal: controller.signal,
|
|
127
127
|
emit: () => { },
|
|
128
128
|
runStep: passthroughStep,
|
|
129
|
+
usageRecorder: deps.usageRecorder,
|
|
129
130
|
});
|
|
130
131
|
const raw = extractJsonObject(finalText(messages));
|
|
131
132
|
if (raw === null)
|
package/package.json
CHANGED