@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.
Files changed (80) hide show
  1. package/README.md +48 -0
  2. package/dist/agents/conversation-agent.d.ts +3 -0
  3. package/dist/agents/conversation-agent.js +2 -2
  4. package/dist/agents/sandbox/shared.d.ts +3 -0
  5. package/dist/agents/sandbox/shared.js +2 -2
  6. package/dist/app/chat-turn.d.ts +3 -0
  7. package/dist/app/chat-turn.js +3 -1
  8. package/dist/app/synthesize-run.d.ts +3 -0
  9. package/dist/app/synthesize-run.js +3 -1
  10. package/dist/execution/artifact-metadata.d.ts +3 -0
  11. package/dist/execution/artifact-metadata.js +8 -2
  12. package/dist/execution/artifact-registration.d.ts +2 -1
  13. package/dist/execution/artifact-registration.js +8 -2
  14. package/dist/execution/ephemeral-runner.d.ts +3 -0
  15. package/dist/execution/ephemeral-runner.js +8 -4
  16. package/dist/execution/post-step-pipeline.d.ts +3 -0
  17. package/dist/execution/post-step-pipeline.js +24 -4
  18. package/dist/execution/reconcile-manifest.d.ts +7 -0
  19. package/dist/execution/reconcile-manifest.js +19 -5
  20. package/dist/execution/report-runner.d.ts +3 -0
  21. package/dist/execution/report-runner.js +4 -4
  22. package/dist/execution/step-summary.d.ts +3 -0
  23. package/dist/execution/step-summary.js +4 -2
  24. package/dist/index.d.ts +3 -0
  25. package/dist/index.js +5 -0
  26. package/dist/lib/chrome.d.ts +4 -1
  27. package/dist/lib/chrome.js +10 -7
  28. package/dist/lib/console-logger.d.ts +19 -0
  29. package/dist/lib/console-logger.js +62 -0
  30. package/dist/lib/logger.d.ts +88 -0
  31. package/dist/lib/logger.js +37 -0
  32. package/dist/lib/otel.d.ts +2 -1
  33. package/dist/lib/otel.js +8 -3
  34. package/dist/runtime/boot.d.ts +2 -2
  35. package/dist/runtime/boot.js +1 -1
  36. package/dist/runtime/connection-budget.d.ts +2 -2
  37. package/dist/runtime/connection-budget.js +5 -3
  38. package/dist/runtime/dbos.d.ts +7 -7
  39. package/dist/runtime/dbos.js +14 -11
  40. package/dist/runtime/shutdown.d.ts +2 -2
  41. package/dist/runtime/shutdown.js +4 -3
  42. package/dist/sandbox/create-sandbox.d.ts +2 -2
  43. package/dist/sandbox/docker-client.d.ts +2 -2
  44. package/dist/sandbox/docker-client.js +4 -1
  45. package/dist/sandbox/k8s-client.d.ts +3 -0
  46. package/dist/sandbox/k8s-client.js +5 -4
  47. package/dist/sandbox/notification-sweep.d.ts +3 -3
  48. package/dist/sandbox/notification-sweep.js +4 -2
  49. package/dist/sandbox/reaper.d.ts +3 -3
  50. package/dist/sandbox/reaper.js +7 -4
  51. package/dist/sandbox/watchdog.d.ts +3 -3
  52. package/dist/sandbox/watchdog.js +8 -2
  53. package/dist/state/init.d.ts +2 -1
  54. package/dist/state/init.js +5 -6
  55. package/dist/tasks/data-profile.d.ts +5 -0
  56. package/dist/tasks/data-profile.js +24 -17
  57. package/dist/tools/bio/search-dgidb.d.ts +11 -1
  58. package/dist/tools/bio/search-dgidb.js +92 -78
  59. package/dist/tools/iterate-report.d.ts +3 -0
  60. package/dist/tools/iterate-report.js +3 -1
  61. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  62. package/dist/tools/research/literature-reviewer.js +2 -2
  63. package/dist/tools/workspace/execute-command.d.ts +3 -0
  64. package/dist/tools/workspace/execute-command.js +3 -1
  65. package/dist/workflows/execute-analysis.d.ts +3 -0
  66. package/dist/workflows/execute-analysis.js +11 -7
  67. package/dist/workflows/execute-target-assessment.d.ts +3 -0
  68. package/dist/workflows/execute-target-assessment.js +19 -15
  69. package/dist/workflows/sandbox-step.d.ts +9 -0
  70. package/dist/workflows/sandbox-step.js +39 -17
  71. package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
  72. package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
  73. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
  74. package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
  75. package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
  76. package/dist/workflows/target-assessment/phase5-persist.js +8 -3
  77. package/dist/workflows/target-assessment/progress.d.ts +2 -1
  78. package/dist/workflows/target-assessment/progress.js +4 -3
  79. package/dist/workspace/filesystem.js +163 -74
  80. 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
