@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
package/README.md CHANGED
@@ -37,6 +37,9 @@ import {
37
37
  createNoopArtifactRegistry, // ArtifactRegistry seam
38
38
  UnavailablePreviewPublisher, // PreviewPublisher seam
39
39
  createDbosRunLauncher, // shared RunLauncher
40
+ createConsoleLogger, // Logger — writes to console
41
+ createNoopLogger, // Logger — discards (the fallback when none is wired)
42
+ defaultErrorFields, // the shipped `Logger.errorFields` mapping
40
43
  defineTool,
41
44
  runAgent,
42
45
  makeLocalAuth,
@@ -45,6 +48,51 @@ import {
45
48
 
46
49
  `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **five capability seams** an embedder wires — `RunAuthorizer`, `ResolveBilling`, `ArtifactRegistry`, `RunCharge`, `PreviewPublisher` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all five ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface.
47
50
 
51
+ ## Logging
52
+
53
+ The harness names no logging library. It logs through a `Logger` you supply, so the destination, format, and verbosity stay yours:
54
+
55
+ ```ts
56
+ export interface Logger {
57
+ debug(msg: string, fields?: LogFields): void;
58
+ info(msg: string, fields?: LogFields): void;
59
+ warn(msg: string, fields?: LogFields): void;
60
+ error(msg: string, fields?: LogFields): void;
61
+ with(fields: LogFields): Logger; // bind context onto every record (slog's `With`)
62
+ named(name: string): Logger; // bind a namespace, rendered as a `[a.b]` message prefix
63
+ errorFields(err: unknown): LogFields; // normalize a thrown value; `defaultErrorFields` is the shipped mapping
64
+ }
65
+ ```
66
+
67
+ Pass `createConsoleLogger()` if you have no logging infrastructure. Wire nothing and the harness falls back to `createNoopLogger()` — it never falls back to `console`, because a host whose UI owns stdout (an alternate-screen TUI, say) discards console output, and a diagnostic written to a discarded stream is worse than no diagnostic at all.
68
+
69
+ `errorFields` is on the interface, not a helper we own, so your realization can defer to your sink's native error handling rather than a shape we imposed.
70
+
71
+ **Migrating from a version that took a `pino.Logger`** — `bootHarness` and the deps bags now take this interface. pino is object-first where this is message-first, so pass an adapter:
72
+
73
+ ```ts
74
+ import type { LogFields, Logger } from "@inflexa-ai/harness";
75
+ import type pino from "pino";
76
+
77
+ function pinoAsHarnessLogger(p: pino.Logger, names: readonly string[] = []): Logger {
78
+ const prefixed = (msg: string): string => (names.length > 0 ? `[${names.join(".")}] ${msg}` : msg);
79
+ const emit =
80
+ (level: "debug" | "info" | "warn" | "error") =>
81
+ (msg: string, fields?: LogFields): void => {
82
+ p[level](fields ?? {}, prefixed(msg));
83
+ };
84
+ return {
85
+ debug: emit("debug"),
86
+ info: emit("info"),
87
+ warn: emit("warn"),
88
+ error: emit("error"),
89
+ with: (fields) => pinoAsHarnessLogger(p.child(fields), names),
90
+ named: (name) => pinoAsHarnessLogger(p, [...names, name]),
91
+ errorFields: (err) => ({ err }), // let pino's own `err` serializer render it
92
+ };
93
+ }
94
+ ```
95
+
48
96
  ## How it executes
49
97
 
50
98
  Chat is a plain in-process turn (`prepareChatTurn` → `runAgent` → `appendTurn`); the harness ships no HTTP layer, so the host owns the route. Compute-heavy work runs as **durable workflows** — `executeAnalysis` starts a child workflow per plan step once its dependencies have completed, and each step drives a sandbox agent inside a container. The same `runAgent` primitive runs in both modes; durability and the event sink are injected, so the loop body is identical.
@@ -41,10 +41,13 @@ import type { RunAuthorizer } from "../execution/run-authorizer.js";
41
41
  import type { RunLauncher } from "../execution/run-launcher.js";
42
42
  import { type SubmitReportDeps } from "../tools/iterate-report.js";
43
43
  import type { EphemeralWorkflowInput, EphemeralResult } from "../execution/ephemeral-runner.js";
44
+ import type { Logger } from "../lib/logger.js";
44
45
  /** Canonical agent id — the single source of truth. */
45
46
  export declare const CONVERSATION_AGENT_ID: "conversation-agent";
46
47
  /** The shared dependencies the composition root explodes apart. */
47
48
  export interface ConversationAgentDeps {
49
+ /** Operational logging seam; omitted falls back to no-op. */
50
+ readonly logger?: Logger;
48
51
  /** The LLM seam every loop-driving tool runs its sub-agent on. */
49
52
  readonly provider: ChatProvider;
50
53
  /** Postgres pool — plan persistence, run inspection, workspace index, working memory. */
@@ -32,7 +32,7 @@ import { createWorkingMemory } from "../memory/working-memory.js";
32
32
  import { conversationPrompt } from "../prompts/conversation.js";
33
33
  import { composeSystemPrompt } from "./system-prompt.js";
34
34
  // Bio-lookup leaf tools (pure — no dependencies).
35
- import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, searchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
35
+ import { searchGeneTool, searchPathwayTool, lookupGoTermTool, searchInteractionsTool, chemblTool, pubchemTool, openTargetsTool, searchPharmgkbTool, searchFaersTool, searchClinicalTrialsTool, searchGeoDatasetsTool, createSearchDgidbTool, searchBgeeExpressionTool, getImpcKoProfileTool, checkSafetyPanelTool, } from "../tools/bio/index.js";
36
36
  import { createNcbiTools, createChemDbTools } from "../tools/bio/keys.js";
37
37
  // Dependency-bearing tool factories.
38
38
  import { createGeneratePlanTool, createLiteratureReviewerTool, createInspectRunTool, createInspectDataProfileTool, createGenerateAnalogyReportTool, } from "../tools/research/index.js";
@@ -70,7 +70,7 @@ export function createConversationAgent(deps) {
70
70
  searchFaersTool,
71
71
  searchClinicalTrialsTool,
72
72
  searchGeoDatasetsTool,
73
- searchDgidbTool,
73
+ createSearchDgidbTool({ ...(deps.logger ? { logger: deps.logger } : {}) }),
74
74
  // Preclinical.
75
75
  searchBgeeExpressionTool,
76
76
  getImpcKoProfileTool,
@@ -25,6 +25,7 @@ import type { ProvenanceCollector as LineageCollector } from "../../provenance/c
25
25
  import { type BioToolKeys } from "../../tools/bio/keys.js";
26
26
  import { type BlockerHolder } from "../../tools/sandbox/report-blocker.js";
27
27
  import type { AgentMeta, SandboxToolName } from "./types.js";
28
+ import type { Logger } from "../../lib/logger.js";
28
29
  /**
29
30
  * Tools every sandbox agent receives — sandbox-environment introspection,
30
31
  * library docs, and run inspection. Spread into each agent's
@@ -50,6 +51,8 @@ export interface SandboxStepCoords {
50
51
  }
51
52
  /** The shared dependency graph every sandbox agent draws from. */
52
53
  export interface SandboxAgentDeps {
54
+ /** Operational logging seam; omitted falls back to no-op. */
55
+ readonly logger?: Logger;
53
56
  readonly provider: ChatProvider;
54
57
  readonly pool: Pool;
55
58
  readonly sandboxClient: SandboxClient;
@@ -26,7 +26,7 @@ import { createInspectRunTool } from "../../tools/research/inspect-run.js";
26
26
  // Data-profile retrieval (dependency-bearing) — always-on, see `createSandboxAgent`.
27
27
  import { createInspectDataProfileTool } from "../../tools/research/inspect-data-profile.js";
28
28
  // Bio leaf tools.
29
- import { checkSafetyPanelTool, chemblTool, getImpcKoProfileTool, lookupGoTermTool, openTargetsTool, pubchemTool, searchBgeeExpressionTool, searchClinicalTrialsTool, searchDgidbTool, searchFaersTool, searchGeneTool, searchGeoDatasetsTool, searchGwasCatalogTool, searchInteractionsTool, searchPathwayTool, searchPharmgkbTool, } from "../../tools/bio/index.js";
29
+ import { checkSafetyPanelTool, chemblTool, getImpcKoProfileTool, lookupGoTermTool, openTargetsTool, pubchemTool, searchBgeeExpressionTool, searchClinicalTrialsTool, createSearchDgidbTool, searchFaersTool, searchGeneTool, searchGeoDatasetsTool, searchGwasCatalogTool, searchInteractionsTool, searchPathwayTool, searchPharmgkbTool, } from "../../tools/bio/index.js";
30
30
  import { createNcbiTools, createChemDbTools } from "../../tools/bio/keys.js";
31
31
  // Workspace read surface.
32
32
  import { createFileStatTool, createGrepTool, createListFilesTool, createReadFileTool, createWorkspaceSearchTool } from "../../tools/workspace/index.js";
@@ -80,7 +80,7 @@ function resolveSandboxTools(deps, tools) {
80
80
  searchClinicalTrials: searchClinicalTrialsTool,
81
81
  searchGeoDatasets: searchGeoDatasetsTool,
82
82
  searchClinvar: ncbi.searchClinvar,
83
- searchDgidb: searchDgidbTool,
83
+ searchDgidb: createSearchDgidbTool({ ...(deps.logger ? { logger: deps.logger } : {}) }),
84
84
  searchGwasCatalog: searchGwasCatalogTool,
85
85
  searchDisgenet: chemDb.searchDisgenet,
86
86
  searchDrugbank: chemDb.searchDrugbank,
@@ -14,7 +14,10 @@
14
14
  */
15
15
  import type { Pool } from "pg";
16
16
  import { type AssembledMessages } from "./message-assembly.js";
17
+ import type { Logger } from "../lib/logger.js";
17
18
  export interface PrepareChatTurnDeps {
19
+ /** Operational logging seam; omitted falls back to no-op. */
20
+ readonly logger?: Logger;
18
21
  readonly pool: Pool;
19
22
  }
20
23
  export interface PrepareChatTurnParams {
@@ -19,6 +19,7 @@ import { createThreadStore } from "../memory/thread-store.js";
19
19
  import { createWorkingMemory } from "../memory/working-memory.js";
20
20
  import { loadAnalysisStatus } from "../state/index.js";
21
21
  import { assembleMessages } from "./message-assembly.js";
22
+ import { createNoopLogger } from "../lib/console-logger.js";
22
23
  /**
23
24
  * Prepare one chat turn: resolve thread ownership, seed the title, load
24
25
  * analysis status, and assemble the message array. Input sanitization stays
@@ -48,7 +49,8 @@ export async function prepareChatTurn(deps, params) {
48
49
  }
49
50
  }
50
51
  catch (err) {
51
- console.warn("[harness.chat] title-seed failed (non-fatal):", err instanceof Error ? err.message : err);
52
+ const logger = (deps.logger ?? createNoopLogger()).named("harness.chat");
53
+ logger.warn("title-seed failed (non-fatal)", logger.errorFields(err));
52
54
  }
53
55
  const analysisState = await loadAnalysisStatus(pool, analysisId).unwrapOr(null);
54
56
  const history = createThreadHistory(pool);
@@ -22,7 +22,10 @@ import type { ChatProvider, EmbeddingProvider } from "../providers/types.js";
22
22
  import type { BioToolKeys } from "../tools/bio/keys.js";
23
23
  import type { RunFinding } from "../workflows/execute-analysis.js";
24
24
  import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
25
+ import type { Logger } from "../lib/logger.js";
25
26
  export interface SynthesizeRunDeps {
27
+ /** Operational logging seam; omitted falls back to no-op. */
28
+ readonly logger?: Logger;
26
29
  readonly pool: Pool;
27
30
  readonly provider: ChatProvider;
28
31
  /** Write-side embedder — its `dimensions` sizes the per-analysis index. */
@@ -20,6 +20,7 @@ import { ensureSearchIndex, searchIndexName } from "../workspace/search-config.j
20
20
  import { createVectorStore } from "../state/vector-store.js";
21
21
  import { loadPlan, queryRun } from "../state/index.js";
22
22
  import { unwrapOrThrow } from "../lib/result.js";
23
+ import { createNoopLogger } from "../lib/console-logger.js";
23
24
  export async function synthesizeRun(deps, params) {
24
25
  const { pool, provider, embedding, resolveWorkspaceRoot, synthesisModel, bioKeys } = deps;
25
26
  const { analysisId, runId, completedSteps, session, emit, onProgress } = params;
@@ -71,7 +72,8 @@ export async function synthesizeRun(deps, params) {
71
72
  }));
72
73
  }
73
74
  catch (indexErr) {
74
- console.warn(`[synthesize-run] vector indexing failed for run ${runId}:`, indexErr instanceof Error ? indexErr.message : indexErr);
75
+ const logger = (deps.logger ?? createNoopLogger()).named("synthesize-run");
76
+ logger.warn("vector indexing failed", { runId, ...logger.errorFields(indexErr) });
75
77
  }
76
78
  await onProgress("persisting", "Persisting synthesis");
77
79
  await persistSynthesis({ workspaceRoot, runId, synthesis });
@@ -24,6 +24,7 @@ import type { AgentSession } from "../auth/types.js";
24
24
  import type { LoopMessage } from "../loop/types.js";
25
25
  import type { AgentChat } from "../providers/types.js";
26
26
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
27
+ import type { Logger } from "../lib/logger.js";
27
28
  export interface ArtifactForMetadata {
28
29
  /** Path used to update `cortex_artifacts` after metadata generation. */
29
30
  readonly dbPath: string;
@@ -33,6 +34,8 @@ export interface ArtifactForMetadata {
33
34
  readonly sizeBytes?: number;
34
35
  }
35
36
  export interface GenerateFileMetadataOptions {
37
+ /** Operational logging seam; omitted falls back to no-op. */
38
+ readonly logger?: Logger;
36
39
  readonly provider: AgentChat;
37
40
  readonly session: AgentSession;
38
41
  readonly artifacts: readonly ArtifactForMetadata[];
@@ -29,6 +29,7 @@ import { createReadFileTool } from "../tools/workspace/read-file.js";
29
29
  import { composeSystemPrompt } from "../agents/system-prompt.js";
30
30
  import { inferArtifactType } from "../schemas/artifact-manifest.js";
31
31
  import { SubmitFileMetadataInputSchema } from "../schemas/file-metadata.js";
32
+ import { createNoopLogger } from "../lib/console-logger.js";
32
33
  /** Sub-agent identity for the describer loop — provenance only. */
33
34
  const DESCRIBER_AGENT_ID = "file-metadata-describer";
34
35
  /**
@@ -156,6 +157,7 @@ function fallbackEntry(artifact, extra) {
156
157
  * result always carries exactly one entry per input artifact.
157
158
  */
158
159
  export async function generateFileMetadata(opts) {
160
+ const logger = (opts.logger ?? createNoopLogger()).named("artifact-metadata").with({ resourceId: opts.resourceId });
159
161
  if (opts.artifacts.length === 0) {
160
162
  return { indexed: 0, entries: [] };
161
163
  }
@@ -183,7 +185,7 @@ export async function generateFileMetadata(opts) {
183
185
  });
184
186
  }
185
187
  catch (err) {
186
- console.warn(`[artifact-metadata] describer loop failed (resource=${opts.resourceId}); using fallbacks: ${err instanceof Error ? err.message : err}`);
188
+ logger.warn("describer loop failed; using fallbacks", logger.errorFields(err));
187
189
  }
188
190
  const entries = [];
189
191
  const fallbackPaths = [];
@@ -198,7 +200,11 @@ export async function generateFileMetadata(opts) {
198
200
  }
199
201
  }
200
202
  if (fallbackPaths.length > 0) {
201
- console.warn(`[artifact-metadata] ${fallbackPaths.length}/${opts.artifacts.length} file(s) used a deterministic fallback description (resource=${opts.resourceId}): ${fallbackPaths.join(", ")}`);
203
+ logger.warn("file(s) used a deterministic fallback description", {
204
+ fallbackCount: fallbackPaths.length,
205
+ artifactCount: opts.artifacts.length,
206
+ fallbackPaths,
207
+ });
202
208
  }
203
209
  return { indexed: holder.size, entries };
204
210
  }
@@ -11,6 +11,7 @@
11
11
  */
12
12
  import type { Pool, PoolClient } from "pg";
13
13
  import type { AgentSession } from "../auth/types.js";
14
+ import type { Logger } from "../lib/logger.js";
14
15
  import type { ArtifactRegistry, ArtifactRegistrationInput } from "./artifact-registry.js";
15
16
  export type { ArtifactRegistrationInput };
16
17
  export interface ArtifactRegistrationResult {
@@ -33,4 +34,4 @@ export interface ArtifactRegistrationResult {
33
34
  * delegates external provenance registration to the injected `ArtifactRegistry`
34
35
  * and applies any returned external ids back onto the local rows.
35
36
  */
36
- export declare function registerStepArtifacts(db: Pool | PoolClient, registry: ArtifactRegistry, input: ArtifactRegistrationInput, session: AgentSession): Promise<ArtifactRegistrationResult>;
37
+ export declare function registerStepArtifacts(db: Pool | PoolClient, registry: ArtifactRegistry, input: ArtifactRegistrationInput, session: AgentSession, logger?: Logger): Promise<ArtifactRegistrationResult>;
@@ -9,6 +9,7 @@
9
9
  * (an external provenance ledger, or a filesystem index) is a composition-root
10
10
  * decision — this module never names one.
11
11
  */
12
+ import { createNoopLogger } from "../lib/console-logger.js";
12
13
  import { upsertArtifacts, updateArtifactId } from "../state/index.js";
13
14
  // ── Registration ─────────────────────────────────────────────────────
14
15
  /**
@@ -18,8 +19,13 @@ import { upsertArtifacts, updateArtifactId } from "../state/index.js";
18
19
  * delegates external provenance registration to the injected `ArtifactRegistry`
19
20
  * and applies any returned external ids back onto the local rows.
20
21
  */
21
- export async function registerStepArtifacts(db, registry, input, session) {
22
+ export async function registerStepArtifacts(db, registry, input, session,
23
+ // A parameter rather than a field on `ArtifactRegistrationInput`: that type is
24
+ // the seam's payload, handed verbatim to `registry.register`, and an
25
+ // embedder's registry has no business receiving the host's logger.
26
+ logger = createNoopLogger()) {
22
27
  const { resourceId, runId, stepId, artifacts } = input;
28
+ const log = logger.named("artifact-registration").with({ runId, stepId });
23
29
  if (artifacts.length === 0) {
24
30
  return { localCount: 0, externalRegistered: 0, externalFailed: 0, failureDetails: [] };
25
31
  }
@@ -27,7 +33,7 @@ export async function registerStepArtifacts(db, registry, input, session) {
27
33
  const emptyHash = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
28
34
  const localEntries = artifacts.map((a) => {
29
35
  if (a.size === 0 || a.hash === emptyHash) {
30
- console.warn(`[artifact-registration] empty artifact: path=${dbPathPrefix}${a.path} hash=${a.hash?.slice(0, 16)} size=${a.size} runId=${runId} stepId=${stepId}`);
36
+ log.warn("empty artifact", { path: `${dbPathPrefix}${a.path}`, hash: a.hash?.slice(0, 16), size: a.size });
31
37
  }
32
38
  return {
33
39
  resourceId,
@@ -29,12 +29,15 @@ import type { SandboxClient } from "../sandbox/client.js";
29
29
  import type { BioToolKeys } from "../tools/bio/keys.js";
30
30
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
31
31
  import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
32
+ import type { Logger } from "../lib/logger.js";
32
33
  /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
33
34
  export declare const EPHEMERAL_AGENT_ID = "ephemeral-executor";
34
35
  /** Workflow-id prefix — the boot-time never-recover sweep matches on it. */
35
36
  export declare const EPHEMERAL_WORKFLOW_PREFIX: "ephemeral:";
36
37
  /** The body's construction-time deps — closed over at registration. */
37
38
  export interface EphemeralDeps {
39
+ /** Operational logging seam; omitted falls back to no-op. */
40
+ readonly logger?: Logger;
38
41
  readonly provider: ChatProvider;
39
42
  readonly pool: Pool;
40
43
  readonly sandboxClient: SandboxClient;
@@ -28,6 +28,7 @@ import { durableStep } from "../loop/run-step.js";
28
28
  import { generateExecutionId } from "../sandbox/execution-id.js";
29
29
  import { mintSandboxIdentity } from "../sandbox/identity.js";
30
30
  import { createEphemeralExecutorAgent } from "../agents/sandbox/ephemeral-executor.js";
31
+ import { createNoopLogger } from "../lib/console-logger.js";
31
32
  /** Sub-agent identity — appended to `callPath`, set as `agentId`. */
32
33
  export const EPHEMERAL_AGENT_ID = "ephemeral-executor";
33
34
  /**
@@ -85,13 +86,16 @@ export async function runEphemeralBody(input, deps) {
85
86
  throw new Error(`runEphemeralBody requires an analysis-scoped session — got ${runSession.scope.kind}`);
86
87
  }
87
88
  const analysisId = runSession.scope.analysisId;
89
+ // Bound after the scope guard: `analysisId` comes off the session's scope,
90
+ // not the workflow input.
91
+ const logger = (deps.logger ?? createNoopLogger()).named("ephemeral").with({ analysisId });
88
92
  const startedAt = performance.now();
89
93
  const executionId = generateExecutionId(EPHEMERAL_AGENT_ID);
90
94
  const workflowId = DBOS.workflowID ?? `${EPHEMERAL_WORKFLOW_PREFIX}${executionId}`;
91
95
  const childSession = forSubAgent(runSession, EPHEMERAL_AGENT_ID);
92
96
  // Resolver throw = loud workflow failure, per the workspace-root-resolution contract.
93
97
  const workspaceRoot = deps.resolveWorkspaceRoot(analysisId);
94
- console.log(`[ephemeral] execution=${executionId} analysis=${analysisId} starting sandbox`);
98
+ logger.info("starting sandbox", { executionId });
95
99
  const sandbox = await deps.sandboxClient.createSandbox({
96
100
  runId: EPHEMERAL_RUN_LITERAL,
97
101
  stepId: EPHEMERAL_RUN_LITERAL,
@@ -148,12 +152,12 @@ export async function runEphemeralBody(input, deps) {
148
152
  const text = finalText(finalMessages);
149
153
  const stepsUsed = countAssistantTurns(finalMessages);
150
154
  const durationMs = Math.round(performance.now() - startedAt);
151
- console.log(`[ephemeral] execution=${executionId} completed stepsUsed=${stepsUsed} durationMs=${durationMs}`);
155
+ logger.info("completed", { executionId, stepsUsed, durationMs });
152
156
  return { text, stepsUsed, durationMs };
153
157
  }
154
158
  catch (err) {
155
159
  const message = err instanceof Error ? err.message : String(err);
156
- console.error(`[ephemeral] execution=${executionId} failed: ${message}`);
160
+ logger.error("failed", { executionId, err: message });
157
161
  throw err;
158
162
  }
159
163
  finally {
@@ -163,7 +167,7 @@ export async function runEphemeralBody(input, deps) {
163
167
  catch (teardownErr) {
164
168
  // Teardown is idempotent — "already gone" is success. A failure here
165
169
  // is best-effort logged so the chat turn doesn't fail on cleanup.
166
- console.warn(`[ephemeral] execution=${executionId} teardown failed (non-fatal): ${teardownErr instanceof Error ? teardownErr.message : teardownErr}`);
170
+ logger.warn("teardown failed (non-fatal)", { executionId, ...logger.errorFields(teardownErr) });
167
171
  }
168
172
  }
169
173
  }
@@ -16,6 +16,7 @@ import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
16
16
  import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
17
17
  import type { ArtifactManifestEntry } from "../schemas/artifact-manifest.js";
18
18
  import type { StepSummary } from "../schemas/step-summary.js";
19
+ import type { Logger } from "../lib/logger.js";
19
20
  import type { ArtifactRegistry } from "./artifact-registry.js";
20
21
  import { type FileMetadataEntry } from "./artifact-metadata.js";
21
22
  import type { PostStepArtifacts, PostStepContext, StepOutputs } from "../workflows/sandbox-step.js";
@@ -25,6 +26,8 @@ import type { PostStepArtifacts, PostStepContext, StepOutputs } from "../workflo
25
26
  */
26
27
  export interface PostStepPipelineDeps {
27
28
  readonly pool: Pool;
29
+ /** Operational logging seam; omitted falls back to no-op. */
30
+ readonly logger?: Logger;
28
31
  /** Non-streaming chat — the metadata + summary sub-agent loops. */
29
32
  readonly provider: AgentChat;
30
33
  /** Write-side embedder for the vector index. */
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { join } from "node:path";
14
14
  import { unwrapOrThrow } from "../lib/result.js";
15
+ import { createNoopLogger } from "../lib/console-logger.js";
15
16
  import { writeFileWithinRoot } from "../lib/fs-helpers.js";
16
17
  import { registerStepArtifacts } from "./artifact-registration.js";
17
18
  import { reconcileManifestWithDisk } from "./reconcile-manifest.js";
@@ -79,7 +80,12 @@ export async function generateStepSummaryAndWrite(deps, postCtx, manifest) {
79
80
  await writeFileWithinRoot(writePrefix, join(writePrefix, "output", "summary.md"), summary.markdown);
80
81
  }
81
82
  catch (err) {
82
- console.warn(`[post-step.summary] writeFile output/summary.md failed for ${input.stepId}: ${err instanceof Error ? err.message : err}`);
83
+ const logger = (deps.logger ?? createNoopLogger()).named("post-step").named("summary");
84
+ logger.warn("writeFile output/summary.md failed", {
85
+ runId: input.runId,
86
+ stepId: input.stepId,
87
+ ...logger.errorFields(err),
88
+ });
83
89
  }
84
90
  }
85
91
  return summary;
@@ -102,6 +108,7 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
102
108
  agentId: input.agentId,
103
109
  manifest: [...manifest],
104
110
  collector: lineageCollector,
111
+ ...(deps.logger ? { logger: deps.logger } : {}),
105
112
  });
106
113
  if (reconciled.manifest.length === 0)
107
114
  return reconciled.manifest;
@@ -111,7 +118,7 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
111
118
  stepId: input.stepId,
112
119
  artifacts: reconciled.manifest,
113
120
  collector: lineageCollector,
114
- }, session);
121
+ }, session, deps.logger);
115
122
  if (reg.externalFailed > 0) {
116
123
  const wholeActivityFailed = reg.externalRegistered === 0;
117
124
  const detail = reg.failureDetails.map((f) => `${f.path}: ${f.error}`).join("\n ");
@@ -119,7 +126,19 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
119
126
  `${reg.externalFailed} row(s) rejected, ${reg.externalRegistered}/${reg.localCount} local artifact(s) registered` +
120
127
  (wholeActivityFailed ? " (WHOLE ACTIVITY ROLLED BACK — outputs orphaned)" : "") +
121
128
  (detail ? `\n ${detail}` : "");
122
- console.error(msg);
129
+ // Logged as fields as well as thrown: the throw reaches `failStep` as one
130
+ // opaque string, so the per-path rejections are only queryable from here.
131
+ // This is what distinguishes a registry rejection from the attestation
132
+ // throws in `reconcileManifestWithDisk` when a step dies.
133
+ (deps.logger ?? createNoopLogger()).named("post-step").named("reconcile").error("external registration failed", {
134
+ runId: input.runId,
135
+ stepId: input.stepId,
136
+ externalFailed: reg.externalFailed,
137
+ externalRegistered: reg.externalRegistered,
138
+ localCount: reg.localCount,
139
+ wholeActivityFailed,
140
+ failures: reg.failureDetails,
141
+ });
123
142
  throw new Error(msg);
124
143
  }
125
144
  return reconciled.manifest;
@@ -176,7 +195,8 @@ export async function vectorIndexStepOutputs(deps, postCtx, artifacts) {
176
195
  }
177
196
  }
178
197
  catch (err) {
179
- console.warn(`[post-step.vector-index] indexing failed for ${input.stepId}: ${err instanceof Error ? err.message : err}`);
198
+ const logger = (deps.logger ?? createNoopLogger()).named("post-step").named("vector-index");
199
+ logger.warn("indexing failed", { runId: input.runId, stepId: input.stepId, ...logger.errorFields(err) });
180
200
  }
181
201
  }
182
202
  /**
@@ -1,5 +1,6 @@
1
1
  import type { ArtifactManifestEntry } from "../schemas/artifact-manifest.js";
2
2
  import type { ProvenanceCollector } from "../provenance/collector.js";
3
+ import type { Logger } from "../lib/logger.js";
3
4
  export interface ReconcileManifestInput {
4
5
  /** Absolute host root of the analysis's workspace tree. */
5
6
  workspaceRoot: string;
@@ -15,6 +16,12 @@ export interface ReconcileManifestInput {
15
16
  manifest: ArtifactManifestEntry[];
16
17
  /** Collector backing the manifest; mutated to reflect drops/rehashes. */
17
18
  collector: ProvenanceCollector;
19
+ /**
20
+ * Operational logging seam; omitted falls back to no-op. Attestation is
21
+ * fail-fast, so what this records — which entry was dropped, which input
22
+ * could not be hashed — is the account of why a step died.
23
+ */
24
+ logger?: Logger;
18
25
  }
19
26
  export interface ReconcileManifestResult {
20
27
  /** Reconciled manifest — phantoms removed, every entry rehashed from disk. */
@@ -21,10 +21,12 @@
21
21
  */
22
22
  import { stat } from "node:fs/promises";
23
23
  import path from "node:path";
24
+ import { createNoopLogger } from "../lib/console-logger.js";
24
25
  import { computeSha256File, hasValidContentHash } from "../lib/fs-helpers.js";
25
26
  import { artifactReconcileDropped } from "../lib/metrics.js";
26
27
  export async function reconcileManifestWithDisk(input) {
27
28
  const { workspaceRoot, resourceId, runId, stepId, agentId, manifest, collector } = input;
29
+ const logger = (input.logger ?? createNoopLogger()).named("reconcile-manifest").with({ runId, stepId, agentId });
28
30
  const stepRoot = path.join(workspaceRoot, "runs", runId, stepId);
29
31
  const reconciled = [];
30
32
  let droppedCount = 0;
@@ -36,7 +38,7 @@ export async function reconcileManifestWithDisk(input) {
36
38
  // ENOENT and silently drop a real artifact, or — worse — succeed
37
39
  // against a host file the step never produced.
38
40
  if (!absPath.startsWith(stepRoot + path.sep) && absPath !== stepRoot) {
39
- console.warn(`[reconcile-manifest] skipping out-of-bounds path=${entry.path} stepId=${stepId} runId=${runId}`);
41
+ logger.warn("skipping out-of-bounds entry", { path: entry.path });
40
42
  continue;
41
43
  }
42
44
  let info;
@@ -45,7 +47,7 @@ export async function reconcileManifestWithDisk(input) {
45
47
  }
46
48
  catch (err) {
47
49
  if (err.code === "ENOENT") {
48
- console.debug(`[reconcile-manifest] dropping phantom path=${entry.path} stepId=${stepId} runId=${runId}`);
50
+ logger.debug("dropping phantom entry", { path: entry.path });
49
51
  collector.removeRecord(entry.path);
50
52
  droppedCount++;
51
53
  artifactReconcileDropped.add(1, { agent_id: agentId, step_id: stepId });
@@ -58,7 +60,7 @@ export async function reconcileManifestWithDisk(input) {
58
60
  // uses `createReadStream`, which throws `EISDIR` on directories and would
59
61
  // fail the whole step's reconciliation.
60
62
  if (!info.isFile()) {
61
- console.debug(`[reconcile-manifest] dropping non-file path=${entry.path} stepId=${stepId} runId=${runId}`);
63
+ logger.debug("dropping non-file entry", { path: entry.path });
62
64
  collector.removeRecord(entry.path);
63
65
  droppedCount++;
64
66
  artifactReconcileDropped.add(1, { agent_id: agentId, step_id: stepId });
@@ -72,7 +74,7 @@ export async function reconcileManifestWithDisk(input) {
72
74
  const hash = await computeSha256File(absPath);
73
75
  reconciled.push({ ...entry, hash, size: info.size });
74
76
  }
75
- await fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId);
77
+ await fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, logger);
76
78
  return { manifest: reconciled, droppedCount };
77
79
  }
78
80
  /**
@@ -88,22 +90,31 @@ export async function reconcileManifestWithDisk(input) {
88
90
  * Lineage attestation is fail-fast: an input we cannot hash (gone, or resolving
89
91
  * outside the analysis tree) throws rather than registering a hashless edge.
90
92
  */
91
- async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId) {
93
+ async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, runId, stepId, logger) {
92
94
  const resourceRoot = path.resolve(workspaceRoot);
93
95
  for (const ref of collector.getTrackedInputs()) {
94
96
  if (ref.source === "artifacts")
95
97
  continue;
96
98
  if (hasValidContentHash(ref.hash))
97
99
  continue;
100
+ // Each throw below is logged before it is raised. The thrown message reaches
101
+ // `failStep` as one opaque string and everything downstream of that is
102
+ // scrubbed, so naming the site and the offending ref HERE is what makes a
103
+ // fail-fast attestation distinguishable from a registry rejection after the
104
+ // fact — the read's `source` in particular says whether the step declared
105
+ // this input at all.
106
+ const attestation = { path: ref.path, source: ref.source, refRunId: ref.runId, refStepId: ref.stepId };
98
107
  // `ref.path` is the absolute container path `/{resourceId}/...`; strip the
99
108
  // mount segment and map the tail onto the host workspace root. `path.join`
100
109
  // normalizes any `..`, and the bound confines the read to the analysis tree.
101
110
  const containerPrefix = `/${resourceId}`;
102
111
  if (ref.path !== containerPrefix && !ref.path.startsWith(containerPrefix + "/")) {
112
+ logger.error("input read resolves outside the analysis tree", { ...attestation, throwSite: "container-prefix-bound" });
103
113
  throw new Error(`[reconcile-manifest] input read resolves outside the analysis tree: path=${ref.path} stepId=${stepId} runId=${runId}`);
104
114
  }
105
115
  const hostPath = path.join(resourceRoot, ref.path.slice(containerPrefix.length + 1));
106
116
  if (hostPath !== resourceRoot && !hostPath.startsWith(resourceRoot + path.sep)) {
117
+ logger.error("input read resolves outside the analysis tree", { ...attestation, hostPath, throwSite: "workspace-root-bound" });
107
118
  throw new Error(`[reconcile-manifest] input read resolves outside the analysis tree: path=${ref.path} stepId=${stepId} runId=${runId}`);
108
119
  }
109
120
  let info;
@@ -112,10 +123,12 @@ async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, run
112
123
  }
113
124
  catch (err) {
114
125
  if (err.code === "ENOENT") {
126
+ logger.error("cannot attest input — not present at reconcile", { ...attestation, hostPath, throwSite: "input-enoent" });
115
127
  throw new Error(`[reconcile-manifest] cannot attest input ${ref.path}: not present at reconcile (stepId=${stepId} runId=${runId})`, {
116
128
  cause: err,
117
129
  });
118
130
  }
131
+ logger.error("cannot attest input — stat failed", { ...attestation, hostPath, throwSite: "input-stat", ...logger.errorFields(err) });
119
132
  throw err;
120
133
  }
121
134
  if (!info.isFile()) {
@@ -124,6 +137,7 @@ async function fillInputHashesFromDisk(collector, workspaceRoot, resourceId, run
124
137
  // it from lineage rather than failing the step — mirrors the output-side
125
138
  // non-file drop above. A genuinely missing FILE (ENOENT) still fails fast
126
139
  // as drift; a directory is not drift.
140
+ logger.debug("dropping non-file input from lineage", attestation);
127
141
  collector.dropInput(ref);
128
142
  continue;
129
143
  }
@@ -37,7 +37,10 @@ import type { EmitFn } from "../loop/types.js";
37
37
  import type { ChatProvider } from "../providers/types.js";
38
38
  import type { PreviewPublisher } from "../tools/report/preview-publisher.js";
39
39
  import type { ChromeConfig } from "../lib/chrome.js";
40
+ import type { Logger } from "../lib/logger.js";
40
41
  export interface ReportRunnerDeps {
42
+ /** Operational logging seam; omitted falls back to no-op. */
43
+ readonly logger?: Logger;
41
44
  readonly provider: ChatProvider;
42
45
  readonly pool: Pool;
43
46
  /** Model id — provenance / metric label; provider owns the wire model. */
@@ -41,10 +41,12 @@ import { withPreviewLock } from "../tools/lib/preview-lock.js";
41
41
  import { createBuildReportTool, createSubmitReportTool, createMintPreviewUrlTool, createPreviewSnapshotTool, createVersionFsTools, } from "../tools/report/index.js";
42
42
  import { createReportBuilderAgent, REPORT_BUILDER_AGENT_ID } from "../agents/report-builder.js";
43
43
  import { createSkillTools } from "../tools/sandbox/skills.js";
44
+ import { createNoopLogger } from "../lib/console-logger.js";
44
45
  /** Skill pack the report-builder prompt directs the model to read (design-system reference). */
45
46
  const REPORT_BUILDER_SKILLS = ["report-html"];
46
47
  const REPORT_AGENT_MAX_STEPS = 75;
47
48
  export async function runReportIteration(deps, opts) {
49
+ const logger = (deps.logger ?? createNoopLogger()).named("report-runner").with({ previewId: opts.previewId });
48
50
  const startTime = performance.now();
49
51
  return withPreviewLock(opts.previewId, async () => {
50
52
  // ── Resolve version directories ───────────────────────────────
@@ -169,9 +171,7 @@ export async function runReportIteration(deps, opts) {
169
171
  }
170
172
  }
171
173
  if (!outcome || agentError || phantomSuccess) {
172
- console.warn(`[report-runner] event=report.agent.finish preview=${opts.previewId} v${newVersion} ` +
173
- `submitted=${outcome?.ok === true} phantom=${phantomSuccess} ` +
174
- `agentError=${agentError ?? "null"}`);
174
+ logger.warn("report.agent.finish", { version: newVersion, submitted: outcome?.ok === true, phantom: phantomSuccess, agentError });
175
175
  const { reason, errorKind } = classifyFailure({
176
176
  phantomSuccess,
177
177
  agentError,
@@ -182,7 +182,7 @@ export async function runReportIteration(deps, opts) {
182
182
  await rm(versionDirAbs, { recursive: true, force: true });
183
183
  }
184
184
  catch (err) {
185
- console.error(`[report-runner] rollback failed for ${versionDirAbs}:`, err instanceof Error ? err.message : err);
185
+ logger.error("rollback failed", { versionDirAbs, ...logger.errorFields(err) });
186
186
  }
187
187
  return failure(opts.previewId, newVersion, reason, errorKind, startTime);
188
188
  }
@@ -29,7 +29,10 @@ import type { LoopMessage } from "../loop/types.js";
29
29
  import type { AgentChat } from "../providers/types.js";
30
30
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
31
31
  import { type StepSummary } from "../schemas/step-summary.js";
32
+ import type { Logger } from "../lib/logger.js";
32
33
  export interface GenerateStepSummaryOptions {
34
+ /** Operational logging seam; omitted falls back to no-op. */
35
+ readonly logger?: Logger;
33
36
  readonly provider: AgentChat;
34
37
  readonly session: AgentSession;
35
38
  readonly modelId: string;