@inflexa-ai/harness 0.2.0 → 0.4.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 (81) 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 +41 -3
  43. package/dist/sandbox/create-sandbox.js +39 -19
  44. package/dist/sandbox/docker-client.d.ts +17 -2
  45. package/dist/sandbox/docker-client.js +39 -12
  46. package/dist/sandbox/k8s-client.d.ts +3 -0
  47. package/dist/sandbox/k8s-client.js +5 -4
  48. package/dist/sandbox/notification-sweep.d.ts +3 -3
  49. package/dist/sandbox/notification-sweep.js +4 -2
  50. package/dist/sandbox/reaper.d.ts +3 -3
  51. package/dist/sandbox/reaper.js +7 -4
  52. package/dist/sandbox/watchdog.d.ts +3 -3
  53. package/dist/sandbox/watchdog.js +8 -2
  54. package/dist/state/init.d.ts +2 -1
  55. package/dist/state/init.js +5 -6
  56. package/dist/tasks/data-profile.d.ts +5 -0
  57. package/dist/tasks/data-profile.js +24 -17
  58. package/dist/tools/bio/search-dgidb.d.ts +11 -1
  59. package/dist/tools/bio/search-dgidb.js +92 -78
  60. package/dist/tools/iterate-report.d.ts +3 -0
  61. package/dist/tools/iterate-report.js +3 -1
  62. package/dist/tools/research/literature-reviewer.d.ts +3 -0
  63. package/dist/tools/research/literature-reviewer.js +2 -2
  64. package/dist/tools/workspace/execute-command.d.ts +3 -0
  65. package/dist/tools/workspace/execute-command.js +3 -1
  66. package/dist/workflows/execute-analysis.d.ts +3 -0
  67. package/dist/workflows/execute-analysis.js +11 -7
  68. package/dist/workflows/execute-target-assessment.d.ts +3 -0
  69. package/dist/workflows/execute-target-assessment.js +19 -15
  70. package/dist/workflows/sandbox-step.d.ts +9 -0
  71. package/dist/workflows/sandbox-step.js +39 -17
  72. package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
  73. package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
  74. package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
  75. package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
  76. package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
  77. package/dist/workflows/target-assessment/phase5-persist.js +8 -3
  78. package/dist/workflows/target-assessment/progress.d.ts +2 -1
  79. package/dist/workflows/target-assessment/progress.js +4 -3
  80. package/dist/workspace/filesystem.js +163 -74
  81. package/package.json +95 -96
@@ -23,6 +23,7 @@ import { forSubAgent } from "../auth/types.js";
23
23
  import { createDataProfilerAgent } from "../agents/sandbox/data-profiler.js";
24
24
  import { runToTerminal } from "../loop/run-to-terminal.js";
25
25
  import { durableStep } from "../loop/run-step.js";
26
+ import { createNoopLogger } from "../lib/console-logger.js";
26
27
  import { unwrapOrThrow } from "../lib/result.js";
27
28
  import { defineTool } from "../tools/define-tool.js";
28
29
  import { renderWorkspace } from "../prompts/briefing.js";
@@ -128,6 +129,7 @@ export function registerDataProfileWorkflow(deps) {
128
129
  * 'failed' on error, and revokes the run authorization on every terminal path.
129
130
  */
130
131
  export async function runDataProfileBody(input, deps) {
132
+ const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId: input.analysisId });
131
133
  // Ownership defaults to true for inputs persisted before the field existed
132
134
  // (a #247 workflow recovered across this deploy): those were always
133
135
  // Cortex-owned and must be revoked here.
@@ -138,9 +140,9 @@ export async function runDataProfileBody(input, deps) {
138
140
  // embedder populated the tree and handed us this manifest in the workflow
139
141
  // input. The body never downloads.
140
142
  if (stagedInputs.length === 0) {
141
- console.warn(`[data-profile] No input files staged for ${analysisId}`);
143
+ logger.warn("no input files staged");
142
144
  if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId)))
143
- logTerminalNoop(analysisId, "completion");
145
+ logTerminalNoop(logger, analysisId, "completion");
144
146
  await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
145
147
  return;
146
148
  }
