@inflexa-ai/harness 0.37.0 → 0.39.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 (101) hide show
  1. package/README.md +1 -2
  2. package/dist/auth/local-run-authorizer.js +5 -4
  3. package/dist/auth/types.d.ts +28 -9
  4. package/dist/auth/types.js +5 -3
  5. package/dist/billing/cached-resolver.d.ts +11 -7
  6. package/dist/billing/cached-resolver.js +17 -11
  7. package/dist/billing/noop-run-charge.js +3 -2
  8. package/dist/billing/noop-usage-recorder.js +2 -1
  9. package/dist/billing/run-charge.d.ts +18 -5
  10. package/dist/billing/run-charge.js +5 -1
  11. package/dist/billing/usage-recorder.d.ts +11 -9
  12. package/dist/billing/usage-recorder.js +2 -3
  13. package/dist/contracts/chat-events.d.ts +4 -1
  14. package/dist/contracts/chat-parts.d.ts +1 -1
  15. package/dist/contracts/schemas/chat-parts.d.ts +4 -4
  16. package/dist/execution/artifact-registration.d.ts +6 -1
  17. package/dist/execution/artifact-registration.js +12 -4
  18. package/dist/execution/artifact-registry.d.ts +10 -3
  19. package/dist/execution/noop-artifact-registry.js +3 -4
  20. package/dist/execution/post-step-pipeline.d.ts +14 -3
  21. package/dist/execution/post-step-pipeline.js +21 -8
  22. package/dist/execution/run-authorizer.d.ts +5 -3
  23. package/dist/execution/run-canceler.js +4 -20
  24. package/dist/index.d.ts +14 -8
  25. package/dist/index.js +12 -6
  26. package/dist/lib/hooks.d.ts +49 -0
  27. package/dist/lib/hooks.js +39 -0
  28. package/dist/lib/result.d.ts +22 -11
  29. package/dist/lib/result.js +24 -14
  30. package/dist/loop/run-agent.d.ts +2 -2
  31. package/dist/loop/run-agent.js +5 -4
  32. package/dist/provenance/seam.d.ts +1 -1
  33. package/dist/providers/ai-sdk.d.ts +43 -5
  34. package/dist/providers/ai-sdk.js +55 -52
  35. package/dist/providers/anthropic.d.ts +2 -3
  36. package/dist/providers/anthropic.js +2 -2
  37. package/dist/providers/embedding.d.ts +13 -7
  38. package/dist/providers/embedding.js +26 -6
  39. package/dist/providers/errors.d.ts +67 -37
  40. package/dist/providers/errors.js +57 -58
  41. package/dist/providers/request-headers.d.ts +18 -0
  42. package/dist/providers/request-headers.js +22 -0
  43. package/dist/report-model/production-resolver.d.ts +5 -2
  44. package/dist/report-model/production-resolver.js +12 -2
  45. package/dist/runtime/assemble.d.ts +3 -2
  46. package/dist/runtime/dbos.d.ts +4 -3
  47. package/dist/runtime/dbos.js +18 -3
  48. package/dist/runtime/result-serialization.d.ts +29 -0
  49. package/dist/runtime/result-serialization.js +45 -0
  50. package/dist/sandbox/client.d.ts +11 -3
  51. package/dist/sandbox/create-sandbox.d.ts +5 -2
  52. package/dist/sandbox/create-sandbox.js +51 -25
  53. package/dist/sandbox/docker-client.d.ts +4 -3
  54. package/dist/sandbox/docker-client.js +23 -31
  55. package/dist/sandbox/k8s-client.d.ts +4 -13
  56. package/dist/sandbox/k8s-client.js +28 -61
  57. package/dist/sandbox/labels.d.ts +34 -0
  58. package/dist/sandbox/labels.js +47 -0
  59. package/dist/sandbox/mount-plan.d.ts +3 -1
  60. package/dist/sandbox/mount-plan.js +9 -0
  61. package/dist/sandbox/sandbox-error.d.ts +36 -13
  62. package/dist/sandbox/sandbox-error.js +32 -17
  63. package/dist/sandbox/types.d.ts +18 -10
  64. package/dist/schemas/validate-plan.js +13 -0
  65. package/dist/schemas/workflow-state.d.ts +4 -4
  66. package/dist/state/analyses.d.ts +4 -4
  67. package/dist/state/analyses.js +4 -4
  68. package/dist/state/schema.d.ts +5 -5
  69. package/dist/tasks/data-profile.d.ts +5 -8
  70. package/dist/tasks/data-profile.js +88 -43
  71. package/dist/tasks/derive-table-exec.d.ts +10 -6
  72. package/dist/tasks/derive-table-exec.js +17 -12
  73. package/dist/tasks/extract-values.d.ts +16 -10
  74. package/dist/tasks/extract-values.js +34 -18
  75. package/dist/tools/execute-analysis.js +29 -24
  76. package/dist/tools/report-session/derive-table.d.ts +9 -1
  77. package/dist/tools/report-session/derive-table.js +25 -22
  78. package/dist/tools/report-session/examine-page.d.ts +6 -3
  79. package/dist/tools/report-session/examine-page.js +16 -8
  80. package/dist/tools/report-session/index.d.ts +1 -1
  81. package/dist/tools/report-session/preview-report.js +11 -9
  82. package/dist/tools/report-session/session-page-publisher.d.ts +17 -21
  83. package/dist/tools/report-session/session-page-publisher.js +5 -7
  84. package/dist/tools/research/inspect-run.d.ts +1 -1
  85. package/dist/tools/sandbox/link-packages.d.ts +8 -0
  86. package/dist/tools/sandbox/link-packages.js +19 -9
  87. package/dist/workflows/execute-analysis.d.ts +25 -16
  88. package/dist/workflows/execute-analysis.js +223 -216
  89. package/dist/workflows/metrics.d.ts +4 -3
  90. package/dist/workflows/metrics.js +4 -3
  91. package/dist/workflows/sandbox-step.d.ts +20 -29
  92. package/dist/workflows/sandbox-step.js +71 -127
  93. package/dist/workflows/suspension.d.ts +32 -0
  94. package/dist/workflows/suspension.js +38 -0
  95. package/package.json +1 -1
  96. package/dist/billing/noop-resolver.d.ts +0 -6
  97. package/dist/billing/noop-resolver.js +0 -7
  98. package/dist/billing/resolver.d.ts +0 -64
  99. package/dist/billing/resolver.js +0 -24
  100. package/dist/loop/budget-exceeded.d.ts +0 -38
  101. package/dist/loop/budget-exceeded.js +0 -88
