@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
@@ -11,6 +11,7 @@
11
11
  * fail-fast vs. best-effort. This module owns only the stage bodies.
12
12
  */
13
13
  import { join } from "node:path";
14
+ import { err, ok } from "neverthrow";
14
15
  import { unwrapOrThrow } from "../lib/result.js";
15
16
  import { createNoopLogger } from "../lib/console-logger.js";
16
17
  import { writeFileWithinRoot } from "../lib/fs-helpers.js";
@@ -92,14 +93,18 @@ export async function generateStepSummaryAndWrite(deps, postCtx, manifest) {
92
93
  }
93
94
  return summary;
94
95
  }
96
+ export function describeStepRegistrationFailure(failure) {
97
+ return failure.kind === "refused" ? `the artifact registry refused the registration: ${failure.refusal.reason}` : failure.message;
98
+ }
95
99
  /**
96
100
  * Reconcile the manifest against disk (drop phantoms, rehash) and register the
97
101
  * survivors with the local ledger + the injected `ArtifactRegistry`. Fail-fast
98
102
  * (see the artifact-manifest spec): `externalFailed` counts only the terminal
99
103
  * rejections — a rejected output whose bytes exist nowhere but the step tree,
100
104
  * plus anything rejected as a consequence of one — so a non-zero count means
101
- * real outputs went unregistered, and it throws with the per-file detail (the
102
- * OSS filesystem registry returns `externalFailed: 0`, so it never trips).
105
+ * real outputs went unregistered, and the `err` carries the per-file detail (the
106
+ * OSS filesystem registry returns `externalFailed: 0`, so it never trips). A
107
+ * refusal of the `register` gate is the other `err`.
103
108
  * Returns the reconciled manifest.
104
109
  */
105
110
  export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest) {
@@ -115,14 +120,17 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
115
120
  ...(deps.logger ? { logger: deps.logger } : {}),
116
121
  });
117
122
  if (reconciled.manifest.length === 0)
118
- return reconciled.manifest;
119
- const reg = await registerStepArtifacts(deps.pool, deps.artifactRegistry, {
123
+ return ok(reconciled.manifest);
124
+ const registered = await registerStepArtifacts(deps.pool, deps.artifactRegistry, {
120
125
  resourceId: input.analysisId,
121
126
  runId: input.runId,
122
127
  stepId: input.stepId,
123
128
  artifacts: reconciled.manifest,
124
129
  collector: lineageCollector,
125
130
  }, session, deps.logger);
131
+ if (registered.isErr())
132
+ return err({ kind: "refused", refusal: registered.error });
133
+ const reg = registered.value;
126
134
  if (reg.externalFailed > 0) {
127
135
  // The registry commits per leaf and per activity, so accepted and rejected
128
136
  // rows arriving together is ordinary and `externalRegistered` is what
@@ -137,7 +145,7 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
137
145
  `${reg.externalFailed} terminal rejection(s), ${reg.externalRegistered}/${reg.localCount} local artifact(s) registered` +
138
146
  (noneRegistered ? " (nothing in this batch registered)" : "") +
139
147
  (detail ? `\n ${detail}` : "");
140
- // Logged as fields as well as thrown: the throw reaches `failStep` as one
148
+ // Logged as fields as well as returned: the failure reaches `failStep` as one
141
149
  // opaque string, so the per-path rejections are only queryable from here.
142
150
  // This is what distinguishes a registry rejection from the attestation
143
151
  // throws in `reconcileManifestWithDisk` when a step dies.
@@ -150,9 +158,9 @@ export async function reconcileAndRegisterStepArtifacts(deps, postCtx, manifest)
150
158
  noneRegistered,
151
159
  failures: reg.failureDetails,
152
160
  });
153
- throw new Error(msg);
161
+ return err({ kind: "rejected", message: msg });
154
162
  }
155
- return reconciled.manifest;
163
+ return ok(reconciled.manifest);
156
164
  }
157
165
  /**
158
166
  * Vector-index the threaded file descriptions + the step summary into the
@@ -212,7 +220,12 @@ export async function vectorIndexStepOutputs(deps, postCtx, artifacts) {
212
220
  let indexed = 0;
213
221
  let failed = 0;
214
222
  for (const entry of liveMetadata) {
215
- const ok = await indexOne(`/${input.analysisId}/${entry.dbPath}`, entry.description, { text: entry.description, type: "output", ...entry.metadata, path: entry.dbPath });
223
+ const ok = await indexOne(`/${input.analysisId}/${entry.dbPath}`, entry.description, {
224
+ text: entry.description,
225
+ type: "output",
226
+ ...entry.metadata,
227
+ path: entry.dbPath,
228
+ });
216
229
  if (ok)
217
230
  indexed++;
218
231
  else
@@ -14,7 +14,9 @@
14
14
  * Mirrors the `SessionPagePublisher` seam: a narrow interface in the harness, the managed
15
15
  * realization injected at the composition root.
16
16
  */