@@ -190,7 +192,7 @@ export async function runDataProfileBody(input, deps) {
190
192
  ``,
191
193
  `All file paths in your response must be relative to the analysis root and must EXACTLY match the paths listed above.`,
192
194
  ].join("\n");
193
- console.log(`[data-profile] execution=${executionId} analysis=${analysisId} starting sandbox`);
195
+ logger.info("starting sandbox", { executionId });
194
196
  const sandbox = await deps.sandboxClient.createSandbox({
195
197
  runId: DATA_PROFILE_RUN_LITERAL,
196
198
  stepId: DATA_PROFILE_STEP_LITERAL,
@@ -310,13 +312,16 @@ export async function runDataProfileBody(input, deps) {
310
312
  indexed++;
311
313
  }
312
314
  if (fallbackCount > 0) {
313
- console.warn(`[data-profile] ${fallbackCount}/${stagedInputs.length} input file(s) used a deterministic fallback description (analysis=${analysisId}) — profiler omitted them`);
315
+ logger.warn("input file(s) used a deterministic fallback description — profiler omitted them", {
316
+ fallbackCount,
317
+ stagedCount: stagedInputs.length,
318
+ });
314
319
  }
315
- console.log(`[data-profile] Indexed ${indexed} file(s) for ${analysisId}`);
320
+ logger.info("indexed file(s)", { indexed });
316
321
  // 5. Complete — store the FULL profiler finding plus the input snapshot for
317
322
  // staleness detection. The scratch tree is gone; this row is all that survives.
318
323
  if (!unwrapOrThrow(await completeDataProfile(deps.pool, analysisId, buildDataProfileResult(profilerData, stagedInputs, new Date().toISOString())))) {
319
- logTerminalNoop(analysisId, "completion");
324
+ logTerminalNoop(logger, analysisId, "completion");
320
325
  }
321
326
  await deps.runAuthorizer.revoke(authorization, "data-profile-completed");
322
327
  }
@@ -325,14 +330,14 @@ export async function runDataProfileBody(input, deps) {
325
330
  await deps.sandboxClient.teardown(sandbox);
326
331
  }
327
332
  catch (teardownErr) {
328
- console.warn(`[data-profile] execution=${executionId} teardown failed (non-fatal):`, teardownErr);
333
+ logger.warn("teardown failed (non-fatal)", { executionId, ...logger.errorFields(teardownErr) });
329
334
  }
330
335
  }
331
336
  }
332
337
  catch (err) {
333
- console.error(`[data-profile] Failed for ${analysisId}:`, err);
338
+ logger.error("profile failed", logger.errorFields(err));
334
339
  if (!unwrapOrThrow(await failDataProfile(deps.pool, analysisId, profileFailureReason(err))))
335
- logTerminalNoop(analysisId, "failure");
340
+ logTerminalNoop(logger, analysisId, "failure");
336
341
  await deps.runAuthorizer.revoke(authorization, "data-profile-failed");
337
342
  }
338
343
  }
