@inflexa-ai/harness 0.2.0 → 0.3.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 +48 -0
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +2 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +2 -2
- package/dist/app/chat-turn.d.ts +3 -0
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +3 -1
- package/dist/execution/artifact-metadata.d.ts +3 -0
- package/dist/execution/artifact-metadata.js +8 -2
- package/dist/execution/artifact-registration.d.ts +2 -1
- package/dist/execution/artifact-registration.js +8 -2
- package/dist/execution/ephemeral-runner.d.ts +3 -0
- package/dist/execution/ephemeral-runner.js +8 -4
- package/dist/execution/post-step-pipeline.d.ts +3 -0
- package/dist/execution/post-step-pipeline.js +24 -4
- package/dist/execution/reconcile-manifest.d.ts +7 -0
- package/dist/execution/reconcile-manifest.js +19 -5
- package/dist/execution/report-runner.d.ts +3 -0
- package/dist/execution/report-runner.js +4 -4
- package/dist/execution/step-summary.d.ts +3 -0
- package/dist/execution/step-summary.js +4 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/lib/chrome.d.ts +4 -1
- package/dist/lib/chrome.js +10 -7
- package/dist/lib/console-logger.d.ts +19 -0
- package/dist/lib/console-logger.js +62 -0
- package/dist/lib/logger.d.ts +88 -0
- package/dist/lib/logger.js +37 -0
- package/dist/lib/otel.d.ts +2 -1
- package/dist/lib/otel.js +8 -3
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/connection-budget.d.ts +2 -2
- package/dist/runtime/connection-budget.js +5 -3
- package/dist/runtime/dbos.d.ts +7 -7
- package/dist/runtime/dbos.js +14 -11
- package/dist/runtime/shutdown.d.ts +2 -2
- package/dist/runtime/shutdown.js +4 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/docker-client.d.ts +2 -2
- package/dist/sandbox/docker-client.js +4 -1
- package/dist/sandbox/k8s-client.d.ts +3 -0
- package/dist/sandbox/k8s-client.js +5 -4
- package/dist/sandbox/notification-sweep.d.ts +3 -3
- package/dist/sandbox/notification-sweep.js +4 -2
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +7 -4
- package/dist/sandbox/watchdog.d.ts +3 -3
- package/dist/sandbox/watchdog.js +8 -2
- package/dist/state/init.d.ts +2 -1
- package/dist/state/init.js +5 -6
- package/dist/tasks/data-profile.d.ts +5 -0
- package/dist/tasks/data-profile.js +24 -17
- package/dist/tools/bio/search-dgidb.d.ts +11 -1
- package/dist/tools/bio/search-dgidb.js +92 -78
- package/dist/tools/iterate-report.d.ts +3 -0
- package/dist/tools/iterate-report.js +3 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -2
- package/dist/tools/workspace/execute-command.d.ts +3 -0
- package/dist/tools/workspace/execute-command.js +3 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +11 -7
- package/dist/workflows/execute-target-assessment.d.ts +3 -0
- package/dist/workflows/execute-target-assessment.js +19 -15
- package/dist/workflows/sandbox-step.d.ts +9 -0
- package/dist/workflows/sandbox-step.js +39 -17
- package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
- package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
- package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
- package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
- package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
- package/dist/workflows/target-assessment/phase5-persist.js +8 -3
- package/dist/workflows/target-assessment/progress.d.ts +2 -1
- package/dist/workflows/target-assessment/progress.js +4 -3
- package/dist/workspace/filesystem.js +163 -74
- package/package.json +95 -96
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import { ok } from "neverthrow";
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
import { defineTool } from "../define-tool.js";
|
|
14
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
14
15
|
import { apiFetchValidated, describeApiError } from "../lib/api-utils.js";
|
|
15
16
|
const DGIDB_GRAPHQL_URL = "https://dgidb.org/api/graphql";
|
|
16
17
|
const GENE_INTERACTIONS_QUERY = `query($genes: [String!]!) {
|
|
@@ -115,85 +116,98 @@ function applyFilters(interactions, filters) {
|
|
|
115
116
|
}
|
|
116
117
|
return out;
|
|
117
118
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (res.isErr())
|
|
153
|
-
throw new Error(describeApiError(res.error));
|
|
154
|
-
const body = res.value;
|
|
155
|
-
if (!body || !body.data)
|
|
156
|
-
throw new Error("DGIdb returned no data");
|
|
157
|
-
if (body.errors?.length) {
|
|
158
|
-
console.warn("dgidb.partial_errors", { errors: body.errors.slice(0, 3) });
|
|
159
|
-
}
|
|
160
|
-
const nodes = (isGene ? body.data.genes?.nodes : body.data.drugs?.nodes) ?? [];
|
|
161
|
-
const nodeMap = new Map();
|
|
162
|
-
for (const node of nodes) {
|
|
163
|
-
nodeMap.set(node.name.toLowerCase(), node);
|
|
164
|
-
if (!isGene && node.conceptId) {
|
|
165
|
-
nodeMap.set(node.conceptId.toLowerCase(), node);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
const results = inputs.map((input) => {
|
|
169
|
-
const node = nodeMap.get(input.toLowerCase());
|
|
170
|
-
if (!node) {
|
|
171
|
-
return { input, found: false, interactions: [] };
|
|
119
|
+
/**
|
|
120
|
+
* A factory rather than a bare const (unlike its bio-leaf siblings) because it
|
|
121
|
+
* logs: DGIdb answers GraphQL partial errors alongside usable data, and an
|
|
122
|
+
* operator debugging odd interaction results needs to see them. `ToolContext`
|
|
123
|
+
* carries no deps by design, so a tool that logs is dep-bearing — the same
|
|
124
|
+
* reason `createSearchClinvarTool` takes its key that way.
|
|
125
|
+
*/
|
|
126
|
+
export function createSearchDgidbTool(deps = {}) {
|
|
127
|
+
const logger = (deps.logger ?? createNoopLogger()).named("search_dgidb");
|
|
128
|
+
return defineTool({
|
|
129
|
+
id: "search_dgidb",
|
|
130
|
+
description: "Query the Drug-Gene Interaction Database (DGIdb) for drug-gene interactions. " +
|
|
131
|
+
"DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB, GuideToPharmacology, CIViC, etc.) " +
|
|
132
|
+
"and returns source counts as a confidence signal. " +
|
|
133
|
+
"searchType='gene' returns drugs interacting with input HUGO gene symbols (most common: 'what drugs hit my gene set?'). " +
|
|
134
|
+
"searchType='drug' returns the genes each input drug is known to act on. " +
|
|
135
|
+
"Free, no API key required. Sorted by source count desc.",
|
|
136
|
+
inputSchema: z.object({
|
|
137
|
+
query: z
|
|
138
|
+
.union([z.string(), z.array(z.string()).max(50)])
|
|
139
|
+
.describe("Single identifier or list of up to 50. For searchType='gene': HUGO symbols. For searchType='drug': drug names or DGIdb concept IDs."),
|
|
140
|
+
searchType: z.enum(["gene", "drug"]).default("gene"),
|
|
141
|
+
interactionTypes: z
|
|
142
|
+
.array(z.string())
|
|
143
|
+
.optional()
|
|
144
|
+
.describe("Case-insensitive substring filter on interaction type. e.g. ['inhibitor', 'antagonist']"),
|
|
145
|
+
sources: z.array(z.string()).optional().describe("Restrict to interactions from these source DBs (case-insensitive substring)."),
|
|
146
|
+
minSources: z.number().int().min(1).default(1).describe("Drop interactions supported by fewer than this many sources."),
|
|
147
|
+
limit: z.number().int().min(1).max(200).default(50).describe("Max interactions per input gene/drug after filtering."),
|
|
148
|
+
}),
|
|
149
|
+
execute: async ({ query, searchType = "gene", interactionTypes, sources, minSources = 1, limit = 50 }) => {
|
|
150
|
+
const inputs = typeof query === "string" ? [query] : query;
|
|
151
|
+
if (inputs.length === 0 || inputs.every((s) => !s.trim())) {
|
|
152
|
+
throw new Error("query must contain at least one identifier");
|
|
172
153
|
}
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
minSources,
|
|
154
|
+
const isGene = searchType === "gene";
|
|
155
|
+
const variableName = isGene ? "genes" : "drugs";
|
|
156
|
+
const queryString = isGene ? GENE_INTERACTIONS_QUERY : DRUG_INTERACTIONS_QUERY;
|
|
157
|
+
const res = await apiFetchValidated(DGIDB_GRAPHQL_URL, DgidbResponseSchema, {
|
|
158
|
+
method: "POST",
|
|
159
|
+
headers: { "Content-Type": "application/json" },
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
query: queryString,
|
|
162
|
+
variables: { [variableName]: inputs },
|
|
163
|
+
}),
|
|
184
164
|
});
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
165
|
+
if (res.isErr())
|
|
166
|
+
throw new Error(describeApiError(res.error));
|
|
167
|
+
const body = res.value;
|
|
168
|
+
if (!body || !body.data)
|
|
169
|
+
throw new Error("DGIdb returned no data");
|
|
170
|
+
if (body.errors?.length) {
|
|
171
|
+
logger.warn("dgidb partial errors alongside data", { errors: body.errors.slice(0, 3) });
|
|
172
|
+
}
|
|
173
|
+
const nodes = (isGene ? body.data.genes?.nodes : body.data.drugs?.nodes) ?? [];
|
|
174
|
+
const nodeMap = new Map();
|
|
175
|
+
for (const node of nodes) {
|
|
176
|
+
nodeMap.set(node.name.toLowerCase(), node);
|
|
177
|
+
if (!isGene && node.conceptId) {
|
|
178
|
+
nodeMap.set(node.conceptId.toLowerCase(), node);
|
|
188
179
|
}
|
|
189
|
-
|
|
180
|
+
}
|
|
181
|
+
const results = inputs.map((input) => {
|
|
182
|
+
const node = nodeMap.get(input.toLowerCase());
|
|
183
|
+
if (!node) {
|
|
184
|
+
return { input, found: false, interactions: [] };
|
|
185
|
+
}
|
|
186
|
+
const inputSide = isGene
|
|
187
|
+
? { geneName: node.name }
|
|
188
|
+
: {
|
|
189
|
+
drugName: node.name,
|
|
190
|
+
drugConceptId: node.conceptId ?? undefined,
|
|
191
|
+
};
|
|
192
|
+
const mapped = (node.interactions ?? []).map((raw) => mapInteraction(raw, searchType, inputSide));
|
|
193
|
+
const filtered = applyFilters(mapped, {
|
|
194
|
+
interactionTypes,
|
|
195
|
+
sources,
|
|
196
|
+
minSources,
|
|
197
|
+
});
|
|
198
|
+
const sorted = filtered.sort((a, b) => {
|
|
199
|
+
if (b.sourceCount !== a.sourceCount) {
|
|
200
|
+
return b.sourceCount - a.sourceCount;
|
|
201
|
+
}
|
|
202
|
+
return (b.interactionScore ?? 0) - (a.interactionScore ?? 0);
|
|
203
|
+
});
|
|
204
|
+
return {
|
|
205
|
+
input,
|
|
206
|
+
found: true,
|
|
207
|
+
interactions: sorted.slice(0, limit),
|
|
208
|
+
};
|
|
190
209
|
});
|
|
191
|
-
return {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
};
|
|
196
|
-
});
|
|
197
|
-
return ok({ results });
|
|
198
|
-
},
|
|
199
|
-
});
|
|
210
|
+
return ok({ results });
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
@@ -47,6 +47,7 @@ import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
|
47
47
|
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
|
+
import type { Logger } from "../lib/logger.js";
|
|
50
51
|
/**
|
|
51
52
|
* The report brief the conversation agent composes: title/audience + sources +
|
|
52
53
|
* the section union. This is the ~12k-char schema that `plan_report` hands back
|
|
@@ -183,6 +184,8 @@ export declare const submitReportInputSchema: z.ZodObject<{
|
|
|
183
184
|
*/
|
|
184
185
|
export declare const planReportTool: Tool;
|
|
185
186
|
export interface SubmitReportDeps {
|
|
187
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
188
|
+
readonly logger?: Logger;
|
|
186
189
|
readonly provider: ChatProvider;
|
|
187
190
|
readonly pool: Pool;
|
|
188
191
|
/** Embedder-supplied workspace-root resolution seam (see workspace/paths.ts). */
|
|
@@ -49,6 +49,7 @@ import { defineTool } from "./define-tool.js";
|
|
|
49
49
|
import { previewDir } from "../workspace/paths.js";
|
|
50
50
|
import { runReportIteration } from "../execution/report-runner.js";
|
|
51
51
|
import { formatBytes, stageReportAssets } from "./lib/report-preflight.js";
|
|
52
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
52
53
|
const REPORT_TOOL_ACCESS_TTL_SECONDS = 3600;
|
|
53
54
|
const PREVIEW_META_FILE = "preview-meta.json";
|
|
54
55
|
async function readPreviewMeta(metaPath) {
|
|
@@ -577,7 +578,8 @@ export function createReportSubmitTool(deps) {
|
|
|
577
578
|
});
|
|
578
579
|
}
|
|
579
580
|
catch (err) {
|
|
580
|
-
|
|
581
|
+
const logger = (deps.logger ?? createNoopLogger()).named("submit-report");
|
|
582
|
+
logger.warn("failed to persist preview meta", logger.errorFields(err));
|
|
581
583
|
}
|
|
582
584
|
}
|
|
583
585
|
const previewTitle = brief?.title ?? existingMeta?.title ?? "Report";
|
|
@@ -17,7 +17,10 @@
|
|
|
17
17
|
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
|
+
import type { Logger } from "../../lib/logger.js";
|
|
20
21
|
export interface LiteratureReviewerDeps {
|
|
22
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
23
|
+
readonly logger?: Logger;
|
|
21
24
|
/** The LLM seam the child loop runs on. */
|
|
22
25
|
readonly provider: ChatProvider;
|
|
23
26
|
/** Model id — provenance / metric label; the provider owns the wire model. */
|
|
@@ -26,7 +26,7 @@ import { createNcbiTools } from "../bio/keys.js";
|
|
|
26
26
|
import { getImpcKoProfileTool } from "../bio/get-impc-ko-profile.js";
|
|
27
27
|
import { lookupGoTermTool } from "../bio/lookup-go-term.js";
|
|
28
28
|
import { searchBgeeExpressionTool } from "../bio/search-bgee-expression.js";
|
|
29
|
-
import {
|
|
29
|
+
import { createSearchDgidbTool } from "../bio/search-dgidb.js";
|
|
30
30
|
import { searchGeneTool } from "../bio/search-gene.js";
|
|
31
31
|
import { searchInteractionsTool } from "../bio/search-interactions.js";
|
|
32
32
|
import { searchPathwayTool } from "../bio/search-pathway.js";
|
|
@@ -43,7 +43,7 @@ export function createLiteratureReviewerTool(deps) {
|
|
|
43
43
|
lookupGoTermTool,
|
|
44
44
|
searchInteractionsTool,
|
|
45
45
|
ncbi.pubmed,
|
|
46
|
-
|
|
46
|
+
createSearchDgidbTool({ ...(deps.logger ? { logger: deps.logger } : {}) }),
|
|
47
47
|
searchBgeeExpressionTool,
|
|
48
48
|
getImpcKoProfileTool,
|
|
49
49
|
];
|
|
@@ -22,7 +22,10 @@
|
|
|
22
22
|
import type { SandboxClient } from "../../sandbox/client.js";
|
|
23
23
|
import type { SandboxRef } from "../../sandbox/types.js";
|
|
24
24
|
import type { ProvenanceCollector } from "../../provenance/collector.js";
|
|
25
|
+
import type { Logger } from "../../lib/logger.js";
|
|
25
26
|
export interface ExecuteCommandDeps {
|
|
27
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
28
|
+
readonly logger?: Logger;
|
|
26
29
|
readonly sandboxClient: SandboxClient;
|
|
27
30
|
/** Live sandbox handle for the step. Created once per step at composition root. */
|
|
28
31
|
readonly sandbox: SandboxRef;
|
|
@@ -26,6 +26,7 @@ import { defineTool } from "../define-tool.js";
|
|
|
26
26
|
import { feedExecFrame } from "../../provenance/exec-frame.js";
|
|
27
27
|
import { boundExecResult } from "./result-bounds.js";
|
|
28
28
|
import { runSandboxExec } from "./run-exec.js";
|
|
29
|
+
import { createNoopLogger } from "../../lib/console-logger.js";
|
|
29
30
|
const ExecuteCommandInputSchema = z.object({
|
|
30
31
|
command: z
|
|
31
32
|
.array(z.string().min(1))
|
|
@@ -86,7 +87,8 @@ export function createExecuteCommandTool(deps) {
|
|
|
86
87
|
});
|
|
87
88
|
}
|
|
88
89
|
catch (err) {
|
|
89
|
-
|
|
90
|
+
const logger = (deps.logger ?? createNoopLogger()).named("execute_command");
|
|
91
|
+
logger.warn("provenance frame handling failed (non-fatal)", { execId, ...logger.errorFields(err) });
|
|
90
92
|
}
|
|
91
93
|
}
|
|
92
94
|
return ok({ status: "ok", ...boundExecResult(result) });
|
|
@@ -57,6 +57,7 @@ import type { Pool } from "pg";
|
|
|
57
57
|
import type { MachineBudget, ResourceSpec } from "../config/resource-limits.js";
|
|
58
58
|
import type { RunSession } from "../auth/types.js";
|
|
59
59
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
60
|
+
import type { Logger } from "../lib/logger.js";
|
|
60
61
|
import type { AnalysisStep } from "../schemas/workflow-state.js";
|
|
61
62
|
import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
|
|
62
63
|
import type { BioToolKeys } from "../tools/bio/keys.js";
|
|
@@ -206,6 +207,8 @@ export type RunProvenanceEvent = {
|
|
|
206
207
|
* embedder, model, keys, roots) the in-body synthesis needs.
|
|
207
208
|
*/
|
|
208
209
|
export interface ExecuteAnalysisDeps {
|
|
210
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
211
|
+
readonly logger?: Logger;
|
|
209
212
|
readonly pool: Pool;
|
|
210
213
|
/** Chat provider for the in-body literature-grounded synthesis. */
|
|
211
214
|
readonly provider: ChatProvider;
|
|
@@ -57,6 +57,7 @@ import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
|
57
57
|
import { mkdir } from "node:fs/promises";
|
|
58
58
|
import { join } from "node:path";
|
|
59
59
|
import { forStep } from "../auth/types.js";
|
|
60
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
60
61
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
61
62
|
import { RunDedupCollisionError, countArtifactsForRun, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, } from "../state/index.js";
|
|
62
63
|
import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
@@ -81,13 +82,14 @@ function emitStreamPart(part) {
|
|
|
81
82
|
* host's own ledger, not here.
|
|
82
83
|
*/
|
|
83
84
|
function emitProvenanceGuarded(deps, event) {
|
|
85
|
+
const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis");
|
|
84
86
|
if (!deps.emitProvenance)
|
|
85
87
|
return;
|
|
86
88
|
try {
|
|
87
89
|
deps.emitProvenance(event);
|
|
88
90
|
}
|
|
89
91
|
catch (err) {
|
|
90
|
-
|
|
92
|
+
logger.error("emitProvenance threw", { runId: event.runId, event: event.type, ...logger.errorFields(err) });
|
|
91
93
|
}
|
|
92
94
|
}
|
|
93
95
|
/**
|
|
@@ -261,6 +263,7 @@ export function registerExecuteAnalysis(deps) {
|
|
|
261
263
|
* (the DBOS calls inside still rely on a workflow context being present).
|
|
262
264
|
*/
|
|
263
265
|
export async function runExecuteAnalysisBody(input, deps) {
|
|
266
|
+
const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis").with({ analysisId: input.analysisId });
|
|
264
267
|
// (0) Validate up-front — no side effects yet, so a malformed plan does
|
|
265
268
|
// NOT leak a runId, run authorization, or running charge.
|
|
266
269
|
validatePlanDag(input.steps);
|
|
@@ -344,7 +347,7 @@ export async function runExecuteAnalysisBody(input, deps) {
|
|
|
344
347
|
}
|
|
345
348
|
catch (err) {
|
|
346
349
|
synthesisError = err;
|
|
347
|
-
|
|
350
|
+
logger.error("synthesizeFindings failed — failing the run", { runId, ...logger.errorFields(err) });
|
|
348
351
|
}
|
|
349
352
|
}
|
|
350
353
|
// (6) collectAndComplete — terminal block. Runs on EVERY path. When synthesis
|
|
@@ -693,6 +696,7 @@ async function cancelInFlight(inFlight, cause, canceledByParent) {
|
|
|
693
696
|
}
|
|
694
697
|
async function collectAndComplete(args) {
|
|
695
698
|
const { input, runId, workflowId, startedAtMs, completed, failed, canceled, budgetExceeded, failureReason, forceFailed, deps } = args;
|
|
699
|
+
const logger = (deps.logger ?? createNoopLogger()).named("executeAnalysis").with({ runId, analysisId: input.analysisId });
|
|
696
700
|
const status = forceFailed
|
|
697
701
|
? "failed"
|
|
698
702
|
: deriveFinalStatus({
|
|
@@ -735,7 +739,7 @@ async function collectAndComplete(args) {
|
|
|
735
739
|
}, { name: "persist-final-status" });
|
|
736
740
|
}
|
|
737
741
|
catch (err) {
|
|
738
|
-
|
|
742
|
+
logger.error("persist-final-status failed", { status, ...logger.errorFields(err) });
|
|
739
743
|
}
|
|
740
744
|
// Sweep never-started steps to `skipped` — but ONLY on genuinely-terminal
|
|
741
745
|
// paths. The gate is the BRANCH, not the written run status: the resumable
|
|
@@ -749,7 +753,7 @@ async function collectAndComplete(args) {
|
|
|
749
753
|
}, { name: "sweep-pending-steps" });
|
|
750
754
|
}
|
|
751
755
|
catch (err) {
|
|
752
|
-
|
|
756
|
+
logger.error("sweep-pending-steps failed", logger.errorFields(err));
|
|
753
757
|
}
|
|
754
758
|
}
|
|
755
759
|
// A forced failure (synthesis threw) is terminal, not a resumable budget
|
|
@@ -761,7 +765,7 @@ async function collectAndComplete(args) {
|
|
|
761
765
|
}, { name: "suspend-analysis" });
|
|
762
766
|
}
|
|
763
767
|
catch (err) {
|
|
764
|
-
|
|
768
|
+
logger.error("suspend-analysis failed", logger.errorFields(err));
|
|
765
769
|
}
|
|
766
770
|
}
|
|
767
771
|
try {
|
|
@@ -773,7 +777,7 @@ async function collectAndComplete(args) {
|
|
|
773
777
|
}), { name: "close-running-charge" });
|
|
774
778
|
}
|
|
775
779
|
catch (err) {
|
|
776
|
-
|
|
780
|
+
logger.error("closeRunningCharge failed", { chargeReason, ...logger.errorFields(err) });
|
|
777
781
|
}
|
|
778
782
|
// Revoke the run authorization for the terminal run state. Ownership
|
|
779
783
|
// defaults to true for inputs persisted before the field existed (a
|
|
@@ -786,7 +790,7 @@ async function collectAndComplete(args) {
|
|
|
786
790
|
await DBOS.runStep(() => deps.runAuthorizer.revoke(authorization, revokeReason), { name: "revoke-run-auth" });
|
|
787
791
|
}
|
|
788
792
|
catch (err) {
|
|
789
|
-
|
|
793
|
+
logger.error("revokeRunAuthorization failed", { revokeReason, ...logger.errorFields(err) });
|
|
790
794
|
}
|
|
791
795
|
// The `run_completed` provenance was already emitted above (before the status write, to
|
|
792
796
|
// beat the CLI's poll-and-shutdown). These branches only fan out the UI stream part, whose
|
|
@@ -34,6 +34,7 @@ import type { Pool } from "pg";
|
|
|
34
34
|
import { z } from "zod";
|
|
35
35
|
import type { RunSession } from "../auth/types.js";
|
|
36
36
|
import type { RunAuthorizer } from "../execution/run-authorizer.js";
|
|
37
|
+
import type { Logger } from "../lib/logger.js";
|
|
37
38
|
import type { AgentChat } from "../providers/types.js";
|
|
38
39
|
import { TargetAssessmentInputSchema } from "./target-assessment/schemas.js";
|
|
39
40
|
import { type ProgressPhase } from "./target-assessment/progress.js";
|
|
@@ -69,6 +70,8 @@ export interface ExecuteTargetAssessmentResult {
|
|
|
69
70
|
readonly bytes: number;
|
|
70
71
|
}
|
|
71
72
|
export interface ExecuteTargetAssessmentDeps {
|
|
73
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
74
|
+
readonly logger?: Logger;
|
|
72
75
|
readonly pool: Pool;
|
|
73
76
|
/** Run-authorization seam — the terminal path revokes through `revoke`. */
|
|
74
77
|
readonly runAuthorizer: RunAuthorizer;
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
34
34
|
import { z } from "zod";
|
|
35
35
|
import { forSubAgent } from "../auth/types.js";
|
|
36
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
36
37
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
37
38
|
import { resolveTarget } from "../tools/lib/identifier-resolver.js";
|
|
38
39
|
import { TargetAssessmentInputSchema } from "./target-assessment/schemas.js";
|
|
@@ -111,6 +112,7 @@ export function registerExecuteTargetAssessment(deps) {
|
|
|
111
112
|
* (the DBOS calls inside still rely on a workflow context being present).
|
|
112
113
|
*/
|
|
113
114
|
export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
115
|
+
const logger = (deps.logger ?? createNoopLogger()).with({ assessmentId: input.assessmentId });
|
|
114
116
|
// (§6.1) Outermost try/finally — every termination path dispatches the
|
|
115
117
|
// matching terminal handler. Phase 0 throws are caught here; every
|
|
116
118
|
// other phase wraps its body in coverage envelopes and does not throw.
|
|
@@ -137,7 +139,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
137
139
|
// (§5.3, §6.3) Phase 0 — target resolution. The only legitimate throw
|
|
138
140
|
// site. On throw, the catch below records `phase0Error` and the
|
|
139
141
|
// terminal block dispatches `markFailed({kind: "target-unresolved"})`.
|
|
140
|
-
await emitProgress(deps.pool, input.assessmentId, "resolving");
|
|
142
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "resolving");
|
|
141
143
|
const resolved = await DBOS.runStep(async () => {
|
|
142
144
|
const r = await resolveTarget(input.target);
|
|
143
145
|
return {
|
|
@@ -154,7 +156,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
154
156
|
// returns the cached envelope. The per-collector run functions emit
|
|
155
157
|
// different bundle shapes; the union is widened to `unknown` here
|
|
156
158
|
// and `Phase1Bundle` is reconstructed by manifest key below.
|
|
157
|
-
await emitProgress(deps.pool, input.assessmentId, "collecting");
|
|
159
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "collecting");
|
|
158
160
|
const manifest = COLLECTOR_MANIFEST;
|
|
159
161
|
const collectorCtx = { ncbiApiKey: deps.ncbiApiKey };
|
|
160
162
|
const collectorPairs = await Promise.all(manifest.map((entry) => DBOS.runStep(() => entry.run(resolved, collectorCtx), {
|
|
@@ -185,7 +187,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
185
187
|
// (§5.5, §5.11) Phase 2 — decisions. Lazy billing resolution happens
|
|
186
188
|
// inside the chat provider's closure (TA-aware provider in §12). The
|
|
187
189
|
// session carries the `billingContextId` the provider reads from.
|
|
188
|
-
await emitProgress(deps.pool, input.assessmentId, "deciding");
|
|
190
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "deciding");
|
|
189
191
|
const decisionSession = buildSession(input, "ta-decisions");
|
|
190
192
|
const [modulatorRes, drugsRes] = await Promise.all([
|
|
191
193
|
modulatorTriage(phase1, {
|
|
@@ -226,7 +228,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
226
228
|
// (§5.6) Phase 3 — fan-out blocks. Each block iterates items via
|
|
227
229
|
// Promise.all over DBOS.runStep; `withHost` semaphores inside each
|
|
228
230
|
// item function cap concurrency to 4 per host.
|
|
229
|
-
await emitProgress(deps.pool, input.assessmentId, "fanning_out");
|
|
231
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "fanning_out");
|
|
230
232
|
const triage = phase2.decisions.modulatorTriage;
|
|
231
233
|
const triageShortlist = triage.coverage === "available"
|
|
232
234
|
? triage.data.shortlist.map((m) => ({
|
|
@@ -287,7 +289,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
287
289
|
},
|
|
288
290
|
};
|
|
289
291
|
// (§5.7) Phase 4 — deterministic assembly. Single DBOS step.
|
|
290
|
-
await emitProgress(deps.pool, input.assessmentId, "assembling");
|
|
292
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "assembling");
|
|
291
293
|
const phase4 = await DBOS.runStep(() => phase4Assemble(deps.pool, phase3), { name: "ta-phase4-assemble" });
|
|
292
294
|
// (§5.8-pre) Approval-precedent grounding — one deterministic openFDA
|
|
293
295
|
// lookup, rendered to markdown and injected into every synthesis prompt.
|
|
@@ -301,14 +303,14 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
301
303
|
result = await fetchApprovalPrecedents({ indication });
|
|
302
304
|
}
|
|
303
305
|
catch (err) {
|
|
304
|
-
|
|
306
|
+
logger.named("ta-approval-precedents").warn("openFDA lookup failed", { indication, ...logger.errorFields(err) });
|
|
305
307
|
result = null;
|
|
306
308
|
}
|
|
307
309
|
}
|
|
308
310
|
return renderApprovalPrecedents(indication, result);
|
|
309
311
|
}, { name: "ta-approval-precedents" });
|
|
310
312
|
// (§5.8) Phase 5 — three per-section syntheses in parallel.
|
|
311
|
-
await emitProgress(deps.pool, input.assessmentId, "synthesizing");
|
|
313
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "synthesizing");
|
|
312
314
|
const synthesisDeps = (agentId) => ({
|
|
313
315
|
chatProvider: deps.chatProvider,
|
|
314
316
|
session: { ...buildSession(input, agentId) },
|
|
@@ -343,6 +345,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
343
345
|
// and returns the persisted-shape bundle. Schema/derived violations
|
|
344
346
|
// throw and the terminal handler dispatches to `markFailed`.
|
|
345
347
|
const persisted = await DBOS.runStep(() => phase5Persist({
|
|
348
|
+
logger,
|
|
346
349
|
assessmentId: input.assessmentId,
|
|
347
350
|
phase4Dossier: phase4.dossier,
|
|
348
351
|
phase2,
|
|
@@ -390,10 +393,10 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
390
393
|
await DBOS.runStep(async () => unwrapOrThrow(await setDossier(deps.pool, input.assessmentId, dossierForPersist)), {
|
|
391
394
|
name: "ta-terminal-completed",
|
|
392
395
|
});
|
|
393
|
-
await emitProgress(deps.pool, input.assessmentId, "completed");
|
|
396
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "completed");
|
|
394
397
|
}
|
|
395
398
|
catch (err) {
|
|
396
|
-
|
|
399
|
+
logger.named("ta-terminal").error("setDossier failed", logger.errorFields(err));
|
|
397
400
|
}
|
|
398
401
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-completed");
|
|
399
402
|
recordTerminalReason("completed");
|
|
@@ -409,7 +412,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
409
412
|
kind: "target-unresolved",
|
|
410
413
|
message: phase0Error instanceof Error ? phase0Error.message : String(phase0Error),
|
|
411
414
|
})), { name: "ta-terminal-failed-resolve" });
|
|
412
|
-
await emitProgress(deps.pool, input.assessmentId, "failed");
|
|
415
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "failed");
|
|
413
416
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-failed");
|
|
414
417
|
recordTerminalReason("target-unresolved");
|
|
415
418
|
return {
|
|
@@ -425,7 +428,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
425
428
|
message: schemaViolation.message,
|
|
426
429
|
details: schemaViolation.issues,
|
|
427
430
|
})), { name: "ta-terminal-failed-schema" });
|
|
428
|
-
await emitProgress(deps.pool, input.assessmentId, "failed");
|
|
431
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "failed");
|
|
429
432
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-failed");
|
|
430
433
|
recordTerminalReason("schema-violation");
|
|
431
434
|
return {
|
|
@@ -440,7 +443,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
440
443
|
kind: "derived-invariant-violation",
|
|
441
444
|
message: derivedViolation.message,
|
|
442
445
|
})), { name: "ta-terminal-failed-derived" });
|
|
443
|
-
await emitProgress(deps.pool, input.assessmentId, "failed");
|
|
446
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "failed");
|
|
444
447
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-failed");
|
|
445
448
|
recordTerminalReason("derived-invariant-violation");
|
|
446
449
|
return {
|
|
@@ -459,7 +462,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
459
462
|
name: "ta-terminal-drain-marker",
|
|
460
463
|
}).catch(() => null);
|
|
461
464
|
await DBOS.runStep(async () => unwrapOrThrow(await markAssessmentSuspended(deps.pool, input.assessmentId)), { name: "ta-terminal-suspended" });
|
|
462
|
-
await emitProgress(deps.pool, input.assessmentId, "suspended");
|
|
465
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "suspended");
|
|
463
466
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-canceled");
|
|
464
467
|
const workflowId = DBOS.workflowID;
|
|
465
468
|
if (workflowId) {
|
|
@@ -485,7 +488,7 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
485
488
|
kind: "unexpected-throw",
|
|
486
489
|
message: unexpectedError instanceof Error ? unexpectedError.message : String(unexpectedError),
|
|
487
490
|
})), { name: "ta-terminal-failed-unexpected" });
|
|
488
|
-
await emitProgress(deps.pool, input.assessmentId, "failed");
|
|
491
|
+
await emitProgress(deps.pool, logger, input.assessmentId, "failed");
|
|
489
492
|
await revokeRunAuthorizationSafe(deps, authorization, "target-assessment-failed");
|
|
490
493
|
recordTerminalReason("unexpected-throw");
|
|
491
494
|
return {
|
|
@@ -514,13 +517,14 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
514
517
|
* aborted by a transient revoke failure.
|
|
515
518
|
*/
|
|
516
519
|
async function revokeRunAuthorizationSafe(deps, authorization, reason) {
|
|
520
|
+
const logger = (deps.logger ?? createNoopLogger()).named("execute-target-assessment");
|
|
517
521
|
try {
|
|
518
522
|
await DBOS.runStep(() => deps.runAuthorizer.revoke(authorization, reason), {
|
|
519
523
|
name: `ta-revoke-run-auth:${reason}`,
|
|
520
524
|
});
|
|
521
525
|
}
|
|
522
526
|
catch (err) {
|
|
523
|
-
|
|
527
|
+
logger.warn("revokeRunAuthorization failed", { reason, ...logger.errorFields(err) });
|
|
524
528
|
}
|
|
525
529
|
}
|
|
526
530
|
// Re-export the canonical TA input schema (consumers and the managed root
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* its own self-cancel on 402.
|
|
28
28
|
*/
|
|
29
29
|
import type { Pool } from "pg";
|
|
30
|
+
import type { Logger } from "../lib/logger.js";
|
|
30
31
|
import type { AgentDefinition, LoopMessage } from "../loop/types.js";
|
|
31
32
|
import type { AgentChat, EmbeddingProvider } from "../providers/types.js";
|
|
32
33
|
import { type RunSession } from "../auth/types.js";
|
|
@@ -183,6 +184,14 @@ export interface SandboxAgentBuildContext {
|
|
|
183
184
|
*/
|
|
184
185
|
export interface SandboxStepDeps {
|
|
185
186
|
readonly pool: Pool;
|
|
187
|
+
/**
|
|
188
|
+
* Operational logging seam. Omit it and the body falls back to a no-op —
|
|
189
|
+
* but a failed step's cause is written here and NOWHERE else (`failStep`
|
|
190
|
+
* scrubs the error before it reaches the DB row, the run panel, or the
|
|
191
|
+
* parent's re-raise), so an embedder that wires nothing gives up its only
|
|
192
|
+
* account of why a step died.
|
|
193
|
+
*/
|
|
194
|
+
readonly logger?: Logger;
|
|
186
195
|
/** Non-streaming chat — drives the agent loop + the post-step sub-agents. */
|
|
187
196
|
readonly provider: AgentChat;
|
|
188
197
|
/** Write-side embedder for the post-step vector index. */
|