package/README.md CHANGED
@@ -35,7 +35,6 @@ import {
35
35
  createConfiguredAiSdkProvider, // ChatProvider — any OpenAI-compatible endpoint
36
36
  createEmbeddingProvider,
37
37
  createLocalRunAuthorizer, // RunAuthorizer seam (local default)
38
- createNoopBillingResolver, // ResolveBilling seam
39
38
  createNoopRunCharge, // RunCharge seam
40
39
  createNoopUsageRecorder, // UsageRecorder seam
41
40
  createNoopArtifactRegistry, // ArtifactRegistry seam
@@ -49,7 +48,7 @@ import {
49
48
  } from "@inflexa-ai/harness";
50
49
  ```
51
50
 
52
- `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`, `UsageRecorder` — 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.
51
+ `assembleCoreRuntime` is the single composition point: it registers the durable workflows and builds the conversation agent over them. The harness declares **four capability seams** an embedder wires — `RunAuthorizer`, `ArtifactRegistry`, `RunCharge`, `UsageRecorder` — plus the **shared `RunLauncher`** (one host-neutral realization, `createDbosRunLauncher`). Local, dependency-free realizations of all four ship from the barrel; an embedder constructs them (or its own) and passes them in. The harness only ever sees the interface. Two optional hooks go into the component that uses their values: `resolveRequestHeaders` into each provider, and `resolveSandboxLabels` into the sandbox client config.
53
52
 
54
53
  ## Logging
55
54
 
@@ -7,9 +7,10 @@
7
7
  * forwarded untouched — local code never
8
8
  * inspects it (no `getAuth`).
9
9
  */
10
+ import { okAsync } from "neverthrow";
10
11
  export function createLocalRunAuthorizer() {
11
12
  return {
12
- async authorize({ auth, scope, provenance, frame }) {
13
+ authorize({ auth, scope, provenance, frame }) {
13
14
  const runSession = {
14
15
  identity: { user: "local" },
15
16
  scope,
@@ -17,9 +18,9 @@ export function createLocalRunAuthorizer() {
17
18
  runFrame: frame,
18
19
  auth,
19
20
  };
20
- return { runSession, ownsMandate: false }; // oss-core-managed-ok
21
+ return okAsync({ runSession, ownsMandate: false }); // oss-core-managed-ok
21
22
  },
22
- async revoke() { },
23
- async revokeByJti() { },
23
+ revoke: () => okAsync(undefined),
24
+ revokeByJti: () => okAsync(undefined),
24
25
  };
25
26
  }
@@ -16,8 +16,9 @@
16
16
  * - `RunSession` — durable, JSON-serializable. Carries a `RunFrame`; minted
17
17
  * only at the run-authorization seam.
18
18
  *
19
- * Neither bundle carries resolved billing headers — billing is resolved
20
- * lazily at the LLM call site (see `harness/billing/resolver.ts`).
19
+ * Neither bundle carries request headers — a provider gets them at the call
20
+ * site from the optional request headers hook of the host (see
21
+ * `providers/request-headers.ts`).
21
22
  */
22
23
  /** Who the request is. Always complete. */
23
24
  export interface Identity {
@@ -89,7 +90,7 @@ export interface AuthContext {
89
90
  * consumer is the provider, which reads every field, so the conduit type is
90
91
  * "all fields, generalized to what's read" — `credential` widened to the
91
92
  * opaque `Credential` (nothing in the harness branches on the concrete shape),
92
- * `runFrame` optional (only the billing assembler reads it).
93
+ * `runFrame` optional (only a host hook reads it).
93
94
  *
94
95
  * Both bundles satisfy this: a `RequestSession` (no RunFrame) and a
95
96
  * `RunSession` (with RunFrame) are each structurally assignable, so the
@@ -130,16 +131,33 @@ export interface RunSession {
130
131
  readonly auth: AuthContext;
131
132
  }
132
133
  /**
133
- * The minimal session view the billing-header assembler reads. Both bundles
134
- * satisfy it; `runFrame` is present only on a `RunSession`, so a
135
- * `RequestSession` emits no run/step tags.
134
+ * The session of a sandbox spawn. A sandbox takes its analysis id, its run id,
135
+ * and its step id from this session.
136
136
  */
137
- export interface BillingSessionView {
137
+ export interface SpawnSession extends RunSession {
138
+ readonly runFrame: RunFrame & {
139
+ readonly stepId: string;
140
+ };
141
+ }
142
+ /**
143
+ * The minimal session view that a host hook reads. Both bundles satisfy it;
144
+ * `runFrame` is present only on a `RunSession`, so a hook that gets a
145
+ * `RequestSession` sees no run or step.
146
+ */
147
+ export interface HookSessionView {
138
148
  readonly identity: Identity;
139
149
  readonly scope: Scope;
140
150
  readonly provenance: Provenance;
141
151
  readonly runFrame?: RunFrame;
142
152
  }
153
+ /**
154
+ * The session that the request headers hook gets: the hook view plus the
155
+ * opaque `auth`, which a host realization downcasts to reach its credential.
156
+ * `AgentSession` structurally satisfies it.
157
+ */
158
+ export interface ResolvableSession extends HookSessionView {
159
+ readonly auth: AuthContext;
160
+ }
143
161
  /**
144
162
  * Derive a child session for a sub-agent call — provenance only. Sets the
145
163
  * child `agentId` and appends it to `callPath`, leaving identity, scope, and
@@ -151,7 +169,8 @@ export declare function forSubAgent<S extends {
151
169
  /**
152
170
  * Derive a child `RunSession` for a sandbox step — pure value derivation.
153
171
  * Sets `runFrame.stepId`; identity, scope, auth, and provenance are
154
- * unchanged. Used by parents when computing child workflow input.
172
+ * unchanged. Used by parents when computing child workflow input, and by a
173
+ * spawn path that names the step of its sandbox.
155
174
  */
156
- export declare function forStep(parent: RunSession, stepId: string): RunSession;
175
+ export declare function forStep(parent: RunSession, stepId: string): SpawnSession;
157
176
  export {};
@@ -16,8 +16,9 @@
16
16
  * - `RunSession` — durable, JSON-serializable. Carries a `RunFrame`; minted
17
17
  * only at the run-authorization seam.
18
18
  *
19
- * Neither bundle carries resolved billing headers — billing is resolved
20
- * lazily at the LLM call site (see `harness/billing/resolver.ts`).
19
+ * Neither bundle carries request headers — a provider gets them at the call
20
+ * site from the optional request headers hook of the host (see
21
+ * `providers/request-headers.ts`).
21
22
  */
22
23
  /** Derive the `{ resourceType, resourceId }` a scope resolves through: `{ "analysis", analysisId }`. */
23
24
  export function scopeResource(scope) {
@@ -44,7 +45,8 @@ export function forSubAgent(session, agentId) {
44
45
  /**
45
46
  * Derive a child `RunSession` for a sandbox step — pure value derivation.
46
47
  * Sets `runFrame.stepId`; identity, scope, auth, and provenance are
47
- * unchanged. Used by parents when computing child workflow input.
48
+ * unchanged. Used by parents when computing child workflow input, and by a
49
+ * spawn path that names the step of its sandbox.
48
50
  */
49
51
  export function forStep(parent, stepId) {
50
52
  return {
@@ -3,21 +3,25 @@
3
3
  *
4
4
  * Resolves an input to a value via `fetch`, caching success only behind a
5
5
  * bounded LRU with per-entry TTL. Concurrent callers for the same key coalesce
6
- * onto one in-flight fetch; a fetch that throws is not cached and clears its
7
- * in-flight slot. Callers supply their own key formula, fetch+validate step,
8
- * and (optionally) cache hit/miss metrics; this module owns eviction,
9
- * coalescing, and expiry — the deep, drift-prone plumbing.
6
+ * onto one in-flight fetch and share its outcome. An `err` is never cached:
7
+ * a host that gates spend per caller must see every refusal again, and a
8
+ * cached refusal or a cached transient fault would outlive its cause. A fetch
9
+ * that rejects is a defect: it is not cached, it clears its in-flight slot,
10
+ * and the rejection passes through. Callers supply their own key formula,
11
+ * fetch+validate step, and (optionally) cache hit/miss metrics; this module
12
+ * owns eviction, coalescing, and expiry — the deep, drift-prone plumbing.
10
13
  */
14
+ import { ResultAsync } from "neverthrow";
11
15
  export interface CachedResolverMetrics {
12
16
  onHit(): void;
13
17
  /** Recorded once per upstream fetch — coalesced callers do not count. */
14
18
  onMiss(): void;
15
19
  }
16
- export interface CachedResolverConfig<In, Out> {
20
+ export interface CachedResolverConfig<In, Out, E> {
17
21
  readonly ttlMs: number;
18
22
  readonly maxEntries: number;
19
23
  readonly keyFn: (input: In) => string;
20
- readonly fetch: (input: In) => Promise<Out>;
24
+ readonly fetch: (input: In) => ResultAsync<Out, E>;
21
25
  readonly metrics?: CachedResolverMetrics;
22
26
  }
23
- export declare function createCachedResolver<In, Out>(cfg: CachedResolverConfig<In, Out>): (input: In) => Promise<Out>;
27
+ export declare function createCachedResolver<In, Out, E>(cfg: CachedResolverConfig<In, Out, E>): (input: In) => ResultAsync<Out, E>;
@@ -3,11 +3,15 @@
3
3
  *
4
4
  * Resolves an input to a value via `fetch`, caching success only behind a
5
5
  * bounded LRU with per-entry TTL. Concurrent callers for the same key coalesce
6
- * onto one in-flight fetch; a fetch that throws is not cached and clears its
7
- * in-flight slot. Callers supply their own key formula, fetch+validate step,
8
- * and (optionally) cache hit/miss metrics; this module owns eviction,
9
- * coalescing, and expiry — the deep, drift-prone plumbing.
6
+ * onto one in-flight fetch and share its outcome. An `err` is never cached:
7
+ * a host that gates spend per caller must see every refusal again, and a
8
+ * cached refusal or a cached transient fault would outlive its cause. A fetch
9
+ * that rejects is a defect: it is not cached, it clears its in-flight slot,
10
+ * and the rejection passes through. Callers supply their own key formula,
11
+ * fetch+validate step, and (optionally) cache hit/miss metrics; this module
12
+ * owns eviction, coalescing, and expiry — the deep, drift-prone plumbing.
10
13
  */
14
+ import { ResultAsync, ok } from "neverthrow";
11
15
  export function createCachedResolver(cfg) {
12
16
  // Map insertion order doubles as LRU recency: a hit re-inserts the key at
13
17
  // the tail; eviction drops the head (oldest).
@@ -23,7 +27,7 @@ export function createCachedResolver(cfg) {
23
27
  }
24
28
  cache.delete(key);
25
29
  cache.set(key, entry);
26
- return entry.value;
30
+ return entry;
27
31
  }
28
32
  function store(key, value) {
29
33
  cache.set(key, { value, expiresAt: Date.now() + cfg.ttlMs });
@@ -34,21 +38,22 @@ export function createCachedResolver(cfg) {
34
38
  cache.delete(oldest);
35
39
  }
36
40
  }
37
- return async function resolve(input) {
41
+ async function resolve(input) {
38
42
  const key = cfg.keyFn(input);
39
43
  const fresh = readFresh(key);
40
44
  if (fresh !== undefined) {
41
45
  cfg.metrics?.onHit();
42
- return fresh;
46
+ return ok(fresh.value);
43
47
  }
44
48
  const pending = inflight.get(key);
45
49
  if (pending)
46
50
  return pending;
47
51
  cfg.metrics?.onMiss();
48
52
  const promise = (async () => {
49
- const value = await cfg.fetch(input);
50
- store(key, value);
51
- return value;
53
+ const fetched = await cfg.fetch(input);
54
+ if (fetched.isOk())
55
+ store(key, fetched.value);
56
+ return fetched;
52
57
  })();
53
58
  inflight.set(key, promise);
54
59
  try {
@@ -57,5 +62,6 @@ export function createCachedResolver(cfg) {
57
62
  finally {
58
63
  inflight.delete(key);
59
64
  }
60
- };
65
+ }
66
+ return (input) => new ResultAsync(resolve(input));
61
67
  }
@@ -1,10 +1,11 @@
1
+ import { okAsync } from "neverthrow";
1
2
  /**
2
3
  * Local/OSS run-charge seam: open and close are both no-ops. No external
3
4
  * billing ledger in the cloud-free build.
4
5
  */
5
6
  export function createNoopRunCharge() {
6
7
  return {
7
- open: async () => { },
8
- close: async () => { },
8
+ open: () => okAsync(undefined),
9
+ close: () => okAsync(undefined),
9
10
  };
10
11
  }
@@ -1,3 +1,4 @@
1
+ import { okAsync } from "neverthrow";
1
2
  /**
2
3
  * Local/OSS usage recorder: drops every record. No ledger, no I/O — the
3
4
  * default an embedder that wires nothing gets. Runs behave exactly as they did
@@ -5,6 +6,6 @@
5
6
  */
6
7
  export function createNoopUsageRecorder() {
7
8
  return {
8
- record: () => { },
9
+ record: () => okAsync(undefined),
9
10
  };
10
11
  }
@@ -2,23 +2,36 @@
2
2
  * `RunCharge` — the run-level billing-bracket seam.
3
3
  *
4
4
  * `executeAnalysis` opens a running charge at init and closes it on the
5
- * terminal path with one of four reasons. A managed embedder wires an
5
+ * terminal path with the outcome of the run. A managed embedder wires an
6
6
  * external running-charge bracket; OSS wires a no-op. The harness only ever sees this
7
7
  * interface — the body brackets the run, the embedder owns the ledger.
8
+ *
9
+ * `open` is a gate: an `err` ends the run before a step starts, or suspends it
10
+ * when the host asks. `close` is a notice: an `err` is logged, and the terminal
11
+ * status of the run does not change (see `lib/hooks.ts`).
8
12
  */
13
+ import type { ResultAsync } from "neverthrow";
9
14
  import type { AgentSession } from "../auth/types.js";
15
+ import type { GateFailure, NoticeFailure } from "../lib/hooks.js";
16
+ /** A suspension carries the reason of the host, which the harness does not read. */
17
+ export type RunChargeOutcome = {
18
+ readonly kind: "ok" | "error" | "canceled";
19
+ } | {
20
+ readonly kind: "suspended";
21
+ readonly reason: string;
22
+ };
10
23
  export interface RunCharge {
11
24
  /** Open the run's running charge at init. */
12
25
  open(args: {
13
26
  analysisId: string;
14
27
  runId: string;
15
28
  session: AgentSession;
16
- }): Promise<void>;
17
- /** Close the run's running charge with one of four terminal reasons. */
29
+ }): ResultAsync<void, GateFailure>;
30
+ /** Close the run's running charge with the outcome of the run. */
18
31
  close(args: {
19
32
  analysisId: string;
20
33
  runId: string;
21
- reason: "ok" | "error" | "canceled" | "budget_exceeded";
34
+ outcome: RunChargeOutcome;
22
35
  session: AgentSession;
23
- }): Promise<void>;
36
+ }): ResultAsync<void, NoticeFailure>;
24
37
  }
@@ -2,8 +2,12 @@
2
2
  * `RunCharge` — the run-level billing-bracket seam.
3
3
  *
4
4
  * `executeAnalysis` opens a running charge at init and closes it on the
5
- * terminal path with one of four reasons. A managed embedder wires an
5
+ * terminal path with the outcome of the run. A managed embedder wires an
6
6
  * external running-charge bracket; OSS wires a no-op. The harness only ever sees this
7
7
  * interface — the body brackets the run, the embedder owns the ledger.
8
+ *
9
+ * `open` is a gate: an `err` ends the run before a step starts, or suspends it
10
+ * when the host asks. `close` is a notice: an `err` is logged, and the terminal
11
+ * status of the run does not change (see `lib/hooks.ts`).
8
12
  */
9
13
  export {};
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * `UsageRecorder` — the per-call LLM usage-accounting seam.
3
3
  *
4
- * Sibling of `ResolveBilling` and `RunCharge`, and complementary to both:
5
- * `ResolveBilling` stamps attribution onto a wire call, `RunCharge` brackets a
6
- * run for managed billing, and `UsageRecorder` streams the fine-grained token
4
+ * Sibling of `RunCharge`, and complementary to it: `RunCharge` brackets a run
5
+ * for managed billing, and `UsageRecorder` streams the fine-grained token
7
6
  * telemetry every completed LLM call produces. The harness names no storage or
8
7
  * display technology — it emits records; the embedder decides where they land.
9
8
  *
@@ -12,7 +11,9 @@
12
11
  * composition root and reaches every `runAgent` invocation through the deps
13
12
  * bags. OSS default: `createNoopUsageRecorder` (`./noop-usage-recorder.ts`).
14
13
  */
14
+ import type { ResultAsync } from "neverthrow";
15
15
  import type { Scope } from "../auth/types.js";
16
+ import type { NoticeFailure } from "../lib/hooks.js";
16
17
  import type { ChatUsage } from "../providers/types.js";
17
18
  /** One completed LLM call's token usage, with the attribution held at the call site. */
18
19
  export interface LlmUsageRecord {
@@ -52,12 +53,13 @@ export interface LlmUsageRecord {
52
53
  /**
53
54
  * Records one `LlmUsageRecord` per completed LLM call.
54
55
  *
55
- * Fire-and-forget: the loop neither awaits `record` nor guards it, so a
56
- * realization MUST NOT throw and MUST NOT block — buffer and flush
57
- * internally, and own your error handling (diagnostics belong on the injected
58
- * `Logger`). A recorder that violates the contract fails or stalls the run it
59
- * was only meant to observe.
56
+ * `record` is a notice (see `lib/hooks.ts`). It gives a failure as an `err`,
57
+ * never as a throw. The loop does not wait for the result and puts no `try`
58
+ * around the call. When the result arrives, the loop logs the reason of an
59
+ * `err`, and the run continues with no change. A recorder that blocks does not
60
+ * make a run slower, but a realization that throws before it returns a
61
+ * `ResultAsync` still fails the run it was only meant to observe.
60
62
  */
61
63
  export interface UsageRecorder {
62
- record(record: LlmUsageRecord): void;
64
+ record(record: LlmUsageRecord): ResultAsync<void, NoticeFailure>;
63
65
  }
@@ -1,9 +1,8 @@
1
1
  /**
2
2
  * `UsageRecorder` — the per-call LLM usage-accounting seam.
3
3
  *
4
- * Sibling of `ResolveBilling` and `RunCharge`, and complementary to both:
5
- * `ResolveBilling` stamps attribution onto a wire call, `RunCharge` brackets a
6
- * run for managed billing, and `UsageRecorder` streams the fine-grained token
4
+ * Sibling of `RunCharge`, and complementary to it: `RunCharge` brackets a run
5
+ * for managed billing, and `UsageRecorder` streams the fine-grained token
7
6
  * telemetry every completed LLM call produces. The harness names no storage or
8
7
  * display technology — it emits records; the embedder decides where they land.
9
8
  *
@@ -124,7 +124,10 @@ export interface ChatErrorEvent {
124
124
  type: "error";
125
125
  /** Human-readable, scrubbed error message. */
126
126
  message: string;
127
- /** Machine-readable failure code, e.g. `"budget_exceeded"`. */
127
+ /**
128
+ * Machine-readable failure code. For a turn that a `suspend` error failed, it
129
+ * is the suspend reason of the host, carried unread (`suspensionOfFailure`).
130
+ */
128
131
  reason?: string;
129
132
  source: EventSource;
130
133
  }
@@ -339,7 +339,7 @@ export interface RunFailedPart {
339
339
  runId: string;
340
340
  /** Scrubbed error message (no internal details). */
341
341
  error: string;
342
- /** Machine-readable failure reason (e.g., "budget_exceeded"). */
342
+ /** The suspend reason of the host, carried unread; present only when the run suspended. */
343
343
  reason?: string;
344
344
  }
345
345
  /**
@@ -151,10 +151,10 @@ export declare const RunStartedPartSchema: z.ZodObject<{
151
151
  stepCount: z.ZodNumber;
152
152
  }, z.core.$strip>;
153
153
  export declare const StepStatusSchema: z.ZodEnum<{
154
+ failed: "failed";
154
155
  pending: "pending";
155
156
  running: "running";
156
157
  completed: "completed";
157
- failed: "failed";
158
158
  skipped: "skipped";
159
159
  queued: "queued";
160
160
  }>;
@@ -163,10 +163,10 @@ export declare const DagStepStateSchema: z.ZodObject<{
163
163
  name: z.ZodString;
164
164
  agent: z.ZodString;
165
165
  status: z.ZodEnum<{
166
+ failed: "failed";
166
167
  pending: "pending";
167
168
  running: "running";
168
169
  completed: "completed";
169
- failed: "failed";
170
170
  skipped: "skipped";
171
171
  queued: "queued";
172
172
  }>;
@@ -186,10 +186,10 @@ export declare const DagStatePartSchema: z.ZodObject<{
186
186
  name: z.ZodString;
187
187
  agent: z.ZodString;
188
188
  status: z.ZodEnum<{
189
+ failed: "failed";
189
190
  pending: "pending";
190
191
  running: "running";
191
192
  completed: "completed";
192
- failed: "failed";
193
193
  skipped: "skipped";
194
194
  queued: "queued";
195
195
  }>;
@@ -570,10 +570,10 @@ export declare const CortexChatPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<
570
570
  name: z.ZodString;
571
571
  agent: z.ZodString;
572
572
  status: z.ZodEnum<{
573
+ failed: "failed";
573
574
  pending: "pending";
574
575
  running: "running";
575
576
  completed: "completed";
576
- failed: "failed";
577
577
  skipped: "skipped";
578
578
  queued: "queued";
579
579
  }>;
@@ -9,8 +9,10 @@
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 { type Result } from "neverthrow";
12
13
  import type { Pool, PoolClient } from "pg";
13
14
  import type { AgentSession } from "../auth/types.js";
15
+ import { type GateRefusal } from "../lib/hooks.js";
14
16
  import type { Logger } from "../lib/logger.js";
15
17
  import type { ArtifactRegistry, ArtifactRegistrationInput } from "./artifact-registry.js";
16
18
  export type { ArtifactRegistrationInput };
@@ -33,5 +35,8 @@ export interface ArtifactRegistrationResult {
33
35
  * Builds the `cortex_artifacts` rows from the manifest, upserts them, then
34
36
  * delegates external provenance registration to the injected `ArtifactRegistry`
35
37
  * and applies any returned external ids back onto the local rows.
38
+ *
39
+ * A refusal of `register` writes no external id: the upserted rows keep
40
+ * `artifact_id = NULL`.
36
41
  */
37
- export declare function registerStepArtifacts(db: Pool | PoolClient, registry: ArtifactRegistry, input: ArtifactRegistrationInput, session: AgentSession, logger?: Logger): Promise<ArtifactRegistrationResult>;
42
+ export declare function registerStepArtifacts(db: Pool | PoolClient, registry: ArtifactRegistry, input: ArtifactRegistrationInput, session: AgentSession, logger?: Logger): Promise<Result<ArtifactRegistrationResult, GateRefusal>>;
@@ -9,7 +9,9 @@
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 { err, ok } from "neverthrow";
12
13
  import { createNoopLogger } from "../lib/console-logger.js";
14
+ import { passGate } from "../lib/hooks.js";
13
15
  import { upsertArtifacts, updateArtifactId } from "../state/index.js";
14
16
  // ── Registration ─────────────────────────────────────────────────────
15
17
  /**
@@ -18,6 +20,9 @@ import { upsertArtifacts, updateArtifactId } from "../state/index.js";
18
20
  * Builds the `cortex_artifacts` rows from the manifest, upserts them, then
19
21
  * delegates external provenance registration to the injected `ArtifactRegistry`
20
22
  * and applies any returned external ids back onto the local rows.
23
+ *
24
+ * A refusal of `register` writes no external id: the upserted rows keep
25
+ * `artifact_id = NULL`.
21
26
  */
22
27
  export async function registerStepArtifacts(db, registry, input, session,
23
28
  // A parameter rather than a field on `ArtifactRegistrationInput`: that type is
@@ -27,7 +32,7 @@ logger = createNoopLogger()) {
27
32
  const { resourceId, runId, stepId, artifacts } = input;
28
33
  const log = logger.named("artifact-registration").with({ runId, stepId });
29
34
  if (artifacts.length === 0) {
30
- return { localCount: 0, externalRegistered: 0, externalFailed: 0, failureDetails: [] };
35
+ return ok({ localCount: 0, externalRegistered: 0, externalFailed: 0, failureDetails: [] });
31
36
  }
32
37
  const dbPathPrefix = `runs/${runId}/${stepId}/`;
33
38
  const emptyHash = "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
@@ -48,7 +53,10 @@ logger = createNoopLogger()) {
48
53
  });
49
54
  await upsertArtifacts(db, localEntries);
50
55
  const localPaths = new Set(localEntries.map((e) => e.path));
51
- const result = await registry.register(input, session);
56
+ const registered = await passGate("ArtifactRegistry.register", registry.register(input, session));
57
+ if (registered.isErr())
58
+ return err(registered.error);
59
+ const result = registered.value;
52
60
  // The registry excluded these from `failed` by its own severity judgement, so
53
61
  // this is their only record. Logged here rather than left to the registry:
54
62
  // the seam is implementable by an embedder, and a rejection does not get to
@@ -67,10 +75,10 @@ logger = createNoopLogger()) {
67
75
  await updateArtifactId(db, resourceId, reg.path, reg.externalId);
68
76
  externalRegistered++;
69
77
  }
70
- return {
78
+ return ok({
71
79
  localCount: localEntries.length,
72
80
  externalRegistered,
73
81
  externalFailed: result.failedCount,
74
82
  failureDetails: result.failed,
75
- };
83
+ });
76
84
  }
@@ -14,9 +14,11 @@
14
14
  * - `createNoopArtifactRegistry` (the harness, this dir) — registers nothing
15
15
  * externally; the local `cortex_artifacts` ledger is the only record.
16
16
  */
17
+ import type { ResultAsync } from "neverthrow";
17
18
  import type { ArtifactManifestEntry } from "../schemas/artifact-manifest.js";
18
19
  import type { ProvenanceCollector } from "../provenance/collector.js";
19
20
  import type { AgentSession } from "../auth/types.js";
21
+ import type { GateFailure, NoticeFailure } from "../lib/hooks.js";
20
22
  /** High-level input for registering one step's artifacts. */
21
23
  export interface ArtifactRegistrationInput {
22
24
  /** Analysis resource ID. */
@@ -85,12 +87,17 @@ export interface ArtifactRegistry {
85
87
  * Implementations MUST NOT touch the local `cortex_artifacts` ledger — that
86
88
  * is the harness's responsibility, applied around this call. The `session` carries
87
89
  * the run credential an adapter needs to address the external system.
90
+ *
91
+ * A partial outcome is an `ok`, with the rejections in `failed`.
88
92
  */
89
- register(input: ArtifactRegistrationInput, session: AgentSession): Promise<ExternalRegistrationResult>;
93
+ register(input: ArtifactRegistrationInput, session: AgentSession): ResultAsync<ExternalRegistrationResult, GateFailure>;
90
94
  /**
91
95
  * Push a step's registered artifacts to permanent storage. A no-op when the
92
96
  * adapter's bytes already live locally; the managed adapter uploads them.
93
- * Throws on a persistent failure so the caller's fail-fast boundary fires.
97
+ *
98
+ * A notice (see `lib/hooks.ts`): an `err` is logged and does not fail the
99
+ * step. The rows stay unsynced, thus a later sync of the step selects them
100
+ * again.
94
101
  */
95
- sync(input: ArtifactSyncInput, session: AgentSession): Promise<void>;
102
+ sync(input: ArtifactSyncInput, session: AgentSession): ResultAsync<void, NoticeFailure>;
96
103
  }
@@ -1,3 +1,4 @@
1
+ import { okAsync } from "neverthrow";
1
2
  /**
2
3
  * Local/OSS artifact-registration seam: `register` records nothing externally
3
4
  * and reports zero failures; `sync` is a no-op. The harness writes the local
@@ -9,9 +10,7 @@
9
10
  */
10
11
  export function createNoopArtifactRegistry() {
11
12
  return {
12
- async register() {
13
- return { registered: [], failed: [], failedCount: 0 };
14
- },
15
- async sync() { },
13
+ register: () => okAsync({ registered: [], failed: [], failedCount: 0 }),
14
+ sync: () => okAsync(undefined),
16
15
  };
17
16
  }
@@ -10,12 +10,14 @@
10
10
  * the orchestration — which stage is `DBOS.runStep`-wrapped, which stage is
11
11
  * fail-fast vs. best-effort. This module owns only the stage bodies.
12
12
  */
13
+ import { type Result } from "neverthrow";
13
14
  import type { Pool } from "pg";
14
15
  import type { AgentChat, EmbeddingProvider } from "../providers/types.js";
15
16
  import type { WorkspaceFilesystem } from "../workspace/filesystem.js";
16
17
  import type { ResolveWorkspaceRoot } from "../workspace/paths.js";
17
18
  import type { ArtifactManifestEntry } from "../schemas/artifact-manifest.js";
18
19
  import type { StepSummary } from "../schemas/step-summary.js";
20
+ import type { GateRefusal } from "../lib/hooks.js";
19
21
  import type { Logger } from "../lib/logger.js";
20
22
  import type { UsageRecorder } from "../billing/usage-recorder.js";
21
23
  import type { ArtifactRegistry } from "./artifact-registry.js";
@@ -53,17 +55,26 @@ export declare function generateStepFileMetadata(deps: PostStepPipelineDeps, pos
53
55
  * still returned for vector indexing.
54
56
  */
55
57
  export declare function generateStepSummaryAndWrite(deps: PostStepPipelineDeps, postCtx: PostStepContext, manifest: readonly ArtifactManifestEntry[]): Promise<StepSummary | undefined>;
58
+ export type StepRegistrationFailure = {
59
+ readonly kind: "refused";
60
+ readonly refusal: GateRefusal;
61
+ } | {
62
+ readonly kind: "rejected";
63
+ readonly message: string;
64
+ };
65
+ export declare function describeStepRegistrationFailure(failure: StepRegistrationFailure): string;
56
66
  /**
57
67
  * Reconcile the manifest against disk (drop phantoms, rehash) and register the
58
68
  * survivors with the local ledger + the injected `ArtifactRegistry`. Fail-fast
59
69
  * (see the artifact-manifest spec): `externalFailed` counts only the terminal
60
70
  * rejections — a rejected output whose bytes exist nowhere but the step tree,
61
71
  * plus anything rejected as a consequence of one — so a non-zero count means
62
- * real outputs went unregistered, and it throws with the per-file detail (the
63
- * OSS filesystem registry returns `externalFailed: 0`, so it never trips).
72
+ * real outputs went unregistered, and the `err` carries the per-file detail (the
73
+ * OSS filesystem registry returns `externalFailed: 0`, so it never trips). A
74
+ * refusal of the `register` gate is the other `err`.
64
75
  * Returns the reconciled manifest.
65
76
  */
66
- export declare function reconcileAndRegisterStepArtifacts(deps: PostStepPipelineDeps, postCtx: PostStepContext, manifest: readonly ArtifactManifestEntry[]): Promise<readonly ArtifactManifestEntry[]>;
77
+ export declare function reconcileAndRegisterStepArtifacts(deps: PostStepPipelineDeps, postCtx: PostStepContext, manifest: readonly ArtifactManifestEntry[]): Promise<Result<readonly ArtifactManifestEntry[], StepRegistrationFailure>>;
67
78
  /**
68
79
  * Vector-index the threaded file descriptions + the step summary into the
69
80
  * per-analysis pgvector store. Best-effort (see the artifact-manifest spec):