@@ -342,8 +347,8 @@ export async function runDataProfileBody(input, deps) {
342
347
  * there is no `running` row to stamp. Not an error — the ledger correctly moved
343
348
  * on; the workflow still revokes its own authorization on the same terminal path.
344
349
  */
345
- function logTerminalNoop(analysisId, write) {
346
- console.warn(`[data-profile] ${write} write skipped for ${analysisId}: ledger row not running (cleared or expired concurrently)`);
350
+ function logTerminalNoop(logger, analysisId, write) {
351
+ logger.warn("terminal write skipped: ledger row not running (cleared or expired concurrently)", { analysisId, write });
347
352
  }
348
353
  /**
349
354
  * Resolve the `RunSession` the workflow runs under. Whether the authorization
@@ -409,6 +414,7 @@ async function startDataProfileWorkflow(deps, params) {
409
414
  * Returns what happened, so the caller can surface it (e.g. in the seed response).
410
415
  */
411
416
  export async function triggerDataProfile(deps, params) {
417
+ const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId: params.analysisId });
412
418
  const { analysisId } = params;
413
419
  try {
414
420
  // ADVISORY pre-check, not the enforcement — the claim CAS carries the same seed
@@ -420,7 +426,7 @@ export async function triggerDataProfile(deps, params) {
420
426
  // a NULL-seed row (`loadDataProfileStatus` would hide the seed of a cleared row).
421
427
  const seeded = unwrapOrThrow(await loadSeedInputFileIds(deps.pool, analysisId));
422
428
  if (seeded === null || seeded.length === 0) {
423
- console.error(`[data-profile] Trigger rejected for ${analysisId}: no seeded input set (missing analysis row or caller skipped seeding)`);
429
+ logger.error("trigger rejected: no seeded input set (missing analysis row or caller skipped seeding)");
424
430
  return "failed";
425
431
  }
426
432
  // The trigger validates the ledger seed above; it must also validate the manifest
@@ -429,7 +435,7 @@ export async function triggerDataProfile(deps, params) {
429
435
  // NULL result while the seed still names files, an incoherence a staleness policy
430
436
  // then loops on re-triggering. Refuse the divergence before any claim.
431
437
  if (params.stagedInputs.length === 0) {
432
- console.error(`[data-profile] Trigger rejected for ${analysisId}: empty manifest dispatched against a seed naming ${seeded.length} file(s)`);
438
+ logger.error("trigger rejected: empty manifest dispatched against a non-empty seed", { seededCount: seeded.length });
433
439
  return "failed";
434
440
  }
435
441
  const started = unwrapOrThrow(await tryStartDataProfile(deps.pool, analysisId));
@@ -448,7 +454,7 @@ export async function triggerDataProfile(deps, params) {
448
454
  return "failed";
449
455
  }
450
456
  catch (err) {
451
- console.error(`[data-profile] Trigger error for ${analysisId}:`, err);
457
+ logger.error("trigger error", logger.errorFields(err));
452
458
  return "failed";
453
459
  }
454
460
  }
@@ -462,17 +468,18 @@ export async function triggerDataProfile(deps, params) {
462
468
  * there is no further channel to report it on.
463
469
  */
464
470
  async function compensateStartFailure(deps, analysisId, phase, err) {
465
- console.error(`[data-profile] ${phase} error for ${analysisId}:`, err);
471
+ const logger = (deps.logger ?? createNoopLogger()).named("data-profile").with({ analysisId });
472
+ logger.error("start failed", { phase, ...logger.errorFields(err) });
466
473
  const failed = await failDataProfile(deps.pool, analysisId, profileFailureReason(err));
467
474
  if (failed.isErr()) {
468
- console.error(`[data-profile] Failed to mark ${analysisId} failed after a start error:`, failed.error);
475
+ logger.error("failed to mark failed after a start error", { phase, err: failed.error });
469
476
  }
470
477
  else if (!failed.value) {
471
478
  // The row this compensation was written to fail is no longer `running` — a
472
479
  // concurrent clear/expire already moved it on, so the running-CAS refused the
473
480
  // stamp. Nothing wedged at `running`, which is the outcome compensation exists
474
481
  // to guarantee; just record the no-op.
475
- logTerminalNoop(analysisId, "compensation");
482
+ logTerminalNoop(logger, analysisId, "compensation");
476
483
  }
477
484
  }
478
485
  /**
@@ -8,6 +8,7 @@
8
8
  * - searchType="gene": input HUGO symbols, returns drugs interacting with each.
9
9
  * - searchType="drug": input drug names/concept IDs, returns genes each acts on.
10
10
  */
11
+ import type { Logger } from "../../lib/logger.js";
11
12
  interface Interaction {
12
13
  geneName: string;
13
14
  drugName: string;
@@ -26,7 +27,16 @@ interface Interaction {
26
27
  value: string;
27
28
  }[];
28
29
  }
29
- export declare const searchDgidbTool: import("../define-tool.js").Tool<{
30
+ /**
31
+ * A factory rather than a bare const (unlike its bio-leaf siblings) because it
32
+ * logs: DGIdb answers GraphQL partial errors alongside usable data, and an
33
+ * operator debugging odd interaction results needs to see them. `ToolContext`
34
+ * carries no deps by design, so a tool that logs is dep-bearing — the same
35
+ * reason `createSearchClinvarTool` takes its key that way.
36
+ */
37
+ export declare function createSearchDgidbTool(deps?: {
38
+ readonly logger?: Logger;
39
+ }): import("../define-tool.js").Tool<{
30
40
  query: string | string[];
31
41
  searchType: "drug" | "gene";
32
42
  minSources: number;
@@ -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;