@inflexa-ai/harness 0.35.2 → 0.35.3
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/dist/lib/otel-spans.d.ts +77 -0
- package/dist/lib/otel-spans.js +123 -0
- package/dist/lib/otel.d.ts +6 -1
- package/dist/lib/otel.js +9 -2
- package/dist/loop/run-agent.js +13 -2
- package/dist/runtime/dbos.d.ts +8 -7
- package/dist/sandbox/await-exec.js +16 -9
- package/dist/sandbox/notification-sweep.js +5 -2
- package/dist/sandbox/reaper.js +5 -2
- package/dist/sandbox/submit-exec.js +6 -3
- package/dist/sandbox/watchdog.js +8 -3
- package/dist/workflows/execute-analysis.js +6 -1
- package/dist/workflows/execute-target-assessment.js +9 -3
- package/package.json +1 -1
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span policy of the harness TracerProvider: which DBOS traces are recorded,
|
|
3
|
+
* which spans reach the exporter, and the names the exported spans carry.
|
|
4
|
+
*
|
|
5
|
+
* DBOS names a workflow span after the registered workflow and a step span
|
|
6
|
+
* after the step function, and it runs each body under its span, so every
|
|
7
|
+
* child (a step, a patched fetch, a pg query) sits under the workflow span.
|
|
8
|
+
* Three shapes of that output are noise in a trace store, and each is stopped
|
|
9
|
+
* in the process, before export, rather than in a collector downstream. The
|
|
10
|
+
* decisions live where the workflows and steps are written; this module holds
|
|
11
|
+
* the mechanism only:
|
|
12
|
+
*
|
|
13
|
+
* - A module that registers a housekeeping workflow declares it untraced with
|
|
14
|
+
* `untracedWorkflow(name)` next to its registration. The sampler drops a
|
|
15
|
+
* root span that carries such a name at creation, and `ParentBased`
|
|
16
|
+
* sampling carries the decision to every child.
|
|
17
|
+
* - A replayed step re-emits its span with `cached=true`, which DBOS sets
|
|
18
|
+
* after the span has started. A sampler cannot see that, so
|
|
19
|
+
* `DbosSpanProcessor` drops the span at `onEnd`.
|
|
20
|
+
* - A step whose DBOS name embeds an id calls `stableSpan` as the first
|
|
21
|
+
* statement of its body. The helper renames the active DBOS span to the
|
|
22
|
+
* stable prefix and puts the id in an `inflexa.*` attribute. The DBOS step
|
|
23
|
+
* name itself is untouched: DBOS compares it against the recorded name on
|
|
24
|
+
* replay and throws `DBOSUnexpectedStepError` on a mismatch, so a rename
|
|
25
|
+
* would break every workflow in flight across a deploy.
|
|
26
|
+
*/
|
|
27
|
+
import { type Attributes, type Context } from "@opentelemetry/api";
|
|
28
|
+
import { type ReadableSpan, type Sampler, type Span, type SpanProcessor } from "@opentelemetry/sdk-trace-base";
|
|
29
|
+
/**
|
|
30
|
+
* Declare a registered DBOS workflow untraced: no root span for it is
|
|
31
|
+
* recorded, and no child span under it. Call it next to the
|
|
32
|
+
* `DBOS.registerWorkflow` that gives the workflow the same name.
|
|
33
|
+
*/
|
|
34
|
+
export declare function untracedWorkflow(name: string): void;
|
|
35
|
+
/**
|
|
36
|
+
* The sampler of the harness TracerProvider. A root span named after an
|
|
37
|
+
* untraced workflow is not recorded; every other root is. A span with a parent
|
|
38
|
+
* takes the parent's decision (the standard `ParentBased` behaviour), so a DBOS
|
|
39
|
+
* step, a fetch, or a pg span under a dropped root is dropped with it, and a
|
|
40
|
+
* remote parent (an inbound `traceparent`) is honoured as usual.
|
|
41
|
+
*/
|
|
42
|
+
export declare function createHarnessSampler(): Sampler;
|
|
43
|
+
/** The plan step id a `compose-step-seed` step carries. */
|
|
44
|
+
export declare const ATTR_INFLEXA_STEP_ID = "inflexa.step_id";
|
|
45
|
+
/** The provider tool-use id of a tool step. */
|
|
46
|
+
export declare const ATTR_INFLEXA_TOOL_USE_ID = "inflexa.tool_use_id";
|
|
47
|
+
/** The exec id (`${workflowId}:${stepId}:${fnId}`) of a sandbox exec step. */
|
|
48
|
+
export declare const ATTR_INFLEXA_EXEC_ID = "inflexa.exec_id";
|
|
49
|
+
/** The 1-based attempt counter of a sandbox poll, pull, or liveness-probe step. */
|
|
50
|
+
export declare const ATTR_INFLEXA_ATTEMPT = "inflexa.attempt";
|
|
51
|
+
/** The per-item key (a ChEMBL or NCT id) of a target-assessment fan-out step. */
|
|
52
|
+
export declare const ATTR_INFLEXA_FANOUT_KEY = "inflexa.fanout_key";
|
|
53
|
+
/**
|
|
54
|
+
* Rename the DBOS span of the step that is running to `name` and put its id
|
|
55
|
+
* in `attributes`. Call it as the first statement of a step body whose DBOS
|
|
56
|
+
* name embeds an id, with that DBOS name as `dbosName`.
|
|
57
|
+
*
|
|
58
|
+
* DBOS runs the body under its span, so with tracing on the active span is
|
|
59
|
+
* the step span. With tracing off DBOS opens no span, and the active span, if
|
|
60
|
+
* there is one, belongs to whoever started the workflow or the chat turn. The
|
|
61
|
+
* helper therefore renames a span only when its name is `dbosName`, and it
|
|
62
|
+
* leaves a missing or non-recording span alone.
|
|
63
|
+
*/
|
|
64
|
+
export declare function stableSpan(dbosName: string, name: string, attributes: Attributes): void;
|
|
65
|
+
/**
|
|
66
|
+
* Sits in front of the exporting processor and forwards every span except a
|
|
67
|
+
* replayed one (`cached=true`), which it drops at `onEnd`.
|
|
68
|
+
*/
|
|
69
|
+
export declare class DbosSpanProcessor implements SpanProcessor {
|
|
70
|
+
private readonly next;
|
|
71
|
+
constructor(next: SpanProcessor);
|
|
72
|
+
onStart(span: Span, parentContext: Context): void;
|
|
73
|
+
onEnding(span: Span): void;
|
|
74
|
+
onEnd(span: ReadableSpan): void;
|
|
75
|
+
forceFlush(): Promise<void>;
|
|
76
|
+
shutdown(): Promise<void>;
|
|
77
|
+
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Span policy of the harness TracerProvider: which DBOS traces are recorded,
|
|
3
|
+
* which spans reach the exporter, and the names the exported spans carry.
|
|
4
|
+
*
|
|
5
|
+
* DBOS names a workflow span after the registered workflow and a step span
|
|
6
|
+
* after the step function, and it runs each body under its span, so every
|
|
7
|
+
* child (a step, a patched fetch, a pg query) sits under the workflow span.
|
|
8
|
+
* Three shapes of that output are noise in a trace store, and each is stopped
|
|
9
|
+
* in the process, before export, rather than in a collector downstream. The
|
|
10
|
+
* decisions live where the workflows and steps are written; this module holds
|
|
11
|
+
* the mechanism only:
|
|
12
|
+
*
|
|
13
|
+
* - A module that registers a housekeeping workflow declares it untraced with
|
|
14
|
+
* `untracedWorkflow(name)` next to its registration. The sampler drops a
|
|
15
|
+
* root span that carries such a name at creation, and `ParentBased`
|
|
16
|
+
* sampling carries the decision to every child.
|
|
17
|
+
* - A replayed step re-emits its span with `cached=true`, which DBOS sets
|
|
18
|
+
* after the span has started. A sampler cannot see that, so
|
|
19
|
+
* `DbosSpanProcessor` drops the span at `onEnd`.
|
|
20
|
+
* - A step whose DBOS name embeds an id calls `stableSpan` as the first
|
|
21
|
+
* statement of its body. The helper renames the active DBOS span to the
|
|
22
|
+
* stable prefix and puts the id in an `inflexa.*` attribute. The DBOS step
|
|
23
|
+
* name itself is untouched: DBOS compares it against the recorded name on
|
|
24
|
+
* replay and throws `DBOSUnexpectedStepError` on a mismatch, so a rename
|
|
25
|
+
* would break every workflow in flight across a deploy.
|
|
26
|
+
*/
|
|
27
|
+
import { trace } from "@opentelemetry/api";
|
|
28
|
+
import { ParentBasedSampler, SamplingDecision, } from "@opentelemetry/sdk-trace-base";
|
|
29
|
+
/**
|
|
30
|
+
* Names of the DBOS workflows whose traces are not recorded. A registering
|
|
31
|
+
* module adds its own name at registration, which runs at boot inside
|
|
32
|
+
* `assembleCoreRuntime`, before `DBOS.launch` starts the first workflow. The
|
|
33
|
+
* sampler reads the set on every root span, so the set is complete before it
|
|
34
|
+
* is consulted, and a name added later applies from the next root span on.
|
|
35
|
+
*/
|
|
36
|
+
const untracedWorkflowNames = new Set();
|
|
37
|
+
/**
|
|
38
|
+
* Declare a registered DBOS workflow untraced: no root span for it is
|
|
39
|
+
* recorded, and no child span under it. Call it next to the
|
|
40
|
+
* `DBOS.registerWorkflow` that gives the workflow the same name.
|
|
41
|
+
*/
|
|
42
|
+
export function untracedWorkflow(name) {
|
|
43
|
+
untracedWorkflowNames.add(name);
|
|
44
|
+
}
|
|
45
|
+
/** Root decision only: `NOT_RECORD` for an untraced workflow, `RECORD_AND_SAMPLED` for every other root. */
|
|
46
|
+
class UntracedWorkflowRootSampler {
|
|
47
|
+
shouldSample(_context, _traceId, spanName) {
|
|
48
|
+
return {
|
|
49
|
+
decision: untracedWorkflowNames.has(spanName) ? SamplingDecision.NOT_RECORD : SamplingDecision.RECORD_AND_SAMPLED,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
toString() {
|
|
53
|
+
return "UntracedWorkflowRootSampler";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The sampler of the harness TracerProvider. A root span named after an
|
|
58
|
+
* untraced workflow is not recorded; every other root is. A span with a parent
|
|
59
|
+
* takes the parent's decision (the standard `ParentBased` behaviour), so a DBOS
|
|
60
|
+
* step, a fetch, or a pg span under a dropped root is dropped with it, and a
|
|
61
|
+
* remote parent (an inbound `traceparent`) is honoured as usual.
|
|
62
|
+
*/
|
|
63
|
+
export function createHarnessSampler() {
|
|
64
|
+
return new ParentBasedSampler({ root: new UntracedWorkflowRootSampler() });
|
|
65
|
+
}
|
|
66
|
+
/** The plan step id a `compose-step-seed` step carries. */
|
|
67
|
+
export const ATTR_INFLEXA_STEP_ID = "inflexa.step_id";
|
|
68
|
+
/** The provider tool-use id of a tool step. */
|
|
69
|
+
export const ATTR_INFLEXA_TOOL_USE_ID = "inflexa.tool_use_id";
|
|
70
|
+
/** The exec id (`${workflowId}:${stepId}:${fnId}`) of a sandbox exec step. */
|
|
71
|
+
export const ATTR_INFLEXA_EXEC_ID = "inflexa.exec_id";
|
|
72
|
+
/** The 1-based attempt counter of a sandbox poll, pull, or liveness-probe step. */
|
|
73
|
+
export const ATTR_INFLEXA_ATTEMPT = "inflexa.attempt";
|
|
74
|
+
/** The per-item key (a ChEMBL or NCT id) of a target-assessment fan-out step. */
|
|
75
|
+
export const ATTR_INFLEXA_FANOUT_KEY = "inflexa.fanout_key";
|
|
76
|
+
/**
|
|
77
|
+
* Rename the DBOS span of the step that is running to `name` and put its id
|
|
78
|
+
* in `attributes`. Call it as the first statement of a step body whose DBOS
|
|
79
|
+
* name embeds an id, with that DBOS name as `dbosName`.
|
|
80
|
+
*
|
|
81
|
+
* DBOS runs the body under its span, so with tracing on the active span is
|
|
82
|
+
* the step span. With tracing off DBOS opens no span, and the active span, if
|
|
83
|
+
* there is one, belongs to whoever started the workflow or the chat turn. The
|
|
84
|
+
* helper therefore renames a span only when its name is `dbosName`, and it
|
|
85
|
+
* leaves a missing or non-recording span alone.
|
|
86
|
+
*/
|
|
87
|
+
export function stableSpan(dbosName, name, attributes) {
|
|
88
|
+
const span = trace.getActiveSpan();
|
|
89
|
+
if (span === undefined || !span.isRecording())
|
|
90
|
+
return;
|
|
91
|
+
// The API `Span` has no name accessor; the SDK span the provider hands out does.
|
|
92
|
+
if (span.name !== dbosName)
|
|
93
|
+
return;
|
|
94
|
+
span.updateName(name);
|
|
95
|
+
span.setAttributes(attributes);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Sits in front of the exporting processor and forwards every span except a
|
|
99
|
+
* replayed one (`cached=true`), which it drops at `onEnd`.
|
|
100
|
+
*/
|
|
101
|
+
export class DbosSpanProcessor {
|
|
102
|
+
next;
|
|
103
|
+
constructor(next) {
|
|
104
|
+
this.next = next;
|
|
105
|
+
}
|
|
106
|
+
onStart(span, parentContext) {
|
|
107
|
+
this.next.onStart(span, parentContext);
|
|
108
|
+
}
|
|
109
|
+
onEnding(span) {
|
|
110
|
+
this.next.onEnding?.(span);
|
|
111
|
+
}
|
|
112
|
+
onEnd(span) {
|
|
113
|
+
if (span.attributes.cached === true)
|
|
114
|
+
return;
|
|
115
|
+
this.next.onEnd(span);
|
|
116
|
+
}
|
|
117
|
+
forceFlush() {
|
|
118
|
+
return this.next.forceFlush();
|
|
119
|
+
}
|
|
120
|
+
shutdown() {
|
|
121
|
+
return this.next.shutdown();
|
|
122
|
+
}
|
|
123
|
+
}
|
package/dist/lib/otel.d.ts
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* so the bundler cannot tree-shake it (side-effect-only imports get dropped).
|
|
6
6
|
*
|
|
7
7
|
* Traces: NodeTracerProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT
|
|
8
|
-
* is set.
|
|
8
|
+
* is set. The provider carries the span policy of otel-spans.ts: the
|
|
9
|
+
* root span of a workflow declared with `untracedWorkflow` is not
|
|
10
|
+
* recorded (and its children with it), a replayed `cached=true` span
|
|
11
|
+
* is dropped before export, and a step body that calls `stableSpan`
|
|
12
|
+
* is exported under a stable name with its id in an `inflexa.*`
|
|
13
|
+
* attribute.
|
|
9
14
|
*
|
|
10
15
|
* Metrics: MeterProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is
|
|
11
16
|
* set and OTEL_METRICS_EXPORTER is not `none`. The export interval is
|
package/dist/lib/otel.js
CHANGED
|
@@ -5,7 +5,12 @@
|
|
|
5
5
|
* so the bundler cannot tree-shake it (side-effect-only imports get dropped).
|
|
6
6
|
*
|
|
7
7
|
* Traces: NodeTracerProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT
|
|
8
|
-
* is set.
|
|
8
|
+
* is set. The provider carries the span policy of otel-spans.ts: the
|
|
9
|
+
* root span of a workflow declared with `untracedWorkflow` is not
|
|
10
|
+
* recorded (and its children with it), a replayed `cached=true` span
|
|
11
|
+
* is dropped before export, and a step body that calls `stableSpan`
|
|
12
|
+
* is exported under a stable name with its id in an `inflexa.*`
|
|
13
|
+
* attribute.
|
|
9
14
|
*
|
|
10
15
|
* Metrics: MeterProvider exports to OTLP when OTEL_EXPORTER_OTLP_ENDPOINT is
|
|
11
16
|
* set and OTEL_METRICS_EXPORTER is not `none`. The export interval is
|
|
@@ -25,6 +30,7 @@
|
|
|
25
30
|
*/
|
|
26
31
|
import { context, propagation, metrics, trace } from "@opentelemetry/api";
|
|
27
32
|
import { createNoopLogger } from "./console-logger.js";
|
|
33
|
+
import { createHarnessSampler, DbosSpanProcessor } from "./otel-spans.js";
|
|
28
34
|
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
29
35
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
30
36
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-http";
|
|
@@ -92,10 +98,11 @@ export function initOtel(options = {}) {
|
|
|
92
98
|
const spanProcessors = [];
|
|
93
99
|
if (endpoint) {
|
|
94
100
|
const base = endpoint.replace(/\/+$/, "");
|
|
95
|
-
spanProcessors.push(new BatchSpanProcessor(new OTLPTraceExporter({ url: `${base}/v1/traces` })));
|
|
101
|
+
spanProcessors.push(new DbosSpanProcessor(new BatchSpanProcessor(new OTLPTraceExporter({ url: `${base}/v1/traces` }))));
|
|
96
102
|
}
|
|
97
103
|
const tracerProvider = new NodeTracerProvider({
|
|
98
104
|
resource,
|
|
105
|
+
sampler: createHarnessSampler(),
|
|
99
106
|
spanProcessors,
|
|
100
107
|
});
|
|
101
108
|
tracerProvider.register();
|
package/dist/loop/run-agent.js
CHANGED
|
@@ -3,6 +3,7 @@ import { jsonSchema, tool as aiTool, } from "ai";
|
|
|
3
3
|
import { createNoopUsageRecorder } from "../billing/noop-usage-recorder.js";
|
|
4
4
|
import { stripNulCharacters } from "../input-sanitization.js";
|
|
5
5
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
6
|
+
import { ATTR_INFLEXA_TOOL_USE_ID, stableSpan } from "../lib/otel-spans.js";
|
|
6
7
|
import { hintForZodIssue, repairToolInput } from "../lib/zod-issues.js";
|
|
7
8
|
import { markInterruptedMessage, syntheticUserMessage } from "../memory/ai-sdk-message-storage.js";
|
|
8
9
|
import { stripUnansweredToolCalls } from "../memory/tool-call-integrity.js";
|
|
@@ -70,7 +71,13 @@ export async function runAgent(agent, initial, session, opts) {
|
|
|
70
71
|
session,
|
|
71
72
|
signal,
|
|
72
73
|
emit,
|
|
73
|
-
runStep: (name, fn) =>
|
|
74
|
+
runStep: (name, fn) => {
|
|
75
|
+
const stepName = `${formatStepName.tool(tu.toolName, tu.toolCallId)}:${name}`;
|
|
76
|
+
return runStep(stepName, () => {
|
|
77
|
+
stableSpan(stepName, `tool:${tu.toolName}:${name}`, { [ATTR_INFLEXA_TOOL_USE_ID]: tu.toolCallId });
|
|
78
|
+
return fn();
|
|
79
|
+
});
|
|
80
|
+
},
|
|
74
81
|
ask,
|
|
75
82
|
turnUsage,
|
|
76
83
|
});
|
|
@@ -540,7 +547,11 @@ async function dispatchTools(toolUses, toolsById, toolCtx, isFatalLoopError, run
|
|
|
540
547
|
}
|
|
541
548
|
await Promise.all(stepTools.map(({ tu, idx }) => {
|
|
542
549
|
const startedAt = performance.now();
|
|
543
|
-
|
|
550
|
+
const stepName = toolStepName(tu.toolName, tu.toolCallId);
|
|
551
|
+
return runStep(stepName, () => {
|
|
552
|
+
stableSpan(stepName, `tool:${tu.toolName}`, { [ATTR_INFLEXA_TOOL_USE_ID]: tu.toolCallId });
|
|
553
|
+
return dispatchTool(tu, toolsById, toolCtx(tu), isFatalLoopError, encoding);
|
|
554
|
+
}).then((settled) => {
|
|
544
555
|
durations[idx] = elapsedMs(startedAt);
|
|
545
556
|
const dispatched = readDispatchedStep(settled);
|
|
546
557
|
results[idx] = dispatched.result;
|
package/dist/runtime/dbos.d.ts
CHANGED
|
@@ -55,13 +55,14 @@ export interface DbosConfig {
|
|
|
55
55
|
* Attribute names follow the `dbos.*` semantic-convention layout
|
|
56
56
|
* (`otelAttributeFormat: "semconv"`).
|
|
57
57
|
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
58
|
+
* The harness TracerProvider shapes what the SDK emits (`lib/otel-spans.ts`):
|
|
59
|
+
* a workflow declared with `untracedWorkflow` is not recorded, a replayed
|
|
60
|
+
* `cached=true` span is dropped before export, and a step body that calls
|
|
61
|
+
* `stableSpan` is exported under a stable name with its id in an
|
|
62
|
+
* `inflexa.*` attribute. The step name DBOS records is unchanged.
|
|
63
|
+
*
|
|
64
|
+
* Known SDK behavior a host must plan for: a recovered workflow starts a
|
|
65
|
+
* new root trace, with no link to the trace of the original execution.
|
|
65
66
|
*/
|
|
66
67
|
readonly tracingEnabled?: boolean;
|
|
67
68
|
}
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
* varied between replays would desynchronise the recorded function-ID sequence.
|
|
56
56
|
*/
|
|
57
57
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
58
|
+
import { ATTR_INFLEXA_ATTEMPT, ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
|
|
58
59
|
import { digestBody } from "./digest.js";
|
|
59
60
|
import { signCallback, verifyCallback } from "./hmac.js";
|
|
60
61
|
import { createEscalationPolicy, probeLiveness, syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
|
|
@@ -182,9 +183,11 @@ export async function awaitExecCallback(ref, execId, emit, deadlineMs, options =
|
|
|
182
183
|
/** Ask the sandbox directly. Returns null when the exec has not finished. */
|
|
183
184
|
const pull = async () => {
|
|
184
185
|
pullAttempt += 1;
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
const pullStepName = `sandbox.pull-exec-result.${execId}.${pullAttempt}`;
|
|
187
|
+
const pulled = await runStep(() => {
|
|
188
|
+
stableSpan(pullStepName, "sandbox.pull-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pullAttempt });
|
|
189
|
+
return pullExecResult(fetchImpl, ref, execId);
|
|
190
|
+
}, { name: pullStepName });
|
|
188
191
|
if (pulled.kind !== "completed")
|
|
189
192
|
return null;
|
|
190
193
|
// Identical treatment to a pushed completion: the bytes were signed by
|
|
@@ -341,9 +344,11 @@ export async function awaitExecPoll(ref, execId, emit, deadlineMs, options = {})
|
|
|
341
344
|
let probeAttempt = 0;
|
|
342
345
|
while (true) {
|
|
343
346
|
pollAttempt += 1;
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
+
const pollStepName = `sandbox.poll-exec-result.${execId}.${pollAttempt}`;
|
|
348
|
+
const polled = await runStep(() => {
|
|
349
|
+
stableSpan(pollStepName, "sandbox.poll-exec-result", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: pollAttempt });
|
|
350
|
+
return pollExecOnce(fetchImpl, ref, execId, cursor);
|
|
351
|
+
}, { name: pollStepName });
|
|
347
352
|
if (polled.kind === "ok") {
|
|
348
353
|
// Identical treatment to a pushed completion: the bytes were signed by
|
|
349
354
|
// the same secret, so a forgery is as fatal here as it is there.
|
|
@@ -385,9 +390,11 @@ export async function awaitExecPoll(ref, execId, emit, deadlineMs, options = {})
|
|
|
385
390
|
}
|
|
386
391
|
if (isAlive && escalation.onPoll(polled.kind === "ok" ? "ok" : "unavailable")) {
|
|
387
392
|
probeAttempt += 1;
|
|
388
|
-
const
|
|
389
|
-
|
|
390
|
-
|
|
393
|
+
const probeStepName = `sandbox.probe-liveness.${execId}.${probeAttempt}`;
|
|
394
|
+
const verdict = await runStep(() => {
|
|
395
|
+
stableSpan(probeStepName, "sandbox.probe-liveness", { [ATTR_INFLEXA_EXEC_ID]: execId, [ATTR_INFLEXA_ATTEMPT]: probeAttempt });
|
|
396
|
+
return probeLiveness(isAlive, ref);
|
|
397
|
+
}, { name: probeStepName });
|
|
391
398
|
// Only an observably dead machine fails the exec — the result lives
|
|
392
399
|
// in that machine and nowhere else, so it is unrecoverable and the
|
|
393
400
|
// synthetic failure races nothing. `alive` (live-but-slow exec,
|
|
@@ -18,7 +18,9 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
20
20
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
21
|
+
import { untracedWorkflow } from "../lib/otel-spans.js";
|
|
21
22
|
const SWEEP_CRON = "0 */5 * * * *";
|
|
23
|
+
const SWEEP_WORKFLOW = "dbos-notifications-sweep";
|
|
22
24
|
const DELETE_BATCH_LIMIT = 10_000;
|
|
23
25
|
export async function sweepStaleNotifications(deps) {
|
|
24
26
|
const logger = (deps.logger ?? createNoopLogger()).named("notification-sweep");
|
|
@@ -68,9 +70,10 @@ export function registerNotificationSweep(deps) {
|
|
|
68
70
|
// a workflow registration. Register, then schedule the same callable.
|
|
69
71
|
const sweep = DBOS.registerWorkflow(async () => {
|
|
70
72
|
await sweepStaleNotifications({ deleteStale, logger: deps.logger });
|
|
71
|
-
}, { name:
|
|
73
|
+
}, { name: SWEEP_WORKFLOW });
|
|
74
|
+
untracedWorkflow(SWEEP_WORKFLOW);
|
|
72
75
|
DBOS.registerScheduled(sweep, {
|
|
73
|
-
name:
|
|
76
|
+
name: SWEEP_WORKFLOW,
|
|
74
77
|
crontab: SWEEP_CRON,
|
|
75
78
|
});
|
|
76
79
|
}
|
package/dist/sandbox/reaper.js
CHANGED
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
23
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
24
|
+
import { untracedWorkflow } from "../lib/otel-spans.js";
|
|
24
25
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
25
26
|
import { reconcileReapedSandbox } from "../state/active-sandboxes.js";
|
|
26
27
|
const REAPER_CRON = "0 */5 * * * *";
|
|
28
|
+
const REAPER_WORKFLOW = "sandbox-reaper";
|
|
27
29
|
const DEFAULT_GRACE_MS = 10 * 60_000;
|
|
28
30
|
/**
|
|
29
31
|
* Workflow statuses that mean "still owns its sandbox — never reap". Must
|
|
@@ -152,9 +154,10 @@ export function registerSandboxReaper(deps) {
|
|
|
152
154
|
graceMs: deps.graceMs,
|
|
153
155
|
logger: deps.logger,
|
|
154
156
|
}), { name: "sandbox-reaper-sweep" });
|
|
155
|
-
}, { name:
|
|
157
|
+
}, { name: REAPER_WORKFLOW });
|
|
158
|
+
untracedWorkflow(REAPER_WORKFLOW);
|
|
156
159
|
DBOS.registerScheduled(reaper, {
|
|
157
|
-
name:
|
|
160
|
+
name: REAPER_WORKFLOW,
|
|
158
161
|
crontab: REAPER_CRON,
|
|
159
162
|
});
|
|
160
163
|
}
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* untouched.
|
|
18
18
|
*/
|
|
19
19
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
20
|
+
import { ATTR_INFLEXA_EXEC_ID, stableSpan } from "../lib/otel-spans.js";
|
|
20
21
|
import { EXEC_STREAM_BYTE_CAP } from "../tools/workspace/result-bounds.js";
|
|
21
22
|
import { signCallback } from "./hmac.js";
|
|
22
23
|
const SUBMIT_HTTP_TIMEOUT_MS = 30_000;
|
|
@@ -71,7 +72,9 @@ export async function submitExec(ref, body, deps = {}) {
|
|
|
71
72
|
stdoutByteCap: body.stdoutByteCap ?? deps.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP,
|
|
72
73
|
stderrByteCap: body.stderrByteCap ?? deps.execStreamByteCap ?? EXEC_STREAM_BYTE_CAP,
|
|
73
74
|
};
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const stepName = `sandbox.submit-exec.${body.execId}`;
|
|
76
|
+
await runStep(() => {
|
|
77
|
+
stableSpan(stepName, "sandbox.submit-exec", { [ATTR_INFLEXA_EXEC_ID]: body.execId });
|
|
78
|
+
return postExec(fetchImpl, ref, capped);
|
|
79
|
+
}, { name: stepName });
|
|
77
80
|
}
|
package/dist/sandbox/watchdog.js
CHANGED
|
@@ -21,11 +21,14 @@
|
|
|
21
21
|
import { createHash } from "node:crypto";
|
|
22
22
|
import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
23
23
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
24
|
+
import { untracedWorkflow } from "../lib/otel-spans.js";
|
|
24
25
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
25
26
|
import { workflowIdFromExec } from "./exec-id.js";
|
|
26
27
|
import { syntheticFailureReason, syntheticFailureResult } from "./liveness.js";
|
|
27
28
|
export const SHARD_COUNT = 8;
|
|
28
29
|
const WATCHDOG_CRON = "*/60 * * * * *";
|
|
30
|
+
const WATCHDOG_PARENT_WORKFLOW = "sandbox-watchdog-parent";
|
|
31
|
+
const WATCHDOG_SHARD_WORKFLOW = "sandbox-watchdog-shard";
|
|
29
32
|
/** Stable hash → shard mapping, exported for tests. */
|
|
30
33
|
export function shardIndex(sandboxId, shardCount = SHARD_COUNT) {
|
|
31
34
|
const digest = createHash("sha1").update(sandboxId).digest();
|
|
@@ -131,7 +134,8 @@ export function registerWatchdog(deps) {
|
|
|
131
134
|
},
|
|
132
135
|
logger: deps.logger,
|
|
133
136
|
});
|
|
134
|
-
}, { name:
|
|
137
|
+
}, { name: WATCHDOG_SHARD_WORKFLOW });
|
|
138
|
+
untracedWorkflow(WATCHDOG_SHARD_WORKFLOW);
|
|
135
139
|
// The scheduled target must itself be a registered workflow — DBOS's
|
|
136
140
|
// scheduler loop skips (and errors on) any `@scheduled` function that lacks
|
|
137
141
|
// a workflow registration. Register, then schedule the same callable.
|
|
@@ -148,9 +152,10 @@ export function registerWatchdog(deps) {
|
|
|
148
152
|
continue;
|
|
149
153
|
await DBOS.startWorkflow(childCheckShard)(shard);
|
|
150
154
|
}
|
|
151
|
-
}, { name:
|
|
155
|
+
}, { name: WATCHDOG_PARENT_WORKFLOW });
|
|
156
|
+
untracedWorkflow(WATCHDOG_PARENT_WORKFLOW);
|
|
152
157
|
DBOS.registerScheduled(watchdogParent, {
|
|
153
|
-
name:
|
|
158
|
+
name: WATCHDOG_PARENT_WORKFLOW,
|
|
154
159
|
crontab: WATCHDOG_CRON,
|
|
155
160
|
});
|
|
156
161
|
}
|
|
@@ -63,6 +63,7 @@ import { mkdir } from "node:fs/promises";
|
|
|
63
63
|
import { join } from "node:path";
|
|
64
64
|
import { forStep } from "../auth/types.js";
|
|
65
65
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
66
|
+
import { ATTR_INFLEXA_STEP_ID, stableSpan } from "../lib/otel-spans.js";
|
|
66
67
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
67
68
|
import { RunDedupCollisionError, countArtifactsForRun, insertStepExecution, loadDataProfileStatus, queryActiveRun, queryStepArtifactPaths, seedStepExecutions, setRunSynthesisOutcome, suspendAnalysis as suspendAnalysisQuery, sweepPendingStepExecutions, updateRunStatus, updateStepExecution, } from "../state/index.js";
|
|
68
69
|
import { isBudgetExceeded } from "../loop/budget-exceeded.js";
|
|
@@ -770,7 +771,11 @@ async function runSchedulerLoop(args) {
|
|
|
770
771
|
// `completed`, so this is the first (and only) moment their results
|
|
771
772
|
// can be named. Checkpointed so a replay re-dispatches the child
|
|
772
773
|
// with a byte-identical prompt instead of re-reading the DB/disk.
|
|
773
|
-
const
|
|
774
|
+
const seedStepName = `compose-step-seed:${stepId}`;
|
|
775
|
+
const prompt = await DBOS.runStep(() => {
|
|
776
|
+
stableSpan(seedStepName, "compose-step-seed", { [ATTR_INFLEXA_STEP_ID]: stepId });
|
|
777
|
+
return composeStepSeed({ input, stepId, runId, deps });
|
|
778
|
+
}, { name: seedStepName });
|
|
774
779
|
const baseChildInput = buildChildInput({
|
|
775
780
|
input,
|
|
776
781
|
stepId,
|
|
@@ -37,6 +37,7 @@ import { DBOS } from "@dbos-inc/dbos-sdk";
|
|
|
37
37
|
import { z } from "zod";
|
|
38
38
|
import { forSubAgent } from "../auth/types.js";
|
|
39
39
|
import { createNoopLogger } from "../lib/console-logger.js";
|
|
40
|
+
import { ATTR_INFLEXA_FANOUT_KEY, stableSpan } from "../lib/otel-spans.js";
|
|
40
41
|
import { unwrapOrThrow } from "../lib/result.js";
|
|
41
42
|
import { durableStep } from "../loop/run-step.js";
|
|
42
43
|
import { createPubMedTool } from "../tools/bio/pubmed.js";
|
|
@@ -278,9 +279,14 @@ export async function runExecuteTargetAssessmentBody(input, deps) {
|
|
|
278
279
|
}))
|
|
279
280
|
: [];
|
|
280
281
|
async function runFanoutBlock(items, stepKindForName, keyOf, fn) {
|
|
281
|
-
return Promise.all(items.map((item) =>
|
|
282
|
-
|
|
283
|
-
|
|
282
|
+
return Promise.all(items.map((item) => {
|
|
283
|
+
const key = keyOf(item);
|
|
284
|
+
const stepName = `ta-fanout:${stepKindForName}:${key}`;
|
|
285
|
+
return DBOS.runStep(() => {
|
|
286
|
+
stableSpan(stepName, `ta-fanout:${stepKindForName}`, { [ATTR_INFLEXA_FANOUT_KEY]: key });
|
|
287
|
+
return fn(item);
|
|
288
|
+
}, { name: stepName }).catch((err) => wrapCollectorThrow(err));
|
|
289
|
+
}));
|
|
284
290
|
}
|
|
285
291
|
const [perModulatorFaersResults, perTrialAesResults, perModulatorPolypharmResults, perClassDrugAesResults] = await Promise.all([
|
|
286
292
|
runFanoutBlock(triageShortlist, "modulator-faers", (item) => item.moleculeChemblId, faersForOneModulator),
|
package/package.json
CHANGED