@inflexa-ai/harness 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +48 -0
- package/dist/agents/conversation-agent.d.ts +3 -0
- package/dist/agents/conversation-agent.js +2 -2
- package/dist/agents/sandbox/shared.d.ts +3 -0
- package/dist/agents/sandbox/shared.js +2 -2
- package/dist/app/chat-turn.d.ts +3 -0
- package/dist/app/chat-turn.js +3 -1
- package/dist/app/synthesize-run.d.ts +3 -0
- package/dist/app/synthesize-run.js +3 -1
- package/dist/execution/artifact-metadata.d.ts +3 -0
- package/dist/execution/artifact-metadata.js +8 -2
- package/dist/execution/artifact-registration.d.ts +2 -1
- package/dist/execution/artifact-registration.js +8 -2
- package/dist/execution/ephemeral-runner.d.ts +3 -0
- package/dist/execution/ephemeral-runner.js +8 -4
- package/dist/execution/post-step-pipeline.d.ts +3 -0
- package/dist/execution/post-step-pipeline.js +24 -4
- package/dist/execution/reconcile-manifest.d.ts +7 -0
- package/dist/execution/reconcile-manifest.js +19 -5
- package/dist/execution/report-runner.d.ts +3 -0
- package/dist/execution/report-runner.js +4 -4
- package/dist/execution/step-summary.d.ts +3 -0
- package/dist/execution/step-summary.js +4 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -0
- package/dist/lib/chrome.d.ts +4 -1
- package/dist/lib/chrome.js +10 -7
- package/dist/lib/console-logger.d.ts +19 -0
- package/dist/lib/console-logger.js +62 -0
- package/dist/lib/logger.d.ts +88 -0
- package/dist/lib/logger.js +37 -0
- package/dist/lib/otel.d.ts +2 -1
- package/dist/lib/otel.js +8 -3
- package/dist/runtime/boot.d.ts +2 -2
- package/dist/runtime/boot.js +1 -1
- package/dist/runtime/connection-budget.d.ts +2 -2
- package/dist/runtime/connection-budget.js +5 -3
- package/dist/runtime/dbos.d.ts +7 -7
- package/dist/runtime/dbos.js +14 -11
- package/dist/runtime/shutdown.d.ts +2 -2
- package/dist/runtime/shutdown.js +4 -3
- package/dist/sandbox/create-sandbox.d.ts +2 -2
- package/dist/sandbox/docker-client.d.ts +2 -2
- package/dist/sandbox/docker-client.js +4 -1
- package/dist/sandbox/k8s-client.d.ts +3 -0
- package/dist/sandbox/k8s-client.js +5 -4
- package/dist/sandbox/notification-sweep.d.ts +3 -3
- package/dist/sandbox/notification-sweep.js +4 -2
- package/dist/sandbox/reaper.d.ts +3 -3
- package/dist/sandbox/reaper.js +7 -4
- package/dist/sandbox/watchdog.d.ts +3 -3
- package/dist/sandbox/watchdog.js +8 -2
- package/dist/state/init.d.ts +2 -1
- package/dist/state/init.js +5 -6
- package/dist/tasks/data-profile.d.ts +5 -0
- package/dist/tasks/data-profile.js +24 -17
- package/dist/tools/bio/search-dgidb.d.ts +11 -1
- package/dist/tools/bio/search-dgidb.js +92 -78
- package/dist/tools/iterate-report.d.ts +3 -0
- package/dist/tools/iterate-report.js +3 -1
- package/dist/tools/research/literature-reviewer.d.ts +3 -0
- package/dist/tools/research/literature-reviewer.js +2 -2
- package/dist/tools/workspace/execute-command.d.ts +3 -0
- package/dist/tools/workspace/execute-command.js +3 -1
- package/dist/workflows/execute-analysis.d.ts +3 -0
- package/dist/workflows/execute-analysis.js +11 -7
- package/dist/workflows/execute-target-assessment.d.ts +3 -0
- package/dist/workflows/execute-target-assessment.js +19 -15
- package/dist/workflows/sandbox-step.d.ts +9 -0
- package/dist/workflows/sandbox-step.js +39 -17
- package/dist/workflows/target-assessment/lib/llm-step.d.ts +3 -0
- package/dist/workflows/target-assessment/lib/llm-step.js +3 -1
- package/dist/workflows/target-assessment/lib/structured-llm.d.ts +3 -0
- package/dist/workflows/target-assessment/lib/structured-llm.js +1 -0
- package/dist/workflows/target-assessment/phase5-persist.d.ts +3 -0
- package/dist/workflows/target-assessment/phase5-persist.js +8 -3
- package/dist/workflows/target-assessment/progress.d.ts +2 -1
- package/dist/workflows/target-assessment/progress.js +4 -3
- package/dist/workspace/filesystem.js +163 -74
- package/package.json +95 -96
|
@@ -32,6 +32,7 @@ import { composeSystemPrompt } from "../agents/system-prompt.js";
|
|
|
32
32
|
import { createReadFileTool } from "../tools/workspace/read-file.js";
|
|
33
33
|
import { StepSummarySchema } from "../schemas/step-summary.js";
|
|
34
34
|
import { incrementSummaryNullCount } from "./step-summary-metrics.js";
|
|
35
|
+
import { createNoopLogger } from "../lib/console-logger.js";
|
|
35
36
|
const SYSTEM_PROMPT = `You are a sandbox agent writing a post-step interpretive summary of work you just completed.
|
|
36
37
|
|
|
37
38
|
Your prior conversation — tool calls, code output, intermediate results — is in the message history above. Use it for narrative and intent.
|
|
@@ -69,6 +70,7 @@ function sanitizeTranscript(messages) {
|
|
|
69
70
|
* without `summary.md` in that case.
|
|
70
71
|
*/
|
|
71
72
|
export async function generateStepSummary(opts) {
|
|
73
|
+
const logger = (opts.logger ?? createNoopLogger()).named("step-summary").with({ runId: opts.runId, stepId: opts.stepId, agentId: opts.agentId });
|
|
72
74
|
const transcript = sanitizeTranscript(opts.messages);
|
|
73
75
|
const userPrompt = stepSummaryPrompt(opts.artifactPaths.join("\n"));
|
|
74
76
|
const writer = {
|
|
@@ -89,13 +91,13 @@ export async function generateStepSummary(opts) {
|
|
|
89
91
|
});
|
|
90
92
|
}
|
|
91
93
|
catch (err) {
|
|
92
|
-
|
|
94
|
+
logger.warn("summary loop failed", logger.errorFields(err));
|
|
93
95
|
incrementSummaryNullCount(opts.agentId, "throw");
|
|
94
96
|
return undefined;
|
|
95
97
|
}
|
|
96
98
|
const markdown = finalText(result.messages);
|
|
97
99
|
if (!markdown || markdown.trim().length === 0) {
|
|
98
|
-
|
|
100
|
+
logger.warn("empty markdown");
|
|
99
101
|
incrementSummaryNullCount(opts.agentId, "empty");
|
|
100
102
|
return undefined;
|
|
101
103
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -108,6 +108,9 @@ export { REFERENCE_INSTALL_RECEIPT_VERSION, ReferenceInstallReceiptSchema, parse
|
|
|
108
108
|
export type { ReferenceInstallReceipt, ReferenceReceiptArtifact } from "./reference-data/receipt.js";
|
|
109
109
|
export { queryActiveSandboxes } from "./state/active-sandboxes.js";
|
|
110
110
|
export { runStepDir } from "./workspace/paths.js";
|
|
111
|
+
export type { LogFields, LogLevel, Logger } from "./lib/logger.js";
|
|
112
|
+
export { defaultErrorFields } from "./lib/logger.js";
|
|
113
|
+
export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
|
|
111
114
|
export { registerSandboxReaper } from "./sandbox/reaper.js";
|
|
112
115
|
export type { RegisterReaperDeps } from "./sandbox/reaper.js";
|
|
113
116
|
export { registerWatchdog } from "./sandbox/watchdog.js";
|
package/dist/index.js
CHANGED
|
@@ -162,6 +162,11 @@ export { queryActiveSandboxes } from "./state/active-sandboxes.js";
|
|
|
162
162
|
// Workspace-tree path convention — the root-relative step directory each
|
|
163
163
|
// sandbox step confines its artifact writes to (join onto the resolved root).
|
|
164
164
|
export { runStepDir } from "./workspace/paths.js";
|
|
165
|
+
// `Logger.errorFields` is on the interface so a realization can defer to its
|
|
166
|
+
// sink's native error handling (pino's `err` serializer, OTel's `exception.*`);
|
|
167
|
+
// `defaultErrorFields` is the shipped mapping for one with no opinion.
|
|
168
|
+
export { defaultErrorFields } from "./lib/logger.js";
|
|
169
|
+
export { createConsoleLogger, createNoopLogger } from "./lib/console-logger.js";
|
|
165
170
|
// Sandbox-hygiene scheduled workflows. Reaper reclaims a dead host's orphaned
|
|
166
171
|
// containers; watchdog converts a dead sandbox into a prompt step failure
|
|
167
172
|
// instead of a deadline-long `DBOS.recv` hang; the sweep prunes stale
|
package/dist/lib/chrome.d.ts
CHANGED
|
@@ -7,9 +7,12 @@
|
|
|
7
7
|
* semaphore so the sidecar isn't overwhelmed by concurrent reports.
|
|
8
8
|
*/
|
|
9
9
|
import type { Browser, BrowserContext, Page } from "puppeteer-core";
|
|
10
|
+
import type { Logger } from "./logger.js";
|
|
10
11
|
export interface ChromeConfig {
|
|
12
|
+
/** Operational logging seam; omitted falls back to no-op. */
|
|
13
|
+
readonly logger?: Logger;
|
|
11
14
|
readonly browserUrl?: string;
|
|
12
15
|
readonly maxPages?: number;
|
|
13
16
|
}
|
|
14
|
-
export declare function getBrowser(browserUrl?: string): Promise<Browser>;
|
|
17
|
+
export declare function getBrowser(browserUrl?: string, injected?: Logger): Promise<Browser>;
|
|
15
18
|
export declare function withPage<T>(cfg: ChromeConfig, fn: (page: Page, context: BrowserContext) => Promise<T>): Promise<T>;
|
package/dist/lib/chrome.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* semaphore so the sidecar isn't overwhelmed by concurrent reports.
|
|
8
8
|
*/
|
|
9
9
|
import puppeteer from "puppeteer-core";
|
|
10
|
+
import { createNoopLogger } from "./console-logger.js";
|
|
10
11
|
let browser;
|
|
11
12
|
let connecting;
|
|
12
13
|
let semaphore;
|
|
@@ -41,7 +42,8 @@ function getSemaphore(maxPages) {
|
|
|
41
42
|
}
|
|
42
43
|
return semaphore;
|
|
43
44
|
}
|
|
44
|
-
export async function getBrowser(browserUrl) {
|
|
45
|
+
export async function getBrowser(browserUrl, injected) {
|
|
46
|
+
const logger = (injected ?? createNoopLogger()).named("chrome");
|
|
45
47
|
if (browser && browser.connected)
|
|
46
48
|
return browser;
|
|
47
49
|
if (connecting)
|
|
@@ -50,21 +52,21 @@ export async function getBrowser(browserUrl) {
|
|
|
50
52
|
if (!browserURL) {
|
|
51
53
|
throw new Error("CHROME_BROWSER_URL is not set — Cortex requires the chrome sidecar to be reachable");
|
|
52
54
|
}
|
|
53
|
-
|
|
55
|
+
logger.info("connecting to browser", { browserURL });
|
|
54
56
|
connecting = puppeteer
|
|
55
57
|
.connect({ browserURL })
|
|
56
58
|
.then((b) => {
|
|
57
59
|
browser = b;
|
|
58
60
|
b.on("disconnected", () => {
|
|
59
|
-
|
|
61
|
+
logger.info("browser disconnected; will reconnect on next request");
|
|
60
62
|
if (browser === b)
|
|
61
63
|
browser = undefined;
|
|
62
64
|
});
|
|
63
|
-
|
|
65
|
+
logger.info("connected", { wsEndpoint: b.wsEndpoint() });
|
|
64
66
|
return b;
|
|
65
67
|
})
|
|
66
68
|
.catch((err) => {
|
|
67
|
-
|
|
69
|
+
logger.error("failed to connect to browser", logger.errorFields(err));
|
|
68
70
|
throw err;
|
|
69
71
|
})
|
|
70
72
|
.finally(() => {
|
|
@@ -76,7 +78,7 @@ export async function withPage(cfg, fn) {
|
|
|
76
78
|
const release = await getSemaphore(cfg.maxPages).acquire();
|
|
77
79
|
let context;
|
|
78
80
|
try {
|
|
79
|
-
const b = await getBrowser(cfg.browserUrl);
|
|
81
|
+
const b = await getBrowser(cfg.browserUrl, cfg.logger);
|
|
80
82
|
context = await b.createBrowserContext();
|
|
81
83
|
const page = await context.newPage();
|
|
82
84
|
return await fn(page, context);
|
|
@@ -87,7 +89,8 @@ export async function withPage(cfg, fn) {
|
|
|
87
89
|
await context.close();
|
|
88
90
|
}
|
|
89
91
|
catch (err) {
|
|
90
|
-
|
|
92
|
+
const logger = (cfg.logger ?? createNoopLogger()).named("chrome");
|
|
93
|
+
logger.error("error closing browser context", logger.errorFields(err));
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
release();
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trivial `Logger` realizations: one that writes to `console`, and the
|
|
3
|
+
* silent fallback the harness substitutes when an embedder wires nothing.
|
|
4
|
+
*
|
|
5
|
+
* `createConsoleLogger` is offered, never defaulted. A host whose UI owns
|
|
6
|
+
* stdout (an alternate-screen TUI, for instance) discards console output, so
|
|
7
|
+
* defaulting to it would quietly reintroduce the invisible-diagnostics failure
|
|
8
|
+
* the seam exists to prevent — the default is `createNoopLogger`, which is at
|
|
9
|
+
* least honest about saying nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { type Logger } from "./logger.js";
|
|
12
|
+
/** Build a `Logger` that writes each record to the matching `console` method. */
|
|
13
|
+
export declare function createConsoleLogger(): Logger;
|
|
14
|
+
/**
|
|
15
|
+
* Build a `Logger` that discards every record. This is what the harness falls
|
|
16
|
+
* back to when an embedder injects no logger, so internal call sites can log
|
|
17
|
+
* unconditionally instead of threading `?.` through every diagnostic.
|
|
18
|
+
*/
|
|
19
|
+
export declare function createNoopLogger(): Logger;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The trivial `Logger` realizations: one that writes to `console`, and the
|
|
3
|
+
* silent fallback the harness substitutes when an embedder wires nothing.
|
|
4
|
+
*
|
|
5
|
+
* `createConsoleLogger` is offered, never defaulted. A host whose UI owns
|
|
6
|
+
* stdout (an alternate-screen TUI, for instance) discards console output, so
|
|
7
|
+
* defaulting to it would quietly reintroduce the invisible-diagnostics failure
|
|
8
|
+
* the seam exists to prevent — the default is `createNoopLogger`, which is at
|
|
9
|
+
* least honest about saying nothing.
|
|
10
|
+
*/
|
|
11
|
+
import { defaultErrorFields } from "./logger.js";
|
|
12
|
+
/** Renders accumulated namespace segments as the message's `[a.b]` prefix. */
|
|
13
|
+
function prefixed(names, msg) {
|
|
14
|
+
return names.length > 0 ? `[${names.join(".")}] ${msg}` : msg;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* `with()` accumulates into `bindings` and `named()` into `names`; each record
|
|
18
|
+
* merges call-site fields over the bindings, so a narrower call-site key
|
|
19
|
+
* deliberately wins over inherited context.
|
|
20
|
+
*/
|
|
21
|
+
function makeConsoleLogger(names, bindings) {
|
|
22
|
+
const emit = (level) => (msg, fields) => {
|
|
23
|
+
// The one sanctioned console site in the harness — writing to console is
|
|
24
|
+
// this function's entire contract. `no-console` exempts this file by path
|
|
25
|
+
// in eslint.config.js rather than by an inline disable here.
|
|
26
|
+
console[level](prefixed(names, msg), { ...bindings, ...fields });
|
|
27
|
+
};
|
|
28
|
+
return {
|
|
29
|
+
debug: emit("debug"),
|
|
30
|
+
info: emit("info"),
|
|
31
|
+
warn: emit("warn"),
|
|
32
|
+
error: emit("error"),
|
|
33
|
+
with: (extra) => makeConsoleLogger(names, { ...bindings, ...extra }),
|
|
34
|
+
named: (name) => makeConsoleLogger([...names, name], bindings),
|
|
35
|
+
// console renders whatever it is handed; there is no native error
|
|
36
|
+
// serializer here to defer to, so the shipped default is the right shape.
|
|
37
|
+
errorFields: defaultErrorFields,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** Build a `Logger` that writes each record to the matching `console` method. */
|
|
41
|
+
export function createConsoleLogger() {
|
|
42
|
+
return makeConsoleLogger([], {});
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a `Logger` that discards every record. This is what the harness falls
|
|
46
|
+
* back to when an embedder injects no logger, so internal call sites can log
|
|
47
|
+
* unconditionally instead of threading `?.` through every diagnostic.
|
|
48
|
+
*/
|
|
49
|
+
export function createNoopLogger() {
|
|
50
|
+
const noop = {
|
|
51
|
+
debug: () => { },
|
|
52
|
+
info: () => { },
|
|
53
|
+
warn: () => { },
|
|
54
|
+
error: () => { },
|
|
55
|
+
with: () => noop,
|
|
56
|
+
named: () => noop,
|
|
57
|
+
// Still the real mapping, not `{}`: callers spread the result into fields
|
|
58
|
+
// they may pass elsewhere, so it must stay honest even when nothing is emitted.
|
|
59
|
+
errorFields: defaultErrorFields,
|
|
60
|
+
};
|
|
61
|
+
return noop;
|
|
62
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness's operational logging seam.
|
|
3
|
+
*
|
|
4
|
+
* The harness ships as a published package embedded by hosts it does not
|
|
5
|
+
* control, so it declares the shape it logs through and lets the embedder bind
|
|
6
|
+
* a realization at its composition root — the same inversion the capability
|
|
7
|
+
* seams use. Naming a concrete logger here would push that library onto every
|
|
8
|
+
* consumer.
|
|
9
|
+
*
|
|
10
|
+
* The alternative that reads as obvious — take the embedder's logger type
|
|
11
|
+
* directly — was rejected for exactly that reason. `@opentelemetry/api-logs`
|
|
12
|
+
* was the other candidate, being the nearest thing to a neutral standard, but
|
|
13
|
+
* it is still pre-1.0 and would force consumers into an OTel SDK to read a log
|
|
14
|
+
* line. JS has no `log/slog` equivalent to conform to, so the harness defines
|
|
15
|
+
* its own minimum and adapts at the edges.
|
|
16
|
+
*
|
|
17
|
+
* Diagnostics MUST go through here, never `console`: an embedder whose UI owns
|
|
18
|
+
* stdout discards console output entirely, which is silent data loss precisely
|
|
19
|
+
* when something has gone wrong. `createConsoleLogger` (`./console-logger.ts`)
|
|
20
|
+
* is the one sanctioned console site.
|
|
21
|
+
*/
|
|
22
|
+
/** Severity of a log record, ascending. */
|
|
23
|
+
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
24
|
+
/**
|
|
25
|
+
* Structured key/value context attached to a record. Identifiers ride here
|
|
26
|
+
* rather than interpolated into the message so records stay queryable and any
|
|
27
|
+
* field-name redaction the host configures can actually match them.
|
|
28
|
+
*/
|
|
29
|
+
export type LogFields = Record<string, unknown>;
|
|
30
|
+
/**
|
|
31
|
+
* Operational logger. Message-first (`error(msg, fields?)`) — the convention
|
|
32
|
+
* `slog`, winston, and `console` share. Object-first loggers exist (pino,
|
|
33
|
+
* bunyan) and an embedder using one adapts at its composition root; shaping
|
|
34
|
+
* this contract to match one of them would model the published surface on a
|
|
35
|
+
* single consumer's choice.
|
|
36
|
+
*/
|
|
37
|
+
export interface Logger {
|
|
38
|
+
debug(msg: string, fields?: LogFields): void;
|
|
39
|
+
info(msg: string, fields?: LogFields): void;
|
|
40
|
+
warn(msg: string, fields?: LogFields): void;
|
|
41
|
+
error(msg: string, fields?: LogFields): void;
|
|
42
|
+
/**
|
|
43
|
+
* Bind `fields` onto every record the returned logger emits — `slog`'s
|
|
44
|
+
* `With`. Lets a workflow body fix `{ runId, stepId }` once instead of
|
|
45
|
+
* repeating it at each call site.
|
|
46
|
+
*/
|
|
47
|
+
with(fields: LogFields): Logger;
|
|
48
|
+
/**
|
|
49
|
+
* Bind a namespace onto every record the returned logger emits, rendered as
|
|
50
|
+
* a bracketed prefix on the message: `named("boot")` turns
|
|
51
|
+
* `info("harness booted")` into `[boot] harness booted`.
|
|
52
|
+
*
|
|
53
|
+
* The namespace is a first-class part of a record, so it is declared once at
|
|
54
|
+
* the seam rather than hand-typed into every message string — hand-typing is
|
|
55
|
+
* what lets one module drift to `[Boot]` or drop the tag entirely. Nesting
|
|
56
|
+
* composes with a dot (`named("post-step").named("reconcile")` →
|
|
57
|
+
* `[post-step.reconcile]`), matching the tag convention already in use.
|
|
58
|
+
*/
|
|
59
|
+
named(name: string): Logger;
|
|
60
|
+
/**
|
|
61
|
+
* Normalize a thrown value into fields to spread onto a record:
|
|
62
|
+
* `logger.error("step failure", { ...logger.errorFields(err), stepId })`.
|
|
63
|
+
*
|
|
64
|
+
* Every catch block would otherwise hand-roll
|
|
65
|
+
* `err instanceof Error ? err.message : String(err)` — repetition that
|
|
66
|
+
* reliably drifts, and that silently drops the stack from whichever site
|
|
67
|
+
* forgot it.
|
|
68
|
+
*
|
|
69
|
+
* It lives on the interface rather than as a free function because how an
|
|
70
|
+
* error is best represented is a property of the SINK, not of the harness: a
|
|
71
|
+
* pino-backed realization may defer to pino's `err` serializer, an
|
|
72
|
+
* OTel-backed one to the `exception.*` semantic conventions. Owning the
|
|
73
|
+
* mapping here would foreclose both. `defaultErrorFields` is the shipped
|
|
74
|
+
* implementation — a realization with no opinion just references it.
|
|
75
|
+
*/
|
|
76
|
+
errorFields(err: unknown): LogFields;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* The default `Logger.errorFields`: `err` carries the message, `stack` rides as
|
|
80
|
+
* its own field when there is one.
|
|
81
|
+
*
|
|
82
|
+
* A raw `Error` is deliberately not passed through as a field value: it is
|
|
83
|
+
* assignable to `unknown` and so type-checks, but `JSON.stringify(new Error())`
|
|
84
|
+
* is `{}` — a sink that serializes to JSON would drop the message entirely,
|
|
85
|
+
* which is the silent-loss failure this seam exists to prevent. Normalizing to
|
|
86
|
+
* strings means any sink gets something printable regardless of its serializer.
|
|
87
|
+
*/
|
|
88
|
+
export declare function defaultErrorFields(err: unknown): LogFields;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The harness's operational logging seam.
|
|
3
|
+
*
|
|
4
|
+
* The harness ships as a published package embedded by hosts it does not
|
|
5
|
+
* control, so it declares the shape it logs through and lets the embedder bind
|
|
6
|
+
* a realization at its composition root — the same inversion the capability
|
|
7
|
+
* seams use. Naming a concrete logger here would push that library onto every
|
|
8
|
+
* consumer.
|
|
9
|
+
*
|
|
10
|
+
* The alternative that reads as obvious — take the embedder's logger type
|
|
11
|
+
* directly — was rejected for exactly that reason. `@opentelemetry/api-logs`
|
|
12
|
+
* was the other candidate, being the nearest thing to a neutral standard, but
|
|
13
|
+
* it is still pre-1.0 and would force consumers into an OTel SDK to read a log
|
|
14
|
+
* line. JS has no `log/slog` equivalent to conform to, so the harness defines
|
|
15
|
+
* its own minimum and adapts at the edges.
|
|
16
|
+
*
|
|
17
|
+
* Diagnostics MUST go through here, never `console`: an embedder whose UI owns
|
|
18
|
+
* stdout discards console output entirely, which is silent data loss precisely
|
|
19
|
+
* when something has gone wrong. `createConsoleLogger` (`./console-logger.ts`)
|
|
20
|
+
* is the one sanctioned console site.
|
|
21
|
+
*/
|
|
22
|
+
/**
|
|
23
|
+
* The default `Logger.errorFields`: `err` carries the message, `stack` rides as
|
|
24
|
+
* its own field when there is one.
|
|
25
|
+
*
|
|
26
|
+
* A raw `Error` is deliberately not passed through as a field value: it is
|
|
27
|
+
* assignable to `unknown` and so type-checks, but `JSON.stringify(new Error())`
|
|
28
|
+
* is `{}` — a sink that serializes to JSON would drop the message entirely,
|
|
29
|
+
* which is the silent-loss failure this seam exists to prevent. Normalizing to
|
|
30
|
+
* strings means any sink gets something printable regardless of its serializer.
|
|
31
|
+
*/
|
|
32
|
+
export function defaultErrorFields(err) {
|
|
33
|
+
if (err instanceof Error) {
|
|
34
|
+
return err.stack ? { err: err.message, stack: err.stack } : { err: err.message };
|
|
35
|
+
}
|
|
36
|
+
return { err: String(err) };
|
|
37
|
+
}
|
package/dist/lib/otel.d.ts
CHANGED
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
* so inbound/outbound trace-context propagation across those paths is
|
|
16
16
|
* not currently wired.
|
|
17
17
|
*/
|
|
18
|
+
import type { Logger } from "./logger.js";
|
|
18
19
|
/**
|
|
19
20
|
* Initialize the OpenTelemetry SDK. Must be called before any code that
|
|
20
21
|
* calls trace.getTracer() or metrics.getMeter().
|
|
21
22
|
*
|
|
22
23
|
* Safe to call multiple times — only the first call has effect.
|
|
23
24
|
*/
|
|
24
|
-
export declare function initOtel(): void;
|
|
25
|
+
export declare function initOtel(injected?: Logger): void;
|
|
25
26
|
/**
|
|
26
27
|
* Flush and shut down the OTel exporters. Called by the graceful-shutdown
|
|
27
28
|
* sequence so in-flight batch spans/metrics make it to the collector
|
package/dist/lib/otel.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* not currently wired.
|
|
17
17
|
*/
|
|
18
18
|
import { propagation, metrics, trace } from "@opentelemetry/api";
|
|
19
|
+
import { createNoopLogger } from "./console-logger.js";
|
|
19
20
|
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
20
21
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
21
22
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
@@ -33,10 +34,11 @@ let registeredMeterProvider;
|
|
|
33
34
|
*
|
|
34
35
|
* Safe to call multiple times — only the first call has effect.
|
|
35
36
|
*/
|
|
36
|
-
export function initOtel() {
|
|
37
|
+
export function initOtel(injected) {
|
|
37
38
|
if (initialized)
|
|
38
39
|
return;
|
|
39
40
|
initialized = true;
|
|
41
|
+
const logger = (injected ?? createNoopLogger()).named("otel");
|
|
40
42
|
// ── W3C Trace Context propagation ──────────────────────────────────
|
|
41
43
|
propagation.setGlobalPropagator(new W3CTraceContextPropagator());
|
|
42
44
|
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT;
|
|
@@ -65,8 +67,11 @@ export function initOtel() {
|
|
|
65
67
|
const testSpan = testTracer.startSpan("init-check");
|
|
66
68
|
const ctx = testSpan.spanContext();
|
|
67
69
|
const isNoop = ctx.spanId === "0000000000000000";
|
|
68
|
-
console.log
|
|
69
|
-
|
|
70
|
+
// A `console.log` here is what forced the CLI embedder to pass
|
|
71
|
+
// `initTelemetry: () => {}` — its TUI owns stdout, so the banner corrupted
|
|
72
|
+
// the screen and the whole of the harness's traces + metrics were switched
|
|
73
|
+
// off to avoid it. At debug through the injected seam it costs nothing.
|
|
74
|
+
logger.debug("TracerProvider registered", { spanId: ctx.spanId, noop: isNoop, endpoint: endpoint ?? null, processors: spanProcessors.length });
|
|
70
75
|
testSpan.end();
|
|
71
76
|
}
|
|
72
77
|
// ── Metrics ────────────────────────────────────────────────────────
|
package/dist/runtime/boot.d.ts
CHANGED
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* once shutdown starts, exiting takes precedence (see `runShutdownSequence`).
|
|
32
32
|
*/
|
|
33
33
|
import type { Pool } from "pg";
|
|
34
|
-
import type
|
|
34
|
+
import type { Logger } from "../lib/logger.js";
|
|
35
35
|
import { type CoreRuntime, type CoreRuntimeDeps } from "./assemble.js";
|
|
36
36
|
import { type ConnectionBudgetConfig } from "./connection-budget.js";
|
|
37
37
|
import { type DbosConfig } from "./dbos.js";
|
|
@@ -46,7 +46,7 @@ export interface BootHarnessDeps {
|
|
|
46
46
|
readonly dbos: DbosConfig;
|
|
47
47
|
/** Connection-budget guard config. */
|
|
48
48
|
readonly connectionBudget: ConnectionBudgetConfig;
|
|
49
|
-
readonly logger:
|
|
49
|
+
readonly logger: Logger;
|
|
50
50
|
/**
|
|
51
51
|
* Host-specific work that must run AFTER workflow registration and BEFORE
|
|
52
52
|
* DBOS launch (scheduled sweeps, ephemeral reap, agent-switch install). The
|
package/dist/runtime/boot.js
CHANGED
|
@@ -49,7 +49,7 @@ export async function bootHarness(deps) {
|
|
|
49
49
|
const runtime = assembleCoreRuntime(core);
|
|
50
50
|
await deps.beforeLaunch?.();
|
|
51
51
|
await launchDbos({ config: deps.dbos, logger });
|
|
52
|
-
logger.info({ executorId: deps.dbos.executorId }
|
|
52
|
+
logger.named("boot").info("harness booted", { executorId: deps.dbos.executorId });
|
|
53
53
|
const shutdown = (signal) => runShutdownSequence({
|
|
54
54
|
signal,
|
|
55
55
|
logger,
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* misconfigured pod fails boot loudly instead of degrading under load.
|
|
22
22
|
*/
|
|
23
23
|
import type { Pool } from "pg";
|
|
24
|
-
import type
|
|
24
|
+
import type { Logger } from "../lib/logger.js";
|
|
25
25
|
/**
|
|
26
26
|
* Narrow config slice the budget guard reads — only the optional app-pool
|
|
27
27
|
* size override. Composition roots map `Env.DB_POOL_MAX` onto this.
|
|
@@ -31,7 +31,7 @@ export interface ConnectionBudgetConfig {
|
|
|
31
31
|
}
|
|
32
32
|
export interface AssertConnectionBudgetOptions {
|
|
33
33
|
pool: Pool;
|
|
34
|
-
logger:
|
|
34
|
+
logger: Logger;
|
|
35
35
|
config: ConnectionBudgetConfig;
|
|
36
36
|
}
|
|
37
37
|
/**
|
|
@@ -38,15 +38,17 @@ export async function assertConnectionBudget({ pool, logger, config }) {
|
|
|
38
38
|
`Lower DB_POOL_MAX or raise max_connections.`);
|
|
39
39
|
}
|
|
40
40
|
const supportedReplicas = Math.floor((maxConnections - SAFETY_MARGIN) / podFootprint);
|
|
41
|
-
logger
|
|
41
|
+
logger
|
|
42
|
+
.named("boot")
|
|
43
|
+
.info(`postgres connection budget OK — this cluster's max_connections ` +
|
|
44
|
+
`supports ~${supportedReplicas} cortex replicas; size replicas/pooler accordingly`, {
|
|
42
45
|
maxConnections,
|
|
43
46
|
appPoolSize,
|
|
44
47
|
dbosPoolSize,
|
|
45
48
|
podFootprint,
|
|
46
49
|
safetyMargin: SAFETY_MARGIN,
|
|
47
50
|
supportedReplicas,
|
|
48
|
-
}
|
|
49
|
-
`supports ~${supportedReplicas} cortex replicas; size replicas/pooler accordingly`);
|
|
51
|
+
});
|
|
50
52
|
return {
|
|
51
53
|
maxConnections,
|
|
52
54
|
appPoolSize,
|
package/dist/runtime/dbos.d.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* See `openspec/changes/harness-dbos-runtime`.
|
|
14
14
|
*/
|
|
15
|
-
import type pino from "pino";
|
|
16
15
|
import type { Pool } from "pg";
|
|
16
|
+
import type { Logger } from "../lib/logger.js";
|
|
17
17
|
/**
|
|
18
18
|
* Narrow config slice the DBOS bootstrap reads. Composition roots map their
|
|
19
19
|
* validated `Env` onto this and pass the executor id in (the harness reads no env).
|
|
@@ -50,9 +50,9 @@ interface State {
|
|
|
50
50
|
* must resolve before the HTTP listener accepts traffic (otherwise the
|
|
51
51
|
* readiness probe could 200 against a runtime that can't own workflows).
|
|
52
52
|
*/
|
|
53
|
-
export declare function launchDbos({ config, logger }: {
|
|
53
|
+
export declare function launchDbos({ config, logger: injected }: {
|
|
54
54
|
config: DbosConfig;
|
|
55
|
-
logger:
|
|
55
|
+
logger: Logger;
|
|
56
56
|
}): Promise<void>;
|
|
57
57
|
/**
|
|
58
58
|
* Shut DBOS down. In-flight workflows are marked recoverable so another
|
|
@@ -63,8 +63,8 @@ export declare function launchDbos({ config, logger }: {
|
|
|
63
63
|
* Must run after HTTP has drained (so no inbound request is orphaned) and
|
|
64
64
|
* before the application pool closes (DBOS needs the system DB).
|
|
65
65
|
*/
|
|
66
|
-
export declare function shutdownDbos({ logger }: {
|
|
67
|
-
logger:
|
|
66
|
+
export declare function shutdownDbos({ logger: injected }: {
|
|
67
|
+
logger: Logger;
|
|
68
68
|
}): Promise<void>;
|
|
69
69
|
/**
|
|
70
70
|
* Cancel any `ephemeral:`-prefixed PENDING workflow this executor owns —
|
|
@@ -79,9 +79,9 @@ export declare function shutdownDbos({ logger }: {
|
|
|
79
79
|
* pre-launch timing, and this is the sole module that owns DBOS. The system
|
|
80
80
|
* DB is the same database as the app pool, so the pool reaches it directly.
|
|
81
81
|
*/
|
|
82
|
-
export declare function sweepEphemeralWorkflows({ pool, logger, executorId, }: {
|
|
82
|
+
export declare function sweepEphemeralWorkflows({ pool, logger: injectedLogger, executorId, }: {
|
|
83
83
|
pool: Pool;
|
|
84
|
-
logger:
|
|
84
|
+
logger: Logger;
|
|
85
85
|
/** Stable per-pod executor id — must match `launchDbos`'s `executorId`. */
|
|
86
86
|
executorId: string;
|
|
87
87
|
}): Promise<void>;
|
package/dist/runtime/dbos.js
CHANGED
|
@@ -53,9 +53,10 @@ function systemDatabaseUrl(config) {
|
|
|
53
53
|
* must resolve before the HTTP listener accepts traffic (otherwise the
|
|
54
54
|
* readiness probe could 200 against a runtime that can't own workflows).
|
|
55
55
|
*/
|
|
56
|
-
export async function launchDbos({ config, logger }) {
|
|
56
|
+
export async function launchDbos({ config, logger: injected }) {
|
|
57
57
|
if (state.launched)
|
|
58
58
|
return;
|
|
59
|
+
const logger = injected.named("dbos");
|
|
59
60
|
const executorID = config.executorId;
|
|
60
61
|
const adminPort = parseInt(config.adminPort, 10);
|
|
61
62
|
DBOS.setConfig({
|
|
@@ -71,12 +72,12 @@ export async function launchDbos({ config, logger }) {
|
|
|
71
72
|
await DBOS.launch();
|
|
72
73
|
state.launched = true;
|
|
73
74
|
state.recoveryStarted = true;
|
|
74
|
-
logger.info({
|
|
75
|
+
logger.info("launched", {
|
|
75
76
|
executorID,
|
|
76
77
|
applicationVersion: config.applicationVersion,
|
|
77
78
|
adminPort,
|
|
78
79
|
durationMs: Math.round(performance.now() - start),
|
|
79
|
-
}
|
|
80
|
+
});
|
|
80
81
|
}
|
|
81
82
|
/**
|
|
82
83
|
* Shut DBOS down. In-flight workflows are marked recoverable so another
|
|
@@ -87,19 +88,20 @@ export async function launchDbos({ config, logger }) {
|
|
|
87
88
|
* Must run after HTTP has drained (so no inbound request is orphaned) and
|
|
88
89
|
* before the application pool closes (DBOS needs the system DB).
|
|
89
90
|
*/
|
|
90
|
-
export async function shutdownDbos({ logger }) {
|
|
91
|
+
export async function shutdownDbos({ logger: injected }) {
|
|
91
92
|
if (!state.launched)
|
|
92
93
|
return;
|
|
94
|
+
const logger = injected.named("dbos");
|
|
93
95
|
const start = performance.now();
|
|
94
96
|
try {
|
|
95
97
|
await DBOS.shutdown();
|
|
96
|
-
logger.info({ durationMs: Math.round(performance.now() - start) }
|
|
98
|
+
logger.info("shutdown", { durationMs: Math.round(performance.now() - start) });
|
|
97
99
|
}
|
|
98
100
|
catch (err) {
|
|
99
|
-
logger.error({
|
|
100
|
-
|
|
101
|
+
logger.error("shutdown failed", {
|
|
102
|
+
...logger.errorFields(err),
|
|
101
103
|
durationMs: Math.round(performance.now() - start),
|
|
102
|
-
}
|
|
104
|
+
});
|
|
103
105
|
}
|
|
104
106
|
finally {
|
|
105
107
|
state.launched = false;
|
|
@@ -119,7 +121,8 @@ export async function shutdownDbos({ logger }) {
|
|
|
119
121
|
* pre-launch timing, and this is the sole module that owns DBOS. The system
|
|
120
122
|
* DB is the same database as the app pool, so the pool reaches it directly.
|
|
121
123
|
*/
|
|
122
|
-
export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
|
|
124
|
+
export async function sweepEphemeralWorkflows({ pool, logger: injectedLogger, executorId, }) {
|
|
125
|
+
const logger = injectedLogger.named("dbos");
|
|
123
126
|
const executorID = executorId;
|
|
124
127
|
try {
|
|
125
128
|
const { rowCount } = await pool.query({
|
|
@@ -131,7 +134,7 @@ export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
|
|
|
131
134
|
values: [Date.now(), executorID],
|
|
132
135
|
});
|
|
133
136
|
if (rowCount && rowCount > 0) {
|
|
134
|
-
logger.info({ executorID, swept: rowCount }
|
|
137
|
+
logger.info("swept orphaned ephemeral workflows", { executorID, swept: rowCount });
|
|
135
138
|
}
|
|
136
139
|
}
|
|
137
140
|
catch (err) {
|
|
@@ -139,7 +142,7 @@ export async function sweepEphemeralWorkflows({ pool, logger, executorId, }) {
|
|
|
139
142
|
if (err && typeof err === "object" && "code" in err && err.code === "42P01") {
|
|
140
143
|
return;
|
|
141
144
|
}
|
|
142
|
-
logger.error({ executorID,
|
|
145
|
+
logger.error("ephemeral sweep failed", { executorID, ...logger.errorFields(err) });
|
|
143
146
|
}
|
|
144
147
|
}
|
|
145
148
|
/** Snapshot of DBOS lifecycle state — read by the readiness probe. */
|
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
* swallowed — once shutdown has started, exiting takes precedence over
|
|
19
19
|
* any single step's failure.
|
|
20
20
|
*/
|
|
21
|
-
import type
|
|
21
|
+
import type { Logger } from "../lib/logger.js";
|
|
22
22
|
export interface ShutdownDeps {
|
|
23
23
|
signal: string;
|
|
24
|
-
logger:
|
|
24
|
+
logger: Logger;
|
|
25
25
|
markDraining: () => void;
|
|
26
26
|
closeHttpServer: () => Promise<void>;
|
|
27
27
|
shutdownDbos: () => Promise<void>;
|
package/dist/runtime/shutdown.js
CHANGED
|
@@ -23,12 +23,13 @@ async function safely(logger, step, fn) {
|
|
|
23
23
|
await fn();
|
|
24
24
|
}
|
|
25
25
|
catch (err) {
|
|
26
|
-
logger.error({
|
|
26
|
+
logger.error(`${step} failed`, { ...logger.errorFields(err), step });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
29
|
export async function runShutdownSequence(deps) {
|
|
30
|
-
const {
|
|
31
|
-
logger.
|
|
30
|
+
const { signal } = deps;
|
|
31
|
+
const logger = deps.logger.named("shutdown");
|
|
32
|
+
logger.info("requested", { signal });
|
|
32
33
|
deps.markDraining();
|
|
33
34
|
await safely(logger, "http-drain", deps.closeHttpServer);
|
|
34
35
|
await safely(logger, "dbos-shutdown", deps.shutdownDbos);
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import type { V1Toleration } from "@kubernetes/client-node";
|
|
15
15
|
import type { Pool } from "pg";
|
|
16
|
-
import type
|
|
16
|
+
import type { Logger } from "../lib/logger.js";
|
|
17
17
|
import { type ResourceLimits } from "../config/resource-limits.js";
|
|
18
18
|
import { type ResolveWorkspaceRoot } from "../workspace/paths.js";
|
|
19
19
|
import { clearSandboxRef } from "../state/index.js";
|
|
@@ -87,7 +87,7 @@ export interface CreateSandboxClientConfig {
|
|
|
87
87
|
* (a configured store whose `current` vanished/went incomplete mid-session) is
|
|
88
88
|
* observable rather than a silent mount drop. No-op when unset.
|
|
89
89
|
*/
|
|
90
|
-
logger?:
|
|
90
|
+
logger?: Logger;
|
|
91
91
|
/** Dependency seams (fetch, durable step/sleep, clock, recv, warn sink) forwarded to submit/await. */
|
|
92
92
|
submitDeps?: SubmitExecDeps;
|
|
93
93
|
awaitOptions?: AwaitExecOptions;
|