- export const searchDgidbTool = defineTool({
119
- id: "search_dgidb",
120
- description: "Query the Drug-Gene Interaction Database (DGIdb) for drug-gene interactions. " +
121
- "DGIdb aggregates 30+ sources (ChEMBL, DrugBank, TTD, PharmGKB, GuideToPharmacology, CIViC, etc.) " +
122
- "and returns source counts as a confidence signal. " +
123
- "searchType='gene' returns drugs interacting with input HUGO gene symbols (most common: 'what drugs hit my gene set?'). " +
124
- "searchType='drug' returns the genes each input drug is known to act on. " +
125
- "Free, no API key required. Sorted by source count desc.",
126
- inputSchema: z.object({
127
- query: z
128
- .union([z.string(), z.array(z.string()).max(50)])
129
- .describe("Single identifier or list of up to 50. For searchType='gene': HUGO symbols. For searchType='drug': drug names or DGIdb concept IDs."),
130
- searchType: z.enum(["gene", "drug"]).default("gene"),
131
- interactionTypes: z.array(z.string()).optional().describe("Case-insensitive substring filter on interaction type. e.g. ['inhibitor', 'antagonist']"),
132
- sources: z.array(z.string()).optional().describe("Restrict to interactions from these source DBs (case-insensitive substring)."),
133
- minSources: z.number().int().min(1).default(1).describe("Drop interactions supported by fewer than this many sources."),
134
- limit: z.number().int().min(1).max(200).default(50).describe("Max interactions per input gene/drug after filtering."),
135
- }),
136
- execute: async ({ query, searchType = "gene", interactionTypes, sources, minSources = 1, limit = 50 }) => {
137
- const inputs = typeof query === "string" ? [query] : query;
138
- if (inputs.length === 0 || inputs.every((s) => !s.trim())) {
139
- throw new Error("query must contain at least one identifier");
140
- }
141
- const isGene = searchType === "gene";
142
- const variableName = isGene ? "genes" : "drugs";
143
- const queryString = isGene ? GENE_INTERACTIONS_QUERY : DRUG_INTERACTIONS_QUERY;
144
- const res = await apiFetchValidated(DGIDB_GRAPHQL_URL, DgidbResponseSchema, {
145
- method: "POST",
146
- headers: { "Content-Type": "application/json" },
147
- body: JSON.stringify({
148
- query: queryString,
149
- variables: { [variableName]: inputs },
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 inputSide = isGene
174
- ? { geneName: node.name }
175
- : {
176
- drugName: node.name,
177
- drugConceptId: node.conceptId ?? undefined,
178
- };
179
- const mapped = (node.interactions ?? []).map((raw) => mapInteraction(raw, searchType, inputSide));
180
- const filtered = applyFilters(mapped, {
181
- interactionTypes,
182
- sources,
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
- const sorted = filtered.sort((a, b) => {
186
- if (b.sourceCount !== a.sourceCount) {
187
- return b.sourceCount - a.sourceCount;
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
- return (b.interactionScore ?? 0) - (a.interactionScore ?? 0);
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
- input,
193
- found: true,
194
- interactions: sorted.slice(0, limit),
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
- console.warn(`[submit-report] failed to persist preview meta: ${err instanceof Error ? err.message : err}`);
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 { searchDgidbTool } from "../bio/search-dgidb.js";
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
- searchDgidbTool,
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
- console.warn(`[execute_command] provenance frame handling failed (non-fatal) for ${execId}: ${err instanceof Error ? err.message : err}`);
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
- console.error(`[executeAnalysis] emitProvenance threw for run ${event.runId} (event=${event.type}):`, err);
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
- console.error(`[executeAnalysis] synthesizeFindings failed for run ${runId} — failing the run:`, err instanceof Error ? err.message : err);
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
- console.error(`[executeAnalysis] persist-final-status failed for run ${runId} (status=${status}):`, err);
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
- console.error(`[executeAnalysis] sweep-pending-steps failed for run ${runId}:`, err);
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
- console.error(`[executeAnalysis] suspend-analysis failed for run ${runId}:`, err);
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
- console.error(`[executeAnalysis] closeRunningCharge failed for run ${runId} (reason=${chargeReason}):`, err);
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
- console.error(`[executeAnalysis] revokeRunAuthorization failed for run ${runId} (reason=${revokeReason}):`, err);
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
- console.warn(`[ta-approval-precedents] openFDA lookup failed for "${indication}": ${err instanceof Error ? err.message : err}`);
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
- console.error(`[ta-terminal] setDossier failed for ${input.assessmentId}: ${err instanceof Error ? err.message : err}`);
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
- console.warn(`[execute-target-assessment] revokeRunAuthorization (${reason}) failed:`, err instanceof Error ? err.message : err);
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. */