17
+ import type { ResultAsync } from "neverthrow";
17
18
  import type { AuthContext, Provenance, RunFrame, RunSession, Scope } from "../auth/types.js";
19
+ import type { GateFailure, NoticeFailure } from "../lib/hooks.js";
18
20
  /**
19
21
  * Locates a run credential when no `RunAuthorization` exists: the persisted
20
22
  * jti from `cortex_runs` plus the revoking caller's opaque `auth` — the sole
@@ -48,8 +50,8 @@ export interface RunAuthorization {
48
50
  * path (a no-op for reused or local authorizations).
49
51
  */
50
52
  export interface RunAuthorizer {
51
- authorize(input: AuthorizeRunInput): Promise<RunAuthorization>;
52
- revoke(authorization: RunAuthorization, reason: string): Promise<void>;
53
+ authorize(input: AuthorizeRunInput): ResultAsync<RunAuthorization, GateFailure>;
54
+ revoke(authorization: RunAuthorization, reason: string): ResultAsync<void, NoticeFailure>;
53
55
  /**
54
56
  * Out-of-band revocation for paths holding no `RunAuthorization`. Normal
55
57
  * terminal paths use `revoke(authorization, reason)` under the run
@@ -57,5 +59,5 @@ export interface RunAuthorizer {
57
59
  * credential's JWT was never persisted and only its jti survives on
58
60
  * `cortex_runs`. Local/OSS authorizers no-op — they mint no jti.
59
61
  */
60
- revokeByJti(ref: RevokeByJtiRef, reason: string): Promise<void>;
62
+ revokeByJti(ref: RevokeByJtiRef, reason: string): ResultAsync<void, NoticeFailure>;
61
63
  }
@@ -18,6 +18,7 @@
18
18
  * terminal frame read-side.
19
19
  */
20
20
  import { createNoopLogger } from "../lib/console-logger.js";
21
+ import { deliverNotice } from "../lib/hooks.js";
21
22
  import { elapsedSinceIso, recordRunCompleted, recordStepCompleted } from "../lib/metrics.js";
22
23
  import { unwrapOrThrow } from "../lib/result.js";
23
24
  import { markRunCanceledIfActive, queryRun } from "../state/runs.js";
