@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
@@ -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. */
@@ -28,6 +28,7 @@
28
28
  */
29
29
  import { DBOS, Error as DBOSErrors } from "@dbos-inc/dbos-sdk";
30
30
  import { insertStepExecution, updateStepExecution } from "../state/index.js";
31
+ import { createNoopLogger } from "../lib/console-logger.js";
31
32
  import { unwrapOrThrow } from "../lib/result.js";
32
33
  import { isBudgetExceeded } from "../loop/budget-exceeded.js";
33
34
  import { runAgent } from "../loop/run-agent.js";
@@ -101,6 +102,14 @@ export function registerSandboxStep(deps) {
101
102
  * (the DBOS calls inside still rely on a workflow context being present).
102
103
  */
103
104
  async function runSandboxStepBody(input, deps) {
105
+ // Step coordinates bind once — every record below carries them without each
106
+ // call site restating them. REPLAY: a recovered workflow re-executes this body
107
+ // and re-emits its records, so a reader dedups by (runId, stepId), not by count.
108
+ const logger = (deps.logger ?? createNoopLogger()).named("sandbox-step").with({
109
+ runId: input.runId,
110
+ stepId: input.stepId,
111
+ agentId: input.agentId,
112
+ });
104
113
  const childWorkflowId = DBOS.workflowID ?? `${input.runId}-${input.stepId}`;
105
114
  // Checkpointed so the value (and every `durationMs` derived from it) replays
106
115
  // identically — a raw `Date.now()` in the workflow body drifts on recovery.
@@ -206,7 +215,7 @@ async function runSandboxStepBody(input, deps) {
206
215
  await emitToParentStream(part);
207
216
  }
208
217
  catch (err) {
209
- console.warn(`[sandbox-step] emitToParentStream failed (non-fatal):`, err instanceof Error ? err.message : err);
218
+ logger.warn("emitToParentStream failed (non-fatal)", logger.errorFields(err));
210
219
  }
211
220
  };
212
221
  // Schema-conformant step-activity emitter — stable id per (runId, stepId)
@@ -230,10 +239,15 @@ async function runSandboxStepBody(input, deps) {
230
239
  const failStep = async (errorClass, err) => {
231
240
  if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
232
241
  throw err;
233
- await tryTeardown(deps, sandbox);
242
+ await tryTeardown(logger, deps, sandbox);
234
243
  const durationMs = (await DBOS.now()) - startedAt;
235
- const detail = err instanceof Error ? (err.stack ?? err.message) : String(err);
236
- console.error(`[sandbox-step] ${errorClass} failure for ${input.runId}/${input.stepId}: ${detail}`);
244
+ // The scrub below is deliberate and load-bearing, which makes this record
245
+ // the ONLY account of why the step died — the thrown error carries just the
246
+ // generic phrase, so nothing downstream can reconstruct the cause.
247
+ logger.error("step failure", {
248
+ errorClass,
249
+ ...logger.errorFields(err),
250
+ });
237
251
  const safe = userFacingStepFailure(errorClass);
238
252
  await DBOS.runStep(async () => {
239
253
  unwrapOrThrow(await updateStepExecution(deps.pool, input.runId, input.stepId, {
@@ -334,7 +348,9 @@ async function runSandboxStepBody(input, deps) {
334
348
  await DBOS.runStep(() => DBOS.send(input.parentWorkflowId, notification, BUDGET_EXCEEDED_TOPIC), { name: "notify-parent-budget-exceeded" });
335
349
  }
336
350
  catch (sendErr) {
337
- console.warn(`[sandbox-step] notify-parent-budget-exceeded failed (non-fatal): ${sendErr instanceof Error ? sendErr.message : sendErr}`);
351
+ logger.warn("notify-parent-budget-exceeded failed (non-fatal)", {
352
+ ...logger.errorFields(sendErr),
353
+ });
338
354
  }
339
355
  // Self-cancel to CANCELLED so the parent's `getResult` observes the
340
356
  // cancellation and triggers the 402 pause cascade. ERROR is NOT
@@ -369,7 +385,7 @@ async function runSandboxStepBody(input, deps) {
369
385
  // terminal-failure status so the parent's fail-fast cascade fires.
370
386
  const blockerOutcome = blockerHolder.outcome;
371
387
  if (blockerOutcome) {
372
- await tryTeardown(deps, sandbox);
388
+ await tryTeardown(logger, deps, sandbox);
373
389
  const durationMs = (await DBOS.now()) - startedAt;
374
390
  await DBOS.runStep(async () => {
375
391
  unwrapOrThrow(await updateStepExecution(deps.pool, input.runId, input.stepId, {
@@ -413,7 +429,7 @@ async function runSandboxStepBody(input, deps) {
413
429
  lineageCollector,
414
430
  };
415
431
  await emitActivity("generating-metadata", "Describing output files");
416
- const manifest = await safeRunValue(() => walkStepArtifacts({
432
+ const manifest = await safeRunValue(logger, () => walkStepArtifacts({
417
433
  writePrefix,
418
434
  stepId: input.stepId,
419
435
  runId: input.runId,
@@ -423,11 +439,11 @@ async function runSandboxStepBody(input, deps) {
423
439
  // gate (`data-step-summary`, file-tree) stay replay-stable and the billed
424
440
  // LLM calls are not re-issued on recovery. The remaining stages (walk /
425
441
  // reconcile / sync / index) stay inline — a separate follow-up.
426
- const metadataEntries = await safeRunValue(() => DBOS.runStep(() => generateStepFileMetadata(deps, postCtx, manifest), {
442
+ const metadataEntries = await safeRunValue(logger, () => DBOS.runStep(() => generateStepFileMetadata(deps, postCtx, manifest), {
427
443
  name: "post-step.generate-file-metadata",
428
444
  }), "post-step.metadata", []);
429
445
  await emitActivity("generating-summary", "Summarizing step results");
430
- const summary = await safeRunValue(() => DBOS.runStep(() => generateStepSummaryAndWrite(deps, postCtx, manifest), {
446
+ const summary = await safeRunValue(logger, () => DBOS.runStep(() => generateStepSummaryAndWrite(deps, postCtx, manifest), {
431
447
  name: "post-step.generate-step-summary",
432
448
  }), "post-step.summary", undefined);
433
449
  // Lineage attestation (registration + sync) is fail-fast (see the artifact-manifest spec): unlike
@@ -459,10 +475,10 @@ async function runSandboxStepBody(input, deps) {
459
475
  stepOutputs = collectStepOutputs(postCtx, postArtifacts);
460
476
  }
461
477
  catch (err) {
462
- console.warn(`[sandbox-step] post-step.collect failed (non-fatal):`, err instanceof Error ? err.message : err);
478
+ logger.warn("post-step.collect failed (non-fatal)", logger.errorFields(err));
463
479
  }
464
480
  await emitActivity("indexing", "Indexing outputs for search");
465
- await safeRun(() => vectorIndexStepOutputs(deps, postCtx, postArtifacts), "post-step.vector-index");
481
+ await safeRun(logger, () => vectorIndexStepOutputs(deps, postCtx, postArtifacts), "post-step.vector-index");
466
482
  // (9) teardown — destroy sandbox + clear sandbox_ref.
467
483
  await DBOS.runStep(() => deps.sandboxClient.teardown(sandbox), { name: "sandbox.teardown" });
468
484
  // (10) mark-terminal + emit.
@@ -537,14 +553,17 @@ async function runSandboxStepBody(input, deps) {
537
553
  * swallows non-fatal failures — these are reflected in the step summary
538
554
  * but do not fail the step itself (synthesis/sync/index can be retried).
539
555
  */
540
- async function safeRun(fn, label) {
556
+ async function safeRun(logger, fn, label) {
541
557
  try {
542
558
  await fn();
543
559
  }
544
560
  catch (err) {
545
561
  if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
546
562
  throw err;
547
- console.warn(`[sandbox-step] ${label} failed (non-fatal):`, err instanceof Error ? err.message : err);
563
+ // The stage rides as a namespace, not interpolated prose: `label` is what
564
+ // says WHICH post-step stage degraded, and a reader must be able to filter
565
+ // on it without parsing the message.
566
+ logger.named(label).warn("failed (non-fatal)", logger.errorFields(err));
548
567
  }
549
568
  }
550
569
  /**
@@ -552,22 +571,25 @@ async function safeRun(fn, label) {
552
571
  * logs and yields `fallback` so the body can thread a safe default downstream
553
572
  * instead of aborting the post-step pipeline.
554
573
  */
555
- async function safeRunValue(fn, label, fallback) {
574
+ async function safeRunValue(logger, fn, label, fallback) {
556
575
  try {
557
576
  return await fn();
558
577
  }
559
578
  catch (err) {
560
579
  if (err instanceof DBOSErrors.DBOSWorkflowCancelledError)
561
580
  throw err;
562
- console.warn(`[sandbox-step] ${label} failed (non-fatal):`, err instanceof Error ? err.message : err);
581
+ // The stage rides as a namespace, not interpolated prose: `label` is what
582
+ // says WHICH post-step stage degraded, and a reader must be able to filter
583
+ // on it without parsing the message.
584
+ logger.named(label).warn("failed (non-fatal)", logger.errorFields(err));
563
585
  return fallback;
564
586
  }
565
587
  }
566
- async function tryTeardown(deps, sandbox) {
588
+ async function tryTeardown(logger, deps, sandbox) {
567
589
  try {
568
590
  await DBOS.runStep(() => deps.sandboxClient.teardown(sandbox), { name: "sandbox.teardown" });
569
591
  }
570
592
  catch (err) {
571
- console.warn(`[sandbox-step] teardown failed (non-fatal):`, err instanceof Error ? err.message : err);
593
+ logger.warn("teardown failed (non-fatal)", logger.errorFields(err));
572
594
  }
573
595
  }
@@ -23,6 +23,7 @@
23
23
  * the caller bumps the attempt counter so the resumed call lands a fresh
24
24
  * DBOS cache slot rather than replaying the cancelled prior attempt.
25
25
  */
26
+ import type { Logger } from "../../../lib/logger.js";
26
27
  import type { AgentChat, ChatRequest, ChatResponse } from "../../../providers/types.js";
27
28
  import type { AgentSession } from "../../../auth/types.js";
28
29
  /**
@@ -38,6 +39,8 @@ export interface BudgetExceededMarker {
38
39
  readonly error: string;
39
40
  }
40
41
  export interface RunLlmStepOptions {
42
+ /** Operational logging seam; omitted falls back to no-op. */
43
+ readonly logger?: Logger;
41
44
  /** Durable-step name — already attempt-numbered by the caller. */
42
45
  readonly stepName: string;
43
46
  /** Agent id stamped in the marker for telemetry. */
@@ -24,6 +24,7 @@
24
24
  * DBOS cache slot rather than replaying the cancelled prior attempt.
25
25
  */
26
26
  import { DBOS } from "@dbos-inc/dbos-sdk";
27
+ import { createNoopLogger } from "../../../lib/console-logger.js";
27
28
  import { unwrapOrThrow } from "../../../lib/result.js";
28
29
  import { isBudgetExceeded } from "../../../loop/budget-exceeded.js";
29
30
  /**
@@ -82,7 +83,8 @@ export async function runLlmStep(opts) {
82
83
  await DBOS.runStep(() => DBOS.send(workflowId, marker, BUDGET_EXCEEDED_TOPIC), { name: `${stepName}:notify-budget-exceeded` });
83
84
  }
84
85
  catch (sendErr) {
85
- console.warn(`[ta-llm-step] notify-budget-exceeded send failed (non-fatal): ${sendErr instanceof Error ? sendErr.message : sendErr}`);
86
+ const logger = (opts.logger ?? createNoopLogger()).named("ta-llm-step");
87
+ logger.warn("notify-budget-exceeded send failed (non-fatal)", { stepName, ...logger.errorFields(sendErr) });
86
88
  }
87
89
  return { kind: "budget-exceeded", sentinel: BUDGET_EXCEEDED_SENTINEL };
88
90
  }
@@ -18,9 +18,12 @@
18
18
  */
19
19
  import { z } from "zod";
20
20
  import type { AgentSession } from "../../../auth/types.js";
21
+ import type { Logger } from "../../../lib/logger.js";
21
22
  import type { AgentChat } from "../../../providers/types.js";
22
23
  import { BUDGET_EXCEEDED_SENTINEL } from "./llm-step.js";
23
24
  export interface StructuredLlmCallOptions<TSchema extends z.ZodType> {
25
+ /** Operational logging seam, forwarded to `runLlmStep`; omitted falls back to no-op. */
26
+ readonly logger?: Logger;
24
27
  /** Attempt-numbered durable-step name (e.g. `"ta-decision:modulator-triage:0"`). */
25
28
  readonly stepName: string;
26
29
  /** Agent id stamped on telemetry and billing-gateway provenance. */
@@ -78,6 +78,7 @@ export async function structuredLlmCall(opts) {
78
78
  toolChoice: { type: "tool", toolName: SUBMIT_TOOL_NAME },
79
79
  };
80
80
  const result = await runLlmStep({
81
+ ...(opts.logger ? { logger: opts.logger } : {}),
81
82
  stepName: opts.stepName,
82
83
  agentId: opts.agentId,
83
84
  provider: opts.provider,
@@ -15,6 +15,7 @@
15
15
  * in §17 if a harness-native audit pipeline is wired.
16
16
  */
17
17
  import { z } from "zod";
18
+ import type { Logger } from "../../lib/logger.js";
18
19
  import { type DossierV4Body } from "@inflexa-ai/harness/contracts/target-dossier.js";
19
20
  import type { Phase2Bundle } from "./steps/phase2-aggregate.js";
20
21
  import type { DossierRecommendationStepOutput, LiabilityBulletsStepOutput, SafetyFlagsTrailStepOutput, TranslationalCommentaryStepOutput } from "./synthesis/index.js";
@@ -27,6 +28,8 @@ export declare class DossierDerivedInvariantError extends Error {
27
28
  constructor(message: string, cause?: unknown | undefined);
28
29
  }
29
30
  export interface Phase5PersistInput {
31
+ /** Operational logging seam; omitted falls back to no-op. */
32
+ readonly logger?: Logger;
30
33
  readonly assessmentId: string;
31
34
  readonly phase4Dossier: DossierV4Body;
32
35
  readonly phase2: Phase2Bundle;
@@ -15,6 +15,7 @@
15
15
  * in §17 if a harness-native audit pipeline is wired.
16
16
  */
17
17
  import { z } from "zod";
18
+ import { createNoopLogger } from "../../lib/console-logger.js";
18
19
  import { DossierV5Schema, } from "@inflexa-ai/harness/contracts/target-dossier.js";
19
20
  import { getDrugPrimaryTargetUniprots } from "../../tools/lib/chembl-client.js";
20
21
  import { deterministicTranslationalCommentary } from "./assemblers/index.js";
@@ -84,7 +85,7 @@ function pickCommentarySeverity(row) {
84
85
  }
85
86
  return "ok";
86
87
  }
87
- function stampSynthesis(phase4Dossier, syn, phase2) {
88
+ function stampSynthesis(logger, phase4Dossier, syn, phase2) {
88
89
  const next = structuredClone(phase4Dossier);
89
90
  if (syn.bullets.coverage === "available") {
90
91
  next.liability_summary.liability_bullets = syn.bullets.data.bullets.map((b) => ({ text: b.text, rationale: b.rationale, category: b.category }));
@@ -147,7 +148,10 @@ function stampSynthesis(phase4Dossier, syn, phase2) {
147
148
  try {
148
149
  const audit = validateRecommendationCitations(syn.recommendation.data, next);
149
150
  if (audit.citations_unresolved.length > 0) {
150
- console.warn(`[phase5-persist] recommendation_audit: ${audit.citations_unresolved.length} unresolved citation(s)`);
151
+ logger.warn("recommendation_audit: unresolved citation(s)", {
152
+ unresolvedCount: audit.citations_unresolved.length,
153
+ unresolved: audit.citations_unresolved,
154
+ });
151
155
  }
152
156
  next.analytics.recommendation_audit = {
153
157
  coverage: "available",
@@ -388,12 +392,13 @@ async function upgradeDossierToV5(body, phase2) {
388
392
  * rejects the final dossier.
389
393
  */
390
394
  export async function phase5Persist(input) {
395
+ const logger = (input.logger ?? createNoopLogger()).named("phase5-persist").with({ assessmentId: input.assessmentId });
391
396
  // The dossier rides through this function as an untyped working blob; the
392
397
  // `as unknown as DossierV{4,5}Body` casts below are version-shim views that
393
398
  // let the typed helpers operate on it. None of these casts is load-bearing
394
399
  // for soundness: `DossierV5Schema.safeParse(fullDossier)` at the end is the
395
400
  // single validation gate, and it throws before anything is returned.
396
- let dossier = stampSynthesis(input.phase4Dossier, input.synthesis, input.phase2);
401
+ let dossier = stampSynthesis(logger, input.phase4Dossier, input.synthesis, input.phase2);
397
402
  // Demote organ-claim flagged bullets into coverage_qualifier.unverified_bullets.
398
403
  const exec = dossier.executive_recommendation;
399
404
  if (exec?.coverage === "available") {
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import type { Pool } from "pg";
14
14
  import type { TargetAssessmentPhase, TargetAssessmentProgressEvent } from "@inflexa-ai/harness/contracts/target-dossier.js";
15
+ import type { Logger } from "../../lib/logger.js";
15
16
  /** DBOS stream key the SSE route reads via `DBOS.readStream(wfId, "progress")`. */
16
17
  export declare const TA_PROGRESS_STREAM_KEY = "progress";
17
18
  export type ProgressPhase = TargetAssessmentPhase;
@@ -35,4 +36,4 @@ export declare const PROGRESS_MESSAGES: Record<ProgressPhase, string>;
35
36
  * Best-effort: a DB or stream failure logs and continues — progress
36
37
  * emission MUST NOT block the workflow.
37
38
  */
38
- export declare function emitProgress(pool: Pool, assessmentId: string, phase: ProgressPhase): Promise<void>;
39
+ export declare function emitProgress(pool: Pool, logger: Logger, assessmentId: string, phase: ProgressPhase): Promise<void>;
@@ -46,7 +46,8 @@ export const PROGRESS_MESSAGES = {
46
46
  * Best-effort: a DB or stream failure logs and continues — progress
47
47
  * emission MUST NOT block the workflow.
48
48
  */
49
- export async function emitProgress(pool, assessmentId, phase) {
49
+ export async function emitProgress(pool, logger, assessmentId, phase) {
50
+ const log = logger.named("ta-progress").with({ assessmentId, phase });
50
51
  const message = PROGRESS_MESSAGES[phase];
51
52
  const percent = PROGRESS_PERCENTS[phase];
52
53
  await DBOS.runStep(async () => {
@@ -58,7 +59,7 @@ export async function emitProgress(pool, assessmentId, phase) {
58
59
  await DBOS.writeStream(TA_PROGRESS_STREAM_KEY, part);
59
60
  }
60
61
  catch (err) {
61
- console.warn(`[ta-progress] writeStream failed for ${assessmentId} (${phase}): ${err instanceof Error ? err.message : err}`);
62
+ log.warn("writeStream failed", log.errorFields(err));
62
63
  }
63
64
  // The "suspended" phase is owned by the terminal handler
64
65
  // (`markAssessmentSuspended` already set status + progress text). A
@@ -69,7 +70,7 @@ export async function emitProgress(pool, assessmentId, phase) {
69
70
  // emission MUST NOT block the workflow. `updateProgress` returns
70
71
  // its DB failure as an `Err`, so handle both branches here.
71
72
  (await updateProgress(pool, assessmentId, message, rowStatus)).match(() => { }, (error) => {
72
- console.warn(`[ta-progress] updateProgress failed for ${assessmentId} (${phase}): ${describeDbError(error)}`);
73
+ log.warn("updateProgress failed", { err: describeDbError(error) });
73
74
  });
74
75
  }
75
76
  }, { name: `ta-progress:${phase}` });