@@ -113,26 +114,9 @@ export function createRunCanceler(deps) {
113
114
  catch (err) {
114
115
  logger.error("pending-step sweep failed", { runId, ...logger.errorFields(err) });
115
116
  }
116
- // Best-effort: the billing authority self-heals (defensive open + stale
117
- // reaper), so a failed close loses attribution only.
118
- let charge = false;
119
- try {
120
- await runCharge.close({ analysisId: row.analysisId, runId, reason: "canceled", session });
121
- charge = true;
122
- }
123
- catch (err) {
124
- logger.error("running-charge close failed", { runId, ...logger.errorFields(err) });
125
- }
126
- let mandate = row.mandateJti === null;
127
- if (row.mandateJti !== null) {
128
- try {
129
- await runAuthorizer.revokeByJti({ jti: row.mandateJti, auth: session.auth }, EXTERNAL_CANCEL_REASON);
130
- mandate = true;
131
- }
132
- catch (err) {
133
- logger.error("mandate revoke failed", { runId, jti: row.mandateJti, ...logger.errorFields(err) });
134
- }
135
- }
117
+ const charge = await deliverNotice(logger.with({ runId }), "RunCharge.close", runCharge.close({ analysisId: row.analysisId, runId, outcome: { kind: "canceled" }, session }));
118
+ const mandate = row.mandateJti === null ||
119
+ (await deliverNotice(logger.with({ runId, jti: row.mandateJti }), "RunAuthorizer.revokeByJti", runAuthorizer.revokeByJti({ jti: row.mandateJti, auth: session.auth }, EXTERNAL_CANCEL_REASON)));
136
120
  // No transition means the run reached a terminal state on its own (or the
137
121
  // write failed) — report the row as it stands rather than claiming `canceled`.
138
122
  let finalStatus = "canceled";
package/dist/index.d.ts CHANGED
@@ -24,10 +24,10 @@ export type { ConversationAgentDeps } from "./agents/conversation-agent.js";
24
24
  export type { BioToolKeys } from "./tools/bio/keys.js";
25
25
  export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
26
26
  export type { RunAuthorizer, AuthorizeRunInput, RunAuthorization, RevokeByJtiRef } from "./execution/run-authorizer.js";
27
- export { createNoopBillingResolver } from "./billing/noop-resolver.js";
28
- export type { ResolveBilling, ResolvableSession, BillingMap, BillingHeaders, BillingFetchResult } from "./billing/resolver.js";
27
+ export type { GateFailure, NoticeFailure } from "./lib/hooks.js";
28
+ export type { RequestHeaders, ResolveRequestHeaders } from "./providers/request-headers.js";
29
29
  export { createNoopRunCharge } from "./billing/noop-run-charge.js";
30
- export type { RunCharge } from "./billing/run-charge.js";
30
+ export type { RunCharge, RunChargeOutcome } from "./billing/run-charge.js";
31
31
  export { createNoopUsageRecorder } from "./billing/noop-usage-recorder.js";
32
32
  export type { UsageRecorder, LlmUsageRecord } from "./billing/usage-recorder.js";
33
33
  export { createNoopArtifactRegistry } from "./execution/noop-artifact-registry.js";
@@ -35,7 +35,7 @@ export type { ArtifactRegistry, ArtifactRegistrationInput, ArtifactSyncInput, Ex
35
35
  export type { ArtifactManifestEntry } from "./schemas/artifact-manifest.js";
36
36
  export type { ProvenanceCollector } from "./provenance/collector.js";
37
37
  export { UnavailableSessionPagePublisher } from "./tools/report-session/session-page-publisher.js";
38
- export type { MakeSessionPagePublisher, SessionPagePublisher, SessionPageMintResult } from "./tools/report-session/session-page-publisher.js";
38
+ export type { MakeSessionPagePublisher, SessionPageGrant, SessionPageMintFailure, SessionPagePublisher, } from "./tools/report-session/session-page-publisher.js";
39
39
  export type { ResolvePageUrl, SessionPageAccess } from "./tools/report-session/index.js";
40
40
  export { bindReadExport, bindSessionEmit } from "./provenance/seam.js";
41
41
  export type { ProvenanceExport, ProvenanceSeam, RunProvenanceEvent, SessionProvenanceEvent } from "./provenance/seam.js";
@@ -63,12 +63,12 @@ export type { RunAgentOptions, RunAgentResult, AgentFinish } from "./loop/run-ag
63
63
  export type { AgentDefinition, RunStep, EmitFn, EmitEvent, EventSource } from "./loop/types.js";
64
64
  export { passthroughStep } from "./loop/run-step.js";
65
65
  export { forStep, forSubAgent, scopeResource, scopeWorkloadId } from "./auth/types.js";
66
- export type { AgentSession, RequestSession, RunSession, Identity, Scope, Credential, Provenance, RunFrame, AuthContext, ResourceCoordinates, } from "./auth/types.js";
66
+ export type { AgentSession, RequestSession, RunSession, Identity, Scope, Credential, Provenance, RunFrame, AuthContext, ResourceCoordinates, HookSessionView, ResolvableSession, } from "./auth/types.js";
67
67
  export { makeLocalAuth } from "./auth/local-auth-context.js";
68
68
  export { createAnthropicProvider } from "./providers/anthropic.js";
69
69
  export type { AnthropicProviderDeps } from "./providers/anthropic.js";
70
70
  export { createConfiguredAiSdkProvider, DEFAULT_MAX_OUTPUT_TOKENS } from "./providers/ai-sdk.js";
71
- export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps } from "./providers/ai-sdk.js";
71
+ export type { AiSdkProviderConfig, ConfiguredAiSdkProviderDeps, ProviderHostPolicy } from "./providers/ai-sdk.js";
72
72
  export { createEmbeddingProvider } from "./providers/embedding.js";
73
73
  export type { EmbeddingProviderDeps } from "./providers/embedding.js";
74
74
  export type { AgentChat, ChatProvider, EmbeddingProvider, ChatRequest, ChatResponse, ChatStreamEvent, ChatUsage, ModelMessage, PromptCachePolicy, ReasoningPolicy, } from "./providers/types.js";
@@ -76,6 +76,10 @@ export { DEFAULT_PROMPT_CACHE, promptCacheProviderOptions, withPromptCacheBreakp
76
76
  export { DEFAULT_REASONING } from "./providers/reasoning.js";
77
77
  export { toProviderError, isProviderError } from "./providers/errors.js";
78
78
  export type { ProviderError } from "./providers/errors.js";
79
+ export { DEFAULT_SUSPEND_ON } from "./providers/errors.js";
80
+ export type { SuspendOn } from "./providers/errors.js";
81
+ export { suspensionOfFailure } from "./workflows/suspension.js";
82
+ export type { Suspension } from "./workflows/suspension.js";
79
83
  export { createStreamingChat } from "./providers/streaming-chat.js";
80
84
  export { prepareChatTurn } from "./app/chat-turn.js";
81
85
  export type { PrepareChatTurnDeps, PrepareChatTurnParams, PrepareChatTurnResult } from "./app/chat-turn.js";
@@ -137,8 +141,10 @@ export type { ResolveWorkspaceRoot } from "./workspace/paths.js";
137
141
  export { createSandboxClient } from "./sandbox/create-sandbox.js";
138
142
  export type { CreateSandboxClientConfig, SandboxBackendConfig } from "./sandbox/create-sandbox.js";
139
143
  export type { SandboxClient } from "./sandbox/client.js";
140
- export { describeSandboxError, SandboxFailure } from "./sandbox/sandbox-error.js";
141
- export type { SandboxError } from "./sandbox/sandbox-error.js";
144
+ export type { ResolveSandboxLabels, SandboxLabels, SandboxSpec } from "./sandbox/types.js";
145
+ export type { SpawnSession } from "./auth/types.js";
146
+ export { describeSandboxError, keepSuspendingRefusal, SandboxFailure } from "./sandbox/sandbox-error.js";
147
+ export type { SandboxError, SuspendingRefusal } from "./sandbox/sandbox-error.js";
142
148
  export type { ExtendAnalysisFarm, FarmLocation, FarmResolution, FarmSource, PackageRequestOutcome, ResolveAnalysisFarm, ToolchainSource, } from "./sandbox/types.js";
143
149
  export { formatQuery, identityAddress, identityKey, identityOf, parseIdentityKey, parseQuery, pythonIdentity, resolveQuery, rIdentity, } from "./sandbox/package-identity.js";
144
150
  export type { PackageIdentity, PackageQuery, ParseQueryError, PoolIndex, QueryResolution, Track } from "./sandbox/package-identity.js";
package/dist/index.js CHANGED
@@ -29,13 +29,11 @@ export { bootHarness } from "./runtime/boot.js";
29
29
  export { createConversationAgent } from "./agents/conversation-agent.js";
30
30
  // Seam: run authorization.
31
31
  export { createLocalRunAuthorizer } from "./auth/local-run-authorizer.js";
32
- // Seam: billing resolution.
33
- export { createNoopBillingResolver } from "./billing/noop-resolver.js";
34
32
  // Seam: run-level billing bracket.
35
33
  export { createNoopRunCharge } from "./billing/noop-run-charge.js";
36
- // Seam: per-call LLM usage accounting. `record` is fire-and-forget — the loop
37
- // neither awaits nor guards it, so a realization must not throw and must not
38
- // block. Wired as the optional `usageRecorder` of `assembleCoreRuntime`;
34
+ // Seam: per-call LLM usage accounting. `record` is a notice: the loop does not
35
+ // wait for its result, and it logs the reason of an err when the result
36
+ // arrives. Wired as the optional `usageRecorder` of `assembleCoreRuntime`;
39
37
  // unwired, every call is dropped by `createNoopUsageRecorder`.
40
38
  export { createNoopUsageRecorder } from "./billing/noop-usage-recorder.js";
41
39
  // Seam: artifact registration.
@@ -135,6 +133,12 @@ export { DEFAULT_REASONING } from "./providers/reasoning.js";
135
133
  // directly needs: `chatStream` throws a `ProviderError` value (a plain object,
136
134
  // not an `Error`), so a `catch` recognizes it by shape, not `instanceof`.
137
135
  export { toProviderError, isProviderError } from "./providers/errors.js";
136
+ // The suspend map of a provider: an HTTP status to the suspend reason of the
137
+ // host. A map from the host replaces `DEFAULT_SUSPEND_ON`, which holds `402`.
138
+ export { DEFAULT_SUSPEND_ON } from "./providers/errors.js";
139
+ // The suspension of work. A host reads the suspend reason of a failed chat turn
140
+ // with `suspensionOfFailure`, by the kind of the error, never by its text.
141
+ export { suspensionOfFailure } from "./workflows/suspension.js";
138
142
  // `createStreamingChat` wraps a `ChatProvider` as a streaming `AgentChat` (the
139
143
  // type `runAgent`'s `provider` option takes): its `chat` drives the provider's
140
144
  // `chatStream` and forwards each text delta to `onText`, so a same-process host
@@ -248,7 +252,9 @@ export { computeInputSignature } from "./execution/input-signature.js";
248
252
  export { createSandboxClient } from "./sandbox/create-sandbox.js";
249
253
  // The throw of the client seam. An embedder matches the class and reads the
250
254
  // typed variant on `error`, instead of a string match on the message.
251
- export { describeSandboxError, SandboxFailure } from "./sandbox/sandbox-error.js";
255
+ // `createSandbox` gives each failure as an `err`, and `keepSuspendingRefusal`
256
+ // keeps a refusal of the label hook with the suspend flag as a value and throws each other failure.
257
+ export { describeSandboxError, keepSuspendingRefusal, SandboxFailure } from "./sandbox/sandbox-error.js";
252
258
  // The identity of a package, the query that asks for one, and the resolution
253
259
  // between the two. An embedder reads `by_name` of the dependency graph with
254
260
  // these, thus the host lookup, the census, and the graph key never disagree on
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The two kinds of host hook: a gate and a notice.
3
+ *
4
+ * A gate gives a value or a permission that the harness must have before it
5
+ * can continue an operation. A notice reports a fact after the operation. The
6
+ * type of each hook shows its kind:
7
+ *
8
+ * gate: (input) => ResultAsync<T, GateFailure>
9
+ * notice: (input) => ResultAsync<void, NoticeFailure>
10
+ *
11
+ * The harness calls each hook through `passGate` or `deliverNotice`, thus no
12
+ * call site selects its own failure policy, and no call site puts a `try` or a
13
+ * `catch` around a hook. A hook gives a failure as an `err`. A hook whose
14
+ * promise rejects has a defect, and the rejection passes through the helpers
15
+ * with no change.
16
+ *
17
+ * The reason of a failure is the text of the host. The harness carries it and
18
+ * never reads it. The flag `suspend` is part of the contract, thus the harness
19
+ * branches on it.
20
+ */
21
+ import { ResultAsync } from "neverthrow";
22
+ import type { Logger } from "./logger.js";
23
+ export interface GateFailure {
24
+ readonly reason: string;
25
+ /** True when the host asks the harness to suspend the work in place of a failure. */
26
+ readonly suspend: boolean;
27
+ }
28
+ export interface NoticeFailure {
29
+ readonly reason: string;
30
+ }
31
+ export type GateName = "RunAuthorizer.authorize" | "RunCharge.open" | "ArtifactRegistry.register" | "resolveRequestHeaders" | "resolveSandboxLabels";
32
+ export type NoticeName = "RunAuthorizer.revoke" | "RunAuthorizer.revokeByJti" | "RunCharge.close" | "ArtifactRegistry.sync" | "UsageRecorder.record";
33
+ export type GateRefusal = {
34
+ readonly kind: "failed";
35
+ readonly gate: GateName;
36
+ readonly reason: string;
37
+ } | {
38
+ readonly kind: "suspended";
39
+ readonly gate: GateName;
40
+ readonly reason: string;
41
+ };
42
+ /**
43
+ * The host can build its `Result` with its own copy of neverthrow. The helper
44
+ * gives the outcome again as a `Result` of the copy of the harness. Thus the
45
+ * checkpoint recipes (`runtime/result-serialization.ts`) recognize the class,
46
+ * and a replay reads the same outcome.
47
+ */
48
+ export declare function passGate<T>(gate: GateName, result: ResultAsync<T, GateFailure>): ResultAsync<T, GateRefusal>;
49
+ export declare function deliverNotice(log: Logger, notice: NoticeName, result: ResultAsync<void, NoticeFailure>): Promise<boolean>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * The two kinds of host hook: a gate and a notice.
3
+ *
4
+ * A gate gives a value or a permission that the harness must have before it
5
+ * can continue an operation. A notice reports a fact after the operation. The
6
+ * type of each hook shows its kind:
7
+ *
8
+ * gate: (input) => ResultAsync<T, GateFailure>
9
+ * notice: (input) => ResultAsync<void, NoticeFailure>
10
+ *
11
+ * The harness calls each hook through `passGate` or `deliverNotice`, thus no
12
+ * call site selects its own failure policy, and no call site puts a `try` or a
13
+ * `catch` around a hook. A hook gives a failure as an `err`. A hook whose
14
+ * promise rejects has a defect, and the rejection passes through the helpers
15
+ * with no change.
16
+ *
17
+ * The reason of a failure is the text of the host. The harness carries it and
18
+ * never reads it. The flag `suspend` is part of the contract, thus the harness
19
+ * branches on it.
20
+ */
21
+ import { err, ok, ResultAsync } from "neverthrow";
22
+ /**
23
+ * The host can build its `Result` with its own copy of neverthrow. The helper
24
+ * gives the outcome again as a `Result` of the copy of the harness. Thus the
25
+ * checkpoint recipes (`runtime/result-serialization.ts`) recognize the class,
26
+ * and a replay reads the same outcome.
27
+ */
28
+ export function passGate(gate, result) {
29
+ return new ResultAsync(Promise.resolve(result).then((settled) => settled.isOk()
30
+ ? ok(settled.value)
31
+ : err({ kind: settled.error.suspend ? "suspended" : "failed", gate, reason: settled.error.reason })));
32
+ }
33
+ export async function deliverNotice(log, notice, result) {
34
+ const delivered = await result;
35
+ if (delivered.isOk())
36
+ return true;
37
+ log.error("a notice of the host failed", { notice, reason: delivered.error.reason });
38
+ return false;
39
+ }
@@ -4,20 +4,31 @@
4
4
  * The agent loop and execution boundary model failure as values, not throws.
5
5
  * The house rules:
6
6
  *
7
- * 1. `try/catch` lives ONLY inside the thin wrappers at calls to code
8
- * external to ours — a third-party SDK, the `pg` driver, `fs`, the DBOS
9
- * runtime. Those wrappers turn a throw into an `err(DomainError)`.
10
- * Everything above them flows `Result` / `ResultAsync`.
7
+ * 1. `try/catch` lives ONLY at a boundary:
8
+ * - the thin wrappers at calls to code external to ours — a third-party
9
+ * SDK, the `pg` driver, `fs`, the DBOS runtime. Those wrappers turn a
10
+ * throw into an `err(DomainError)`.
11
+ * - an API entry point, where a caller outside the harness gets the
12
+ * outcome.
13
+ * - a workflow body that gets the throw of a failed step or a failed
14
+ * child workflow, and runs its failure path.
15
+ * - the dispatch catch of the loop.
16
+ * Everything else flows `Result` / `ResultAsync`. A host hook has no
17
+ * `try/catch` around it: `passGate` / `deliverNotice` (`lib/hooks.ts`)
18
+ * consume its `Result`.
11
19
  * 2. Absence is NOT an error. A "not found" stays in the ok channel as a
12
20
  * data variant (`ok({ found: false })`), never an `err`. `err` is
13
21
  * reserved for what used to be `throw`n — an unexpected upstream failure.
14
22
  * 3. A `Result` is unwrapped at exactly three edges, each speaking its own
15
23
  * dialect of failure:
16
24
  * - HTTP routes `.match(...)` a `Result` into a response.
17
- * - The DBOS step boundary throws on `err` (`unwrapOrThrow` /
18
- * `resultStep` in `loop/run-step.ts`): durability records a step as
19
- * failed — and retries / fails fast — ONLY on a thrown exception, so
20
- * an `err` crossing `DBOS.runStep` MUST become a throw.
25
+ * - The DBOS step boundary throws on an `err` that must fail or retry
26
+ * the step (`unwrapOrThrow` / `resultStep` in `loop/run-step.ts`):
27
+ * durability records a step as failed — and retries / fails fast —
28
+ * ONLY on a thrown exception. A step can also return a `Result` as
29
+ * its value: `runtime/result-serialization.ts` keeps the `Ok` / `Err`
30
+ * class across the checkpoint, DBOS records the step as a success,
31
+ * and the body MUST use the `err`.
21
32
  * - Tool dispatch maps `err` into an `is_error` `tool_result`
22
33
  * (`loop/run-agent.ts`).
23
34
  *
@@ -43,9 +54,9 @@ export interface DomainError {
43
54
  /**
44
55
  * A thrown wrapper for a structured error value that is not already an
45
56
  * `Error`. The original value rides on `.cause`, so the cause-walking
46
- * classifiers (`classifyProviderError`, `isBudgetExceeded`) still reach the
47
- * `status` / `code` signals they look for after a `Result` is rethrown at a
48
- * boundary.
57
+ * classifiers (`classifyProviderError`, `findSuspendError`) still reach the
58
+ * `status` / `code` / `type` signals they look for after a `Result` is
59
+ * rethrown at a boundary.
49
60
  */
50
61
  export declare class ResultError extends Error {
51
62
  readonly value: unknown;
@@ -4,20 +4,31 @@
4
4
  * The agent loop and execution boundary model failure as values, not throws.
5
5
  * The house rules:
6
6
  *
7
- * 1. `try/catch` lives ONLY inside the thin wrappers at calls to code
8
- * external to ours — a third-party SDK, the `pg` driver, `fs`, the DBOS
9
- * runtime. Those wrappers turn a throw into an `err(DomainError)`.
10
- * Everything above them flows `Result` / `ResultAsync`.
7
+ * 1. `try/catch` lives ONLY at a boundary:
8
+ * - the thin wrappers at calls to code external to ours — a third-party
9
+ * SDK, the `pg` driver, `fs`, the DBOS runtime. Those wrappers turn a
10
+ * throw into an `err(DomainError)`.
11
+ * - an API entry point, where a caller outside the harness gets the
12
+ * outcome.
13
+ * - a workflow body that gets the throw of a failed step or a failed
14
+ * child workflow, and runs its failure path.
15
+ * - the dispatch catch of the loop.
16
+ * Everything else flows `Result` / `ResultAsync`. A host hook has no
17
+ * `try/catch` around it: `passGate` / `deliverNotice` (`lib/hooks.ts`)
18
+ * consume its `Result`.
11
19
  * 2. Absence is NOT an error. A "not found" stays in the ok channel as a
12
20
  * data variant (`ok({ found: false })`), never an `err`. `err` is
13
21
  * reserved for what used to be `throw`n — an unexpected upstream failure.
14
22
  * 3. A `Result` is unwrapped at exactly three edges, each speaking its own
15
23
  * dialect of failure:
16
24
  * - HTTP routes `.match(...)` a `Result` into a response.
17
- * - The DBOS step boundary throws on `err` (`unwrapOrThrow` /
18
- * `resultStep` in `loop/run-step.ts`): durability records a step as
19
- * failed — and retries / fails fast — ONLY on a thrown exception, so
20
- * an `err` crossing `DBOS.runStep` MUST become a throw.
25
+ * - The DBOS step boundary throws on an `err` that must fail or retry
26
+ * the step (`unwrapOrThrow` / `resultStep` in `loop/run-step.ts`):
27
+ * durability records a step as failed — and retries / fails fast —
28
+ * ONLY on a thrown exception. A step can also return a `Result` as
29
+ * its value: `runtime/result-serialization.ts` keeps the `Ok` / `Err`
30
+ * class across the checkpoint, DBOS records the step as a success,
31
+ * and the body MUST use the `err`.
21
32
  * - Tool dispatch maps `err` into an `is_error` `tool_result`
22
33
  * (`loop/run-agent.ts`).
23
34
  *
@@ -32,9 +43,9 @@ import { Err, Ok } from "neverthrow";
32
43
  /**
33
44
  * A thrown wrapper for a structured error value that is not already an
34
45
  * `Error`. The original value rides on `.cause`, so the cause-walking
35
- * classifiers (`classifyProviderError`, `isBudgetExceeded`) still reach the
36
- * `status` / `code` signals they look for after a `Result` is rethrown at a
37
- * boundary.
46
+ * classifiers (`classifyProviderError`, `findSuspendError`) still reach the
47
+ * `status` / `code` / `type` signals they look for after a `Result` is
48
+ * rethrown at a boundary.
38
49
  */
39
50
  export class ResultError extends Error {
40
51
  value;
@@ -49,9 +60,8 @@ function describe(value) {
49
60
  return value.message;
50
61
  if (value && typeof value === "object" && "type" in value) {
51
62
  // Prefer a human `message` field when the structured error carries one
52
- // (e.g. a `ProviderError`) so the wrapped `Error.message` stays
53
- // pattern-matchable (`isBudgetExceeded`'s message backstop); fall back
54
- // to the `type` discriminant otherwise.
63
+ // (e.g. a `ProviderError`) so the wrapped `Error.message` names the
64
+ // failure; fall back to the `type` discriminant otherwise.
55
65
  const message = value.message;
56
66
  if (typeof message === "string" && message.length > 0)
57
67
  return message;
@@ -100,8 +100,8 @@ export interface RunAgentOptions {
100
100
  readonly logger?: Logger;
101
101
  /**
102
102
  * Per-call LLM usage-accounting seam. Omitted falls back to the no-op
103
- * recorder. Delivery is fire-and-forget by contract — the loop neither
104
- * awaits `record` nor guards it, so a realization must not throw or block.
103
+ * recorder. `record` is a notice: the loop does not wait for its result,
104
+ * and it logs the reason of an `err` when the result arrives.
105
105
  */
106
106
  readonly usageRecorder?: UsageRecorder;
107
107
  /**
@@ -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 { deliverNotice } from "../lib/hooks.js";
6
7
  import { ATTR_INFLEXA_TOOL_USE_ID, passThroughSpan, stableSpan } from "../lib/otel-spans.js";
7
8
  import { ResultError } from "../lib/result.js";
8
9
  import { describeZodIssueShapes } from "../lib/zod-issue-shape.js";
@@ -150,8 +151,8 @@ async function runAgentLoop(agent, initial, session, opts) {
150
151
  * reported nothing to account for, so it is folded (to no effect) and left
151
152
  * out of the ledger rather than entered as an all-absent record.
152
153
  *
153
- * Delivery is bare — no `await`, no `try` — because the `UsageRecorder`
154
- * contract forbids `record` to throw or block.
154
+ * `record` is a notice. The loop does not wait for its result, thus a
155
+ * recorder that blocks does not make the run slower.
155
156
  */
156
157
  const accountForCall = (reply, stepName) => {
157
158
  addChatUsage(usage, reply.usage);
@@ -159,7 +160,7 @@ async function runAgentLoop(agent, initial, session, opts) {
159
160
  const reported = reply.usage;
160
161
  if (reported === undefined || !hasReportedUsage(reported))
161
162
  return;
162
- usageRecorder.record({
163
+ void deliverNotice(log, "UsageRecorder.record", usageRecorder.record({
163
164
  recordKey: recordKeyFor(session, opts.invocationId, stepName),
164
165
  agentId: source.agentId,
165
166
  callPath: source.callPath,
@@ -169,7 +170,7 @@ async function runAgentLoop(agent, initial, session, opts) {
169
170
  ...(reply.requestedModelId === undefined ? {} : { requestedModelId: reply.requestedModelId }),
170
171
  ...(reply.servedModelId === undefined ? {} : { servedModelId: reply.servedModelId }),
171
172
  usage: reported,
172
- });
173
+ }));
173
174
  };
174
175
  /** The rollups this loop stamps on its finish — each absent when nothing reported. */
175
176
  const finishUsage = () => ({
@@ -69,7 +69,7 @@ export type RunProvenanceEvent = {
69
69
  runId: string;
70
70
  /**
71
71
  * The body's terminal status. Both boundary sites resolve it through `deriveFinalStatus`,
72
- * which records a budget pause as `"canceled"` — so `"suspended_insufficient_funds"` (a
72
+ * which records a suspension as `"canceled"` — so `"suspended_insufficient_funds"` (a
73
73
  * `RunStatus` member) is never emitted here and is deliberately absent from this narrower
74
74
  * `ExecuteAnalysisFinalStatus`-minus-`"running"` set.
75
75
  */
@@ -1,6 +1,8 @@
1
1
  import { type LanguageModel } from "ai";
2
- import type { ResolveBilling } from "../billing/resolver.js";
2
+ import { type AgentSession } from "../auth/types.js";
3
3
  import type { Logger } from "../lib/logger.js";
4
+ import { type ProviderError, type SuspendOn } from "./errors.js";
5
+ import { type ResolveRequestHeaders } from "./request-headers.js";
4
6
  import type { ChatProvider, FetchLike, ProviderCapabilities } from "./types.js";
5
7
  /**
6
8
  * The harness-owned retry envelope. The AI SDK's built-in retry exposes only a
@@ -30,9 +32,13 @@ export declare const RETRY_MAX_DELAY_MS = 30000;
30
32
  * and the server then enforces the true cap of the model.
31
33
  */
32
34
  export declare const DEFAULT_MAX_OUTPUT_TOKENS = 64000;
33
- export interface AiSdkProviderDeps {
35
+ /** With no `suspendOn`, a provider uses {@link DEFAULT_SUSPEND_ON}. */
36
+ export interface ProviderHostPolicy {
37
+ readonly resolveRequestHeaders?: ResolveRequestHeaders;
38
+ readonly suspendOn?: SuspendOn;
39
+ }
40
+ export interface AiSdkProviderDeps extends ProviderHostPolicy {
34
41
  readonly model: LanguageModel;
35
- readonly resolveBilling: ResolveBilling;
36
42
  readonly capabilities?: Partial<ProviderCapabilities>;
37
43
  readonly logger?: Logger;
38
44
  /**
@@ -193,9 +199,8 @@ export type AiSdkProviderConfig = {
193
199
  */
194
200
  readonly maxRetries?: number;
195
201
  };
196
- export interface ConfiguredAiSdkProviderDeps {
202
+ export interface ConfiguredAiSdkProviderDeps extends ProviderHostPolicy {
197
203
  readonly config: AiSdkProviderConfig;
198
- readonly resolveBilling: ResolveBilling;
199
204
  readonly logger?: Logger;
200
205
  }
201
206
  /**
@@ -207,6 +212,38 @@ export interface ConfiguredAiSdkProviderDeps {
207
212
  * the same retry tick. `random` is injectable so the delay is testable.
208
213
  */
209
214
  export declare function computeRetryDelayMs(error: unknown, exponentialBackoffDelay: number, random?: () => number): number;
215
+ /**
216
+ * A rejection of the request headers hook is a defect of the host, and the
217
+ * harness does not catch it (host-hooks spec): the envelope never retries it,
218
+ * and the outer catch rethrows it unchanged. The hook call has no `catch`, thus
219
+ * this flag marks the rejection: it stays set when the hook call of the attempt
220
+ * did not settle as a value.
221
+ */
222
+ export interface HookCall {
223
+ unsettled: boolean;
224
+ }
225
+ /** A refusal of the hook throws `HeadersRefused`. */
226
+ export declare function headersForAttempt(hook: ResolveRequestHeaders | undefined, session: AgentSession, call: HookCall): Promise<Record<string, string>>;
227
+ export declare function failureOf(e: unknown, workload: string, suspendOn: SuspendOn): ProviderError;
228
+ /**
229
+ * Wrap a single wire attempt in the harness retry envelope. Built per call so
230
+ * the caller's `AbortSignal` reaches the primitive: it rethrows abort errors
231
+ * without retrying and cancels its own backoff sleep when the signal fires, so
232
+ * the retried closure adds no abort handling of its own. `shouldRetry` defers to
233
+ * the harness retryability taxonomy under the `suspendOn` map of the provider —
234
+ * thus a mapped status is never retried — and it never retries a refusal or a
235
+ * rejection of the request headers hook. A non-retried first failure is rethrown untouched,
236
+ * preserving the exact throwable the outer catch classifies. `createRetryError` carries the last real failure on `cause` so
237
+ * that, once retries are exhausted, `toProviderError`'s status walk reaches the
238
+ * true HTTP status instead of stopping at a synthetic wrapper. The attempt
239
+ * counter is per-instance because each call builds its own retry.
240
+ *
241
+ * An aborted caller signal also stops the retry. A wall-clock guard of a caller
242
+ * aborts with a `TimeoutError` reason, which the taxonomy marks as a retryable
243
+ * timeout. Without this guard the envelope would loop an expiry that the caller
244
+ * itself declared.
245
+ */
246
+ export declare function createRetry(signal: AbortSignal | undefined, logger: Logger, maxRetries: number, suspendOn: SuspendOn, hookCall: HookCall): import("@ai-sdk/provider-utils").RetryFunction;
210
247
  export declare function createAiSdkProvider(deps: AiSdkProviderDeps): ChatProvider;
211
248
  /**
212
249
  * Wrap a fetch so that the wait until the response starts does not exceed
@@ -227,6 +264,7 @@ export declare function createAiSdkProvider(deps: AiSdkProviderDeps): ChatProvid
227
264
  * and the behavior stays byte-identical.
228
265
  */
229
266
  export declare function wrapFetchWithRequestTimeout(fetchImpl: FetchLike, requestTimeoutMs: number): FetchLike;
267
+ export declare function hostPolicyOf(deps: ProviderHostPolicy): ProviderHostPolicy;
230
268
  /**
231
269
  * Realize an `AiSdkProviderConfig` into a `ChatProvider` bound to that config's
232
270
  * connection and model. The model is closed into the returned provider here (it