@tangle-network/agent-runtime 0.350.2 → 0.353.2
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/{activation-CfJMyzEs.js → activation-BkRaG5t0.js} +4 -4
- package/dist/{activation-CfJMyzEs.js.map → activation-BkRaG5t0.js.map} +1 -1
- package/dist/{activation-DyiHrTlS.d.ts → activation-CFS31SWI.d.ts} +2 -2
- package/dist/agent.d.ts +1 -1
- package/dist/agent.js +3 -3
- package/dist/{bundle-Ze8pd07a.js → bundle-DeDkJbsr.js} +2 -2
- package/dist/{bundle-Ze8pd07a.js.map → bundle-DeDkJbsr.js.map} +1 -1
- package/dist/candidate-execution/index.js +4 -4
- package/dist/chat-turn.d.ts +2 -0
- package/dist/chat-turn.js +1 -0
- package/dist/{coordination-manager-BkVfWlMz.js → coordination-manager-BrRkc55z.js} +4 -4
- package/dist/{coordination-manager-BkVfWlMz.js.map → coordination-manager-BrRkc55z.js.map} +1 -1
- package/dist/{delegate-DgEie4ad.js → delegate-CAdajnf8.js} +3 -3
- package/dist/{delegate-DgEie4ad.js.map → delegate-CAdajnf8.js.map} +1 -1
- package/dist/durable.d.ts +7 -130
- package/dist/durable.js +24 -952
- package/dist/durable.js.map +1 -1
- package/dist/{errors-wuhGvpE3.js → errors-AzSuGI23.js} +4 -1
- package/dist/errors-AzSuGI23.js.map +1 -0
- package/dist/execution-handle-Bd2_nUB6.js +167 -0
- package/dist/execution-handle-Bd2_nUB6.js.map +1 -0
- package/dist/execution-handle-CujvqgCn.d.ts +130 -0
- package/dist/{graph-Cx0FhKSE.js → graph-CyaIHVGR.js} +7 -6
- package/dist/{graph-Cx0FhKSE.js.map → graph-CyaIHVGR.js.map} +1 -1
- package/dist/{improve-i0HU1-aa.js → improve-DwG-yioQ.js} +10 -9
- package/dist/{improve-i0HU1-aa.js.map → improve-DwG-yioQ.js.map} +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.js +14 -11
- package/dist/index.js.map +1 -1
- package/dist/intelligence.d.ts +2 -2
- package/dist/intelligence.js +12 -10
- package/dist/intelligence.js.map +1 -1
- package/dist/journal-store-BJPMUydp.js +3925 -0
- package/dist/journal-store-BJPMUydp.js.map +1 -0
- package/dist/kernel.d.ts +1 -1
- package/dist/kernel.js +11 -10
- package/dist/{loop-runner-DvfOXVx9.d.ts → loop-runner-D9rcTNf2.d.ts} +2 -2
- package/dist/{loop-runner-BwN9MhdP.js → loop-runner-DwS3jARE.js} +3 -3
- package/dist/{loop-runner-BwN9MhdP.js.map → loop-runner-DwS3jARE.js.map} +1 -1
- package/dist/loop-runner-bin.d.ts +1 -1
- package/dist/loop-runner-bin.js +1 -1
- package/dist/mcp/bin.js +4 -4
- package/dist/mcp/index.d.ts +1 -1
- package/dist/mcp/index.js +8 -8
- package/dist/mcp/memory-bin.js +2 -2
- package/dist/{model-policy-GSxWwP-b.js → model-policy-C-RGAUW4.js} +2 -2
- package/dist/{model-policy-GSxWwP-b.js.map → model-policy-C-RGAUW4.js.map} +1 -1
- package/dist/{prepare-DE8TQnLu.js → prepare-XWwsOh8O.js} +4 -3
- package/dist/{prepare-DE8TQnLu.js.map → prepare-XWwsOh8O.js.map} +1 -1
- package/dist/{profile-CAhZDW2T.js → profile-C01SQZKa.js} +3 -3
- package/dist/{profile-CAhZDW2T.js.map → profile-C01SQZKa.js.map} +1 -1
- package/dist/profiles.d.ts +1 -1
- package/dist/profiles.js +2 -2
- package/dist/{provision-supervisor-Bde_FF0R.js → provision-supervisor-AEnei2Jq.js} +7 -7
- package/dist/{provision-supervisor-Bde_FF0R.js.map → provision-supervisor-AEnei2Jq.js.map} +1 -1
- package/dist/{run-cancellation-BLy_Zz-w.js → run-cancellation-CoJRKvih.js} +2 -2
- package/dist/{run-cancellation-BLy_Zz-w.js.map → run-cancellation-CoJRKvih.js.map} +1 -1
- package/dist/{runtime-6Ec9sHRe.js → runtime-DCYSaYHW.js} +42 -39
- package/dist/{runtime-6Ec9sHRe.js.map → runtime-DCYSaYHW.js.map} +1 -1
- package/dist/{sandbox-profile-compat-BxMQ1VXv.js → sandbox-profile-compat-CXjozdWL.js} +4 -623
- package/dist/{sandbox-profile-compat-BxMQ1VXv.js.map → sandbox-profile-compat-CXjozdWL.js.map} +1 -1
- package/dist/{search-executor-B7g_5Wjl.js → search-executor-B2jc63hZ.js} +14 -11
- package/dist/{search-executor-B7g_5Wjl.js.map → search-executor-B2jc63hZ.js.map} +1 -1
- package/dist/{server-fvEMwGMQ.js → server-CKBTwWM8.js} +6 -6
- package/dist/{server-fvEMwGMQ.js.map → server-CKBTwWM8.js.map} +1 -1
- package/dist/{structural-rollout-B86bSixU.js → structural-rollout-rRT3ypqz.js} +7 -5
- package/dist/{structural-rollout-B86bSixU.js.map → structural-rollout-rRT3ypqz.js.map} +1 -1
- package/dist/{supervise-CaeZcgAW.js → supervise-B3_wtgHS.js} +18 -21
- package/dist/supervise-B3_wtgHS.js.map +1 -0
- package/dist/supervision-discovery-CewADdEs.js +772 -0
- package/dist/supervision-discovery-CewADdEs.js.map +1 -0
- package/dist/{supervisor-C1OePYE-.js → supervisor-C_sDmX5H.js} +156 -3984
- package/dist/supervisor-C_sDmX5H.js.map +1 -0
- package/dist/testing.d.ts +1 -1
- package/dist/testing.js +13 -13
- package/dist/{tool-server-CwuYeW_Z.js → tool-server-CZWd_Opp.js} +2 -2
- package/dist/{tool-server-CwuYeW_Z.js.map → tool-server-CZWd_Opp.js.map} +1 -1
- package/dist/{top-snapshot-ChwpTr7A.js → top-snapshot-DcV7VG4m.js} +178 -161
- package/dist/top-snapshot-DcV7VG4m.js.map +1 -0
- package/dist/tui/bin.js +45 -74
- package/dist/tui/bin.js.map +1 -1
- package/dist/tui/index.d.ts +25 -54
- package/dist/tui/index.js +2 -2
- package/dist/{types-Fowuwdz2.d.ts → types-DTHxh4X9.d.ts} +47 -63
- package/dist/util-ebWMWzY0.js +624 -0
- package/dist/util-ebWMWzY0.js.map +1 -0
- package/dist/workspace-store/index.js +9 -3
- package/dist/workspace-store/index.js.map +1 -1
- package/package.json +18 -13
- package/dist/errors-wuhGvpE3.js.map +0 -1
- package/dist/record-events-B2GUgYdz.js +0 -261
- package/dist/record-events-B2GUgYdz.js.map +0 -1
- package/dist/supervise-CaeZcgAW.js.map +0 -1
- package/dist/supervisor-C1OePYE-.js.map +0 -1
- package/dist/top-snapshot-ChwpTr7A.js.map +0 -1
|
@@ -1,19 +1,20 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { c as RetainedInteractiveAdmissionError, d as RetainedRunDispatchBindingError, f as RuntimeRunStateError, i as ConfigError, l as RetainedInteractiveBindingError, m as ValidationError$1, r as BackendTransportError, t as AgentEvalError$1, u as RetainedRunAdmissionError } from "./errors-
|
|
3
|
-
import {
|
|
1
|
+
import { $ as traceRootWriter, A as runtimeProviderCode, At as sandboxLeafOutputFromEvents, B as retainedExecutorContext, Bt as createSandboxUsageLedger, D as runtimeFailureCause, Dt as createSandboxToolPartState, E as reportedFailureCause, Et as assertValidSpend, F as executorEvidenceWriter, Ft as extractTransportEventIdentity, Gt as sandboxTerminalUsageField, H as workspaceCaptureWriter, Ht as isSandboxTerminalEvent, I as isProvisioningRefusal, It as parseCanonicalTransportEvent, Jt as parseCodexUsageRecord, L as leafContinuationExecutionId, Lt as assertSandboxServedModel, M as BUDGET_CONTINUATION_TASK, Mt as sandboxProgressEvents, N as DISPATCH_NOT_ADMITTED_CODE, P as RetainedExecutionPendingError, R as prepareRetainedExecutor, Rt as canonicalSandboxUsageMode, St as newUsageTotals, T as sha256, U as UNAVAILABLE_CODES, Ut as notifySandboxEventObserver, V as retainedExecutorSeamKey, W as UNAVAILABLE_STATUSES, Wt as sandboxEventServedBackend, X as unavailableSignalOfFailure, Y as unavailableSignalInText, Yt as recordValue, _ as publishExclusiveDurableFile, a as isNoEntError, at as executorFailure, b as captureRunWriteContext, bt as createBudgetPool, ct as hashJson, d as SpawnEventIndex, dt as causeChain, f as closesCursorSlot, ft as errMessage, g as assertNoSymlinkDescendant, gt as defaultRedactor, ht as errorText, it as contentRef, j as settlementFailureCause, jt as sandboxOutputMarker, k as runtimeFailureRemediationSchema, lt as credentialCommandExit, m as outsideCursorNamespace, mt as errorProperty, n as appendSpawnEvents, nt as shareParsedStrings, ot as executorFailureInfra, p as insideCursorNamespace, pt as errorHttpStatus, q as unavailablePauseMs, r as spawnJournalRecordEvents, rt as contentAddress, st as executorFailureReason, t as FileSpawnJournal, tt as detachedSnapshot, ut as isCredentialRefusal, vt as resolveRedactor, wt as spendFromUsageTotals, x as fenceRunWrite, xt as meterUsageEvent, y as writeAtomicDurableFile, yt as BudgetReconcileFault, zt as createSandboxTurnEventCollector } from "./journal-store-BJPMUydp.js";
|
|
2
|
+
import { c as RetainedInteractiveAdmissionError, d as RetainedRunDispatchBindingError, f as RuntimeRunStateError, i as ConfigError, l as RetainedInteractiveBindingError, m as ValidationError$1, r as BackendTransportError, t as AgentEvalError$1, u as RetainedRunAdmissionError } from "./errors-AzSuGI23.js";
|
|
3
|
+
import { A as ACCEPTED_RESULT_GRACE_MS, B as teardownExecutor, C as withTimeout, E as addResourceSpend, F as armDeadlineTimer, I as boundedChildDeadlineAt, L as checkInIdleMs, O as resourceTelemetry, P as ROOT_DEADLINE_REASON, R as isDeadlineAbortReason, S as unmeteredSpend, T as zeroTokenUsage, V as teardownSurfaces, _ as roundBoxMinutes, a as cloneSpend, b as throwAbort, c as errorMessage, d as isAbortError$1, f as isAsyncIterable, g as randomUuid, h as randomSuffix, i as chargedTokens, j as CHILD_DEADLINE_REASON, k as withBudgetResources, m as promptCacheTokenClasses, n as addSpend, o as cloneTokenUsage, p as mapWithConcurrency, r as addTokenUsage, s as deleteBoxSafe, t as abortError, v as sleep, w as zeroSpend, x as throwIfAborted, y as stringifySafe, z as singleFlightTeardown } from "./util-ebWMWzY0.js";
|
|
4
4
|
import { runTaskQueue } from "./task-queue.js";
|
|
5
|
-
import { _ as agentHarness, a as concreteProfileModel, c as profileModelExecutionSettings, d as assertRouterAutoDeclaration, f as assertRouterAutoResponse, g as observedModelMatchesDeclared, h as observedModelHasSnapshot, l as profileProviderModel, m as mergeObservedModelIdentity, o as enforceTokenLimits, s as profileBridgeWireModel, t as assertExecutableAgentProfile, u as resolveRouterRetryPolicy } from "./model-policy-
|
|
5
|
+
import { _ as agentHarness, a as concreteProfileModel, c as profileModelExecutionSettings, d as assertRouterAutoDeclaration, f as assertRouterAutoResponse, g as observedModelMatchesDeclared, h as observedModelHasSnapshot, l as profileProviderModel, m as mergeObservedModelIdentity, o as enforceTokenLimits, s as profileBridgeWireModel, t as assertExecutableAgentProfile, u as resolveRouterRetryPolicy } from "./model-policy-C-RGAUW4.js";
|
|
6
|
+
import { B as sha256Bytes$1, H as hasControlCharacter, I as deepFreezeCandidate, M as verifyWorkspaceSnapshotArtifacts, U as nativeSystemPrompt, c as verifyAgentCandidateWorkspaceTree, g as verifiedArtifactChunks, o as readAgentCandidateWorkspaceArtifactFiles, s as verifyAgentCandidateWorkspaceArtifacts, t as assertSandboxProfileCompatibility, v as verifyAgentCandidateWorkspaceArchive } from "./sandbox-profile-compat-CXjozdWL.js";
|
|
6
7
|
import { i as notifyRuntimeHookEvent } from "./runtime-hooks-CsW2pfFk.js";
|
|
7
|
-
import
|
|
8
|
+
import * as traceContract from "@tangle-network/agent-trace-contract";
|
|
9
|
+
import { ATTR, OPERATION_NAME_ATTR, toW3CSpanId, toW3CTraceId } from "@tangle-network/agent-trace-contract";
|
|
10
|
+
import { AGENT_PROFILE_MATERIALIZATION_AXES, AgentEnvironmentCapabilitiesSchema, AgentExactRunControlRefSchema, AgentInteractiveSessionControlClaimAcknowledgementSchema, AgentInteractiveSessionControlClaimSchema, AgentInteractiveSessionPromptAcknowledgementSchema, AgentInteractiveSessionRefSchema, AgentInteractiveSessionStatusSchema, AgentInteractiveSessionStopAcknowledgementSchema, AgentNativeContextContinuationResultSchema, AgentRunCancellationAcknowledgementSchema, AgentRunCancellationRequestSchema, AgentTurnInputSchema, AgentTurnResultSchema, ContextTransferReceiptSchema, ContextTransferRequestSchema, InteractionAcknowledgementSchema, InteractionResponseCommandSchema, NativeContextBoundaryProofSchema, NativeContextContinuationRequestSchema, REASONING_EFFORTS, RuntimeEventEnvelopeSchema, TerminalReplayWindowSchema, TerminalSessionRefSchema, agentCandidateArtifactRefSchema, agentCandidateWorkspaceManifestMaterialSchema, agentCandidateWorkspaceSnapshotEvidenceSchema, agentInteractiveSessionControlClaimAcknowledgementMatchesRequest, agentInteractiveSessionControlClaimMatchesRef, agentInteractiveSessionPromptAcknowledgementMatchesCommand, agentInteractiveSessionRefMatchesStart, agentInteractiveSessionRunRef, agentInteractiveSessionStatusMatchesRef, agentInteractiveSessionStopAcknowledgementMatchesCommand, agentNativeContextContinuationResultMatchesRequest, agentProfileSchema, agentRunCancellationAcknowledgementMatchesRequest, agentRunCancellationRequestDigest, agentWorkspaceSnapshotRefDigest, canonicalAgentProfileDigest, canonicalCandidateDigest, canonicalWorkspaceCwd, contextTransferReceiptMatches, exactAgentInteractiveSessionStart, harnessTypeSchema, nativeContextContinuationTurnDigest, nativeReasoningControl, profileMaterializationAxes, profileMaterializationAxes as profileMaterializationAxes$1, renderInputPartsAsText, sha256DigestSchema, workspaceCheckpointRequestDigest, workspaceCheckpointResultMatchesRequest, workspaceCleanupAcknowledgementMatches, workspaceCleanupRequestDigest, workspaceForkRequestDigest, workspaceForkResultMatchesRequest } from "@tangle-network/agent-interface";
|
|
8
11
|
import { isToolSpan, toolSpansToTraceAnalysisStore } from "@tangle-network/agent-eval";
|
|
9
12
|
import { createAgentRunOutcomeTracker } from "@tangle-network/sandbox/runtime";
|
|
10
13
|
import { SDKError, generateIdempotencyKey, isRecord, withRetry } from "@tangle-network/agent-core";
|
|
11
|
-
import { collectAgentFinalMessageText, collectAgentResponseText } from "@tangle-network/sandbox";
|
|
12
14
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
15
|
+
import { redact } from "@tangle-network/agent-eval/traces";
|
|
14
16
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
15
|
-
import {
|
|
16
|
-
import { appendFileSync, closeSync, constants, createReadStream, existsSync, fstatSync, fsyncSync, ftruncateSync, mkdirSync, openSync, readFileSync, readSync, statSync, writeSync } from "node:fs";
|
|
17
|
+
import { appendFileSync, closeSync, constants, createReadStream, existsSync, mkdirSync, openSync, readFileSync } from "node:fs";
|
|
17
18
|
import { basename, delimiter, dirname, isAbsolute, join, relative, resolve, sep, win32 } from "node:path";
|
|
18
19
|
import { spawn } from "node:child_process";
|
|
19
20
|
import { access, chmod, copyFile, mkdir, mkdtemp, open, readFile, readdir, realpath, rm, rmdir, stat, symlink, writeFile } from "node:fs/promises";
|
|
@@ -24,8 +25,8 @@ import { request } from "node:http";
|
|
|
24
25
|
import { request as request$1 } from "node:https";
|
|
25
26
|
import { Readable } from "node:stream";
|
|
26
27
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
27
|
-
import {
|
|
28
|
-
import { clearImmediate, setImmediate
|
|
28
|
+
import { decodeOtlpAttributes, msToUnixNano, otlpTraceExport, readOtlpPartialSuccess, toOtlpAttributes } from "@tangle-network/agent-trace-contract/otlp";
|
|
29
|
+
import { clearImmediate, setImmediate } from "node:timers";
|
|
29
30
|
// src/runtime/turn-input.ts
|
|
30
31
|
/**
|
|
31
32
|
* Copy only fields that describe a new provider turn.
|
|
@@ -311,3073 +312,6 @@ function delay(ms, signal) {
|
|
|
311
312
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
312
313
|
});
|
|
313
314
|
}
|
|
314
|
-
// src/record-value.ts
|
|
315
|
-
function recordValue(value) {
|
|
316
|
-
return isRecord(value) ? value : void 0;
|
|
317
|
-
}
|
|
318
|
-
// src/runtime/harness-usage.ts
|
|
319
|
-
/**
|
|
320
|
-
* Read and validate one codex `turn.completed` usage record.
|
|
321
|
-
*
|
|
322
|
-
* Every counter must be a non-negative safe integer. Two cross-field invariants hold, and they are
|
|
323
|
-
* what states that the two named counters are SUBSETS of their totals rather than additions to
|
|
324
|
-
* them: `cached_input_tokens <= input_tokens` and `reasoning_output_tokens <= output_tokens`.
|
|
325
|
-
* Measured against the codex CLI: a turn reporting `output_tokens 1523` with
|
|
326
|
-
* `reasoning_output_tokens 1516` answered with about seven tokens of text.
|
|
327
|
-
*
|
|
328
|
-
* `label` names the surface the record came from, so one message serves both readers.
|
|
329
|
-
* Throws `ValidationError` on any violation.
|
|
330
|
-
*/
|
|
331
|
-
function parseCodexUsageRecord(usage, label) {
|
|
332
|
-
const record = recordValue(usage);
|
|
333
|
-
if (record === void 0) throw new ValidationError$1(`${label}: usage must be an object, received ${describe$1(usage)}`);
|
|
334
|
-
const inputTokens = naturalNumber$1(record.input_tokens, "input_tokens", label);
|
|
335
|
-
const cachedInputTokens = naturalNumber$1(record.cached_input_tokens, "cached_input_tokens", label);
|
|
336
|
-
const outputTokens = naturalNumber$1(record.output_tokens, "output_tokens", label);
|
|
337
|
-
const reasoningOutputTokens = naturalNumber$1(record.reasoning_output_tokens, "reasoning_output_tokens", label);
|
|
338
|
-
const cacheWriteInputTokens = record.cache_write_input_tokens === void 0 ? void 0 : naturalNumber$1(record.cache_write_input_tokens, "cache_write_input_tokens", label);
|
|
339
|
-
if (cachedInputTokens > inputTokens) throw new ValidationError$1(`${label}: cached_input_tokens exceeds input_tokens`);
|
|
340
|
-
if (reasoningOutputTokens > outputTokens) throw new ValidationError$1(`${label}: reasoning_output_tokens exceeds output_tokens`);
|
|
341
|
-
return {
|
|
342
|
-
inputTokens,
|
|
343
|
-
cachedInputTokens,
|
|
344
|
-
outputTokens,
|
|
345
|
-
reasoningOutputTokens,
|
|
346
|
-
...cacheWriteInputTokens === void 0 ? {} : { cacheWriteInputTokens }
|
|
347
|
-
};
|
|
348
|
-
}
|
|
349
|
-
/** Names the wire record every codex decode error is reported against. */
|
|
350
|
-
const codexUsageContext = "codex turn.completed";
|
|
351
|
-
/**
|
|
352
|
-
* codex reports the tokens of a turn inside its own `turn.completed` event, which rides the
|
|
353
|
-
* transport either as `{ type: 'raw', data: { type: 'turn.completed', usage: { … } } }` or as
|
|
354
|
-
* the provider-normalized Interface envelope `{ type: 'raw', data: { type: 'raw', event: {
|
|
355
|
-
* type: 'turn.completed', usage: { … } } } }`. It emits no canonical usage event, so this adapter
|
|
356
|
-
* is a codex worker's only usage source.
|
|
357
|
-
*
|
|
358
|
-
* The record is read by `parseCodexUsageRecord`, the same reader `runLocalHarness` uses on the
|
|
359
|
-
* codex CLI's own stdout, so both surfaces hold one field policy and both cross-field invariants.
|
|
360
|
-
*
|
|
361
|
-
* A `turn.completed` that carries a `usage` member the reader cannot read is a `ValidationError`
|
|
362
|
-
* naming the field. A `turn.completed` with no `usage` member reports no usage at all, which
|
|
363
|
-
* leaves the turn's tokens unknown rather than zero.
|
|
364
|
-
*/
|
|
365
|
-
const decodeCodexTurnUsage = (event) => {
|
|
366
|
-
if (String(event.type ?? "") !== "raw") return void 0;
|
|
367
|
-
const envelope = recordValue(event.data);
|
|
368
|
-
const data = envelope?.type === "raw" && recordValue(envelope.event) !== void 0 ? recordValue(envelope.event) : envelope;
|
|
369
|
-
if (data === void 0 || data.type !== "turn.completed" || data.usage === void 0) return;
|
|
370
|
-
const record = parseCodexUsageRecord(data.usage, codexUsageContext);
|
|
371
|
-
return {
|
|
372
|
-
harness: "codex",
|
|
373
|
-
input: record.inputTokens,
|
|
374
|
-
output: record.outputTokens,
|
|
375
|
-
cachedInput: record.cachedInputTokens,
|
|
376
|
-
reasoningOutput: record.reasoningOutputTokens,
|
|
377
|
-
...record.cacheWriteInputTokens === void 0 ? {} : { cacheWriteInput: record.cacheWriteInputTokens }
|
|
378
|
-
};
|
|
379
|
-
};
|
|
380
|
-
/** Claude's adapter forwards one delta per response snapshot, keyed and deduplicated there. */
|
|
381
|
-
const decodeClaudeAssistantUsage = (event) => {
|
|
382
|
-
if (String(event.type ?? "") !== "raw") return void 0;
|
|
383
|
-
const envelope = recordValue(event.data);
|
|
384
|
-
const data = envelope?.type === "raw" && recordValue(envelope.event) !== void 0 ? recordValue(envelope.event) : envelope;
|
|
385
|
-
if (data?.type !== "assistant.usage") return void 0;
|
|
386
|
-
if (typeof data.message_id !== "string" || data.message_id.length === 0) throw new ValidationError$1("claude assistant.usage: missing response identity");
|
|
387
|
-
const usage = recordValue(data.usage);
|
|
388
|
-
if (!usage) throw new ValidationError$1("claude assistant.usage: usage must be an object");
|
|
389
|
-
const fresh = naturalNumber$1(usage.input_tokens, "input_tokens", "claude assistant.usage");
|
|
390
|
-
const cachedInput = naturalNumber$1(usage.cache_read_input_tokens, "cache_read_input_tokens", "claude assistant.usage");
|
|
391
|
-
const cacheWriteInput = naturalNumber$1(usage.cache_creation_input_tokens, "cache_creation_input_tokens", "claude assistant.usage");
|
|
392
|
-
const output = naturalNumber$1(usage.output_tokens, "output_tokens", "claude assistant.usage");
|
|
393
|
-
const input = fresh + cachedInput + cacheWriteInput;
|
|
394
|
-
if (!Number.isSafeInteger(input)) throw new ValidationError$1("claude assistant.usage: total input tokens exceed a safe integer");
|
|
395
|
-
return {
|
|
396
|
-
harness: "claude-code",
|
|
397
|
-
input,
|
|
398
|
-
output,
|
|
399
|
-
cachedInput,
|
|
400
|
-
cacheWriteInput
|
|
401
|
-
};
|
|
402
|
-
};
|
|
403
|
-
/**
|
|
404
|
-
* The harness → usage-decoder registry. Add a harness by adding one entry.
|
|
405
|
-
*
|
|
406
|
-
* Keyed by `HarnessType`, the vocabulary the `harness` argument is drawn from, so a key no caller
|
|
407
|
-
* can produce does not compile. A harness absent from this registry reports usage through the
|
|
408
|
-
* canonical events or not at all.
|
|
409
|
-
*/
|
|
410
|
-
const harnessUsageDecoders = {
|
|
411
|
-
codex: decodeCodexTurnUsage,
|
|
412
|
-
"claude-code": decodeClaudeAssistantUsage
|
|
413
|
-
};
|
|
414
|
-
/**
|
|
415
|
-
* Decode a sandbox event with one harness's adapter, or `undefined` when the event carries no
|
|
416
|
-
* harness-native usage.
|
|
417
|
-
*
|
|
418
|
-
* A NAMED harness reads with that harness's adapter only, and a named harness with no adapter
|
|
419
|
-
* reports nothing. It never falls through to another harness's adapter: a different harness's
|
|
420
|
-
* `turn.completed` decoded as codex would either drop the counters codex does not name or fail on
|
|
421
|
-
* a field codex requires, and both answers would be about the wrong harness. The composite over
|
|
422
|
-
* every registered adapter runs only when the caller cannot name the harness.
|
|
423
|
-
*
|
|
424
|
-
* Throws `ValidationError` when an adapter recognizes the event as its harness's usage carrier and
|
|
425
|
-
* cannot read the numbers.
|
|
426
|
-
*/
|
|
427
|
-
function decodeHarnessUsage(event, harness) {
|
|
428
|
-
if (!event || typeof event !== "object") return void 0;
|
|
429
|
-
if (harness !== void 0) return harnessUsageDecoders[harness]?.(event);
|
|
430
|
-
for (const decode of new Set(Object.values(harnessUsageDecoders))) {
|
|
431
|
-
const usage = decode(event);
|
|
432
|
-
if (usage !== void 0) return usage;
|
|
433
|
-
}
|
|
434
|
-
}
|
|
435
|
-
function naturalNumber$1(value, field, label) {
|
|
436
|
-
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) throw new ValidationError$1(`${label}: usage.${field} must be a non-negative safe integer, received ${describe$1(value)}`);
|
|
437
|
-
return value;
|
|
438
|
-
}
|
|
439
|
-
function describe$1(value) {
|
|
440
|
-
if (value === null) return "null";
|
|
441
|
-
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
442
|
-
if (typeof value === "string") return JSON.stringify(value);
|
|
443
|
-
if (Array.isArray(value)) return "an array";
|
|
444
|
-
return typeof value;
|
|
445
|
-
}
|
|
446
|
-
// src/runtime/sandbox-events.ts
|
|
447
|
-
/**
|
|
448
|
-
* Forward a sandbox event to an optional observer without letting observer
|
|
449
|
-
* behavior affect the run. The observer receives a defensive copy, synchronous
|
|
450
|
-
* throws are swallowed, and returned promises are deliberately not awaited.
|
|
451
|
-
*/
|
|
452
|
-
function notifySandboxEventObserver(event, observer, meta) {
|
|
453
|
-
if (!observer) return;
|
|
454
|
-
try {
|
|
455
|
-
const result = observer(cloneEventForObserver(event), meta);
|
|
456
|
-
if (result && typeof result.then === "function") result.then(void 0, () => {});
|
|
457
|
-
} catch {}
|
|
458
|
-
}
|
|
459
|
-
function cloneEventForObserver(event) {
|
|
460
|
-
try {
|
|
461
|
-
return structuredClone(event);
|
|
462
|
-
} catch {
|
|
463
|
-
return copyPlainSpine(event, /* @__PURE__ */ new WeakMap());
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
function copyPlainSpine(value, seen) {
|
|
467
|
-
if (value === null || typeof value !== "object") return value;
|
|
468
|
-
const existing = seen.get(value);
|
|
469
|
-
if (existing !== void 0) return existing;
|
|
470
|
-
if (Array.isArray(value)) {
|
|
471
|
-
const copy = [];
|
|
472
|
-
seen.set(value, copy);
|
|
473
|
-
for (const item of value) copy.push(copyPlainSpine(item, seen));
|
|
474
|
-
return copy;
|
|
475
|
-
}
|
|
476
|
-
const proto = Object.getPrototypeOf(value);
|
|
477
|
-
if (proto !== Object.prototype && proto !== null) return {};
|
|
478
|
-
const copy = {};
|
|
479
|
-
seen.set(value, copy);
|
|
480
|
-
for (const [key, child] of Object.entries(value)) copy[key] = copyPlainSpine(child, seen);
|
|
481
|
-
return copy;
|
|
482
|
-
}
|
|
483
|
-
/**
|
|
484
|
-
* Read the served execution identity off one Sandbox event.
|
|
485
|
-
*
|
|
486
|
-
* The platform reports `effectiveBackend` on `execution.started` and again on the terminal
|
|
487
|
-
* event (`@tangle-network/sandbox`, `EffectiveBackend`). Absence returns `undefined`
|
|
488
|
-
* and must stay unknown — a request is not a receipt, so nothing here may be inferred from
|
|
489
|
-
* what was asked for.
|
|
490
|
-
*/
|
|
491
|
-
function sandboxEventServedBackend(event) {
|
|
492
|
-
if (!event || typeof event !== "object") return void 0;
|
|
493
|
-
const served = recordValue(recordValue(event.data)?.effectiveBackend);
|
|
494
|
-
if (!served) return void 0;
|
|
495
|
-
const provider = firstString(served.provider);
|
|
496
|
-
const model = firstString(served.model);
|
|
497
|
-
const source = firstString(served.source);
|
|
498
|
-
if (provider === void 0 && model === void 0) return void 0;
|
|
499
|
-
return {
|
|
500
|
-
...provider !== void 0 ? { provider } : {},
|
|
501
|
-
...model !== void 0 ? { model } : {},
|
|
502
|
-
...source !== void 0 ? { source } : {}
|
|
503
|
-
};
|
|
504
|
-
}
|
|
505
|
-
/**
|
|
506
|
-
* Fail the execution when the platform reports serving a model other than the exact one asked for.
|
|
507
|
-
*
|
|
508
|
-
* Measured motive (agent-runtime#892, live infrastructure): 6 of 6 boxes whose profile
|
|
509
|
-
* declared `zai-coding-plan/glm-5.2` reported
|
|
510
|
-
* `{"provider":"openai-compat","model":"deepseek/deepseek-v4-flash","source":"environment"}`,
|
|
511
|
-
* while the materialization receipt recorded the declared model as `status: "known"`. Sending
|
|
512
|
-
* `backend.model` makes that substitution unlikely; only reading the report back makes it
|
|
513
|
-
* detectable. A run that cannot say which model produced its evidence must not settle as one
|
|
514
|
-
* that can.
|
|
515
|
-
*
|
|
516
|
-
* Silent when the platform reports no served model: unobserved stays unobserved.
|
|
517
|
-
*/
|
|
518
|
-
function assertSandboxServedModel(event, expected) {
|
|
519
|
-
const wanted = expected?.model?.trim();
|
|
520
|
-
if (!wanted) return;
|
|
521
|
-
const served = sandboxEventServedBackend(event);
|
|
522
|
-
if (served?.model === void 0) return;
|
|
523
|
-
if (sameModelId(served.model, wanted, [served.provider, expected?.provider])) return;
|
|
524
|
-
const attribution = [served.provider !== void 0 ? `provider ${JSON.stringify(served.provider)}` : void 0, served.source !== void 0 ? `source ${JSON.stringify(served.source)}` : void 0].filter((part) => part !== void 0);
|
|
525
|
-
const detail = attribution.length > 0 ? ` (${attribution.join(", ")})` : "";
|
|
526
|
-
throw new Error(`sandbox served model ${JSON.stringify(served.model)}${detail} instead of the exact profile model ${JSON.stringify(wanted)}`);
|
|
527
|
-
}
|
|
528
|
-
/**
|
|
529
|
-
* Do two model ids name the same model, allowing for routing prefixes?
|
|
530
|
-
*
|
|
531
|
-
* Either side may spell the model bare (`glm-5.2`), provider-qualified
|
|
532
|
-
* (`zai-coding-plan/glm-5.2`), or route-qualified, and the platform reports the provider in its
|
|
533
|
-
* own field rather than always in the id. So the comparison drops any known provider prefix and
|
|
534
|
-
* then accepts a `/`-boundary suffix match: a longer route to the SAME leaf model is a routing
|
|
535
|
-
* difference, not a substitution.
|
|
536
|
-
*
|
|
537
|
-
* What it deliberately does NOT absorb is a different leaf: `glm-5.2` against `glm-5.3` stays a
|
|
538
|
-
* mismatch, which is the substitution measured directly against the provider API (a request for
|
|
539
|
-
* `glm-5.2` was served `glm-5.3`). A version suffix is the whole difference between two
|
|
540
|
-
* instruments, so nothing here may treat it as noise.
|
|
541
|
-
*/
|
|
542
|
-
function sameModelId(served, wanted, providers) {
|
|
543
|
-
const a = stripProviderPrefix(served, providers);
|
|
544
|
-
const b = stripProviderPrefix(wanted, providers);
|
|
545
|
-
return a === b || a.endsWith(`/${b}`) || b.endsWith(`/${a}`);
|
|
546
|
-
}
|
|
547
|
-
function stripProviderPrefix(id, providers) {
|
|
548
|
-
let out = id.trim().toLowerCase();
|
|
549
|
-
for (const provider of providers) {
|
|
550
|
-
const prefix = provider?.trim().toLowerCase();
|
|
551
|
-
if (prefix && out.startsWith(`${prefix}/`)) out = out.slice(prefix.length + 1);
|
|
552
|
-
}
|
|
553
|
-
return out;
|
|
554
|
-
}
|
|
555
|
-
function firstString(...values) {
|
|
556
|
-
return values.find((value) => typeof value === "string" && value.length > 0);
|
|
557
|
-
}
|
|
558
|
-
const terminalTypes = /* @__PURE__ */ new Set([
|
|
559
|
-
"message.completed",
|
|
560
|
-
"result",
|
|
561
|
-
"final",
|
|
562
|
-
"done"
|
|
563
|
-
]);
|
|
564
|
-
/** True for an event type that ends a sandbox turn. */
|
|
565
|
-
function isSandboxTerminalEvent(type) {
|
|
566
|
-
return terminalTypes.has(type);
|
|
567
|
-
}
|
|
568
|
-
/**
|
|
569
|
-
* Which member of a terminal event's `data` carries its usage receipt.
|
|
570
|
-
*
|
|
571
|
-
* The one place the terminal types legitimately differ, stated rather than implied by two lists:
|
|
572
|
-
* sandbox 0.4.0's `done` reports under `tokenUsage` with the cost at the top level, every other
|
|
573
|
-
* terminal type reports under `usage`.
|
|
574
|
-
*/
|
|
575
|
-
function sandboxTerminalUsageField(type) {
|
|
576
|
-
return type === "done" ? "tokenUsage" : "usage";
|
|
577
|
-
}
|
|
578
|
-
/**
|
|
579
|
-
* Extract a `RuntimeStreamEvent`-shaped `llm_call` from a sandbox event when
|
|
580
|
-
* the event carries usage/cost data. Returns `undefined` for non-cost events
|
|
581
|
-
* so the kernel can iterate the full stream without branching.
|
|
582
|
-
*
|
|
583
|
-
* Pure by contract: it never throws on a failed run. The terminal truth
|
|
584
|
-
* boundary is the public Sandbox outcome tracker, applied after the complete
|
|
585
|
-
* stream. Post-hoc readers — {@link sumSandboxUsage}, the
|
|
586
|
-
* analyst trace store, the chat projection — must stay able to read a failed
|
|
587
|
-
* turn's events, which is when reading them matters most.
|
|
588
|
-
*
|
|
589
|
-
* Canonical cost-carrying types observed in the wild:
|
|
590
|
-
* - `llm_call` — `data: { model, tokensIn, tokensOut, costUsd, ... }`
|
|
591
|
-
* - `message.completed` / `result` — `data: { usage: { inputTokens,
|
|
592
|
-
* outputTokens, totalCostUsd? } }`
|
|
593
|
-
* - `cost.usage` / `usage` — same shape under a dedicated type
|
|
594
|
-
*
|
|
595
|
-
* Numeric coercion is strict: `Number.isFinite` gates every accumulator write
|
|
596
|
-
* so a sentinel `NaN` from a misbehaving backend cannot poison the ledger.
|
|
597
|
-
*/
|
|
598
|
-
function extractLlmCallEvent(event, agentRunName) {
|
|
599
|
-
const data = canonicalUsageData(event);
|
|
600
|
-
return data === void 0 ? void 0 : buildLlmCall(data, agentRunName);
|
|
601
|
-
}
|
|
602
|
-
function canonicalUsageData(event) {
|
|
603
|
-
if (!event || typeof event !== "object") return void 0;
|
|
604
|
-
const type = String(event.type ?? "");
|
|
605
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
606
|
-
if (type === "llm_call" || type === "cost.usage" || type === "usage") return usageReceiptData(data, recordValue(data.usage));
|
|
607
|
-
if (type === "error" || isSandboxTerminalEvent(type)) {
|
|
608
|
-
const field = type === "error" ? "tokenUsage" : sandboxTerminalUsageField(type);
|
|
609
|
-
return usageReceiptData(data, recordValue(data[field]) ?? recordValue(data[field === "usage" ? "tokenUsage" : "usage"]));
|
|
610
|
-
}
|
|
611
|
-
}
|
|
612
|
-
/** A {@link SandboxUsageLedger} for one worker. Pass the worker's harness to decode with that
|
|
613
|
-
* harness's adapter; omit it to try every registered adapter. */
|
|
614
|
-
function createSandboxUsageLedger(harness) {
|
|
615
|
-
let held = [];
|
|
616
|
-
let sawCanonical = false;
|
|
617
|
-
const credited = {};
|
|
618
|
-
const receipts = /* @__PURE__ */ new Map();
|
|
619
|
-
const settledTokens = zeroTokenUsage();
|
|
620
|
-
let turnTokens = zeroTokenUsage();
|
|
621
|
-
let turnUsageUnknown = false;
|
|
622
|
-
let settledUsage;
|
|
623
|
-
return {
|
|
624
|
-
settledUsage: () => settledUsage === void 0 ? void 0 : { ...settledUsage },
|
|
625
|
-
tokenUsage() {
|
|
626
|
-
const total = cloneTokenUsage(settledTokens);
|
|
627
|
-
addTokenUsage(total, turnTokens);
|
|
628
|
-
return total;
|
|
629
|
-
},
|
|
630
|
-
observe(event, agentRunName) {
|
|
631
|
-
const call = extractLlmCallEvent(event, agentRunName);
|
|
632
|
-
if (call) {
|
|
633
|
-
sawCanonical = true;
|
|
634
|
-
const receipt = creditCanonicalUsage(event, call, credited, receipts, (receipt, mode) => {
|
|
635
|
-
refineCanonicalTokens(turnTokens, call, receipt, mode);
|
|
636
|
-
});
|
|
637
|
-
if (receipt?.tokensKnown === false) {
|
|
638
|
-
turnTokens.tokensKnown = false;
|
|
639
|
-
turnUsageUnknown = true;
|
|
640
|
-
}
|
|
641
|
-
return receipt;
|
|
642
|
-
}
|
|
643
|
-
let usage;
|
|
644
|
-
try {
|
|
645
|
-
usage = decodeHarnessUsage(event, harness);
|
|
646
|
-
} catch (err) {
|
|
647
|
-
turnUsageUnknown = true;
|
|
648
|
-
return {
|
|
649
|
-
type: "llm_call",
|
|
650
|
-
model: agentRunName,
|
|
651
|
-
tokensKnown: false,
|
|
652
|
-
usdKnown: false,
|
|
653
|
-
tokensUnknownReason: err instanceof Error ? err.message : String(err)
|
|
654
|
-
};
|
|
655
|
-
}
|
|
656
|
-
if (usage) held.push(usage);
|
|
657
|
-
},
|
|
658
|
-
settleTurn(agentRunName) {
|
|
659
|
-
const reports = held;
|
|
660
|
-
const canonical = sawCanonical;
|
|
661
|
-
held = [];
|
|
662
|
-
sawCanonical = false;
|
|
663
|
-
const receipt = !canonical && reports.length > 0 ? harnessUsageLlmCall(reports, agentRunName) : void 0;
|
|
664
|
-
if (receipt) creditCanonicalUsage({
|
|
665
|
-
type: "llm_call",
|
|
666
|
-
data: {}
|
|
667
|
-
}, receipt, credited, receipts, (credit, mode) => refineCanonicalTokens(turnTokens, receipt, credit, mode));
|
|
668
|
-
settledUsage = !turnUsageUnknown && credited.tokensIn !== void 0 && credited.tokensOut !== void 0 ? {
|
|
669
|
-
inputTokens: credited.tokensIn,
|
|
670
|
-
outputTokens: credited.tokensOut,
|
|
671
|
-
...credited.totalTokens === void 0 ? {} : { totalTokens: credited.totalTokens },
|
|
672
|
-
...credited.reasoningTokens === void 0 ? {} : { reasoningTokens: credited.reasoningTokens },
|
|
673
|
-
...credited["promptCache.readTokens"] === void 0 ? {} : { cacheReadInputTokens: credited["promptCache.readTokens"] },
|
|
674
|
-
...credited["promptCache.writeTokens"] === void 0 ? {} : { cacheCreationInputTokens: credited["promptCache.writeTokens"] },
|
|
675
|
-
...credited.costUsd === void 0 ? {} : { cost: credited.costUsd }
|
|
676
|
-
} : void 0;
|
|
677
|
-
for (const key of Object.keys(credited)) delete credited[key];
|
|
678
|
-
receipts.clear();
|
|
679
|
-
addTokenUsage(settledTokens, turnTokens);
|
|
680
|
-
turnTokens = zeroTokenUsage();
|
|
681
|
-
turnUsageUnknown = false;
|
|
682
|
-
return receipt;
|
|
683
|
-
}
|
|
684
|
-
};
|
|
685
|
-
}
|
|
686
|
-
/** Shared receipt semantics; an undeclared nonterminal observation is not a cumulative total. */
|
|
687
|
-
function canonicalSandboxUsageMode(event) {
|
|
688
|
-
const declaredMode = recordValue(event.data)?.usageMode;
|
|
689
|
-
return declaredMode === "delta" || declaredMode === "cumulative" ? declaredMode : declaredMode === void 0 && event.type === "llm_call" ? "delta" : declaredMode === void 0 && (event.type === "error" || isSandboxTerminalEvent(String(event.type))) ? "cumulative" : void 0;
|
|
690
|
-
}
|
|
691
|
-
/** Terminal totals replace already credited usage; only per-call receipts add to it. */
|
|
692
|
-
function creditCanonicalUsage(event, call, credited, receipts, onCredit) {
|
|
693
|
-
const mode = canonicalSandboxUsageMode(event);
|
|
694
|
-
const result = { ...call };
|
|
695
|
-
const data = recordValue(event.data) ?? {};
|
|
696
|
-
const usageData = canonicalUsageData(event) ?? {};
|
|
697
|
-
const supplemental = Object.fromEntries(["totalTokens", "reasoningTokens"].flatMap((key) => {
|
|
698
|
-
const value = usageData[key];
|
|
699
|
-
return value === void 0 ? [] : [[key, typeof value === "number" && Number.isFinite(value) ? value : null]];
|
|
700
|
-
}));
|
|
701
|
-
const explicitTokensUnknown = [
|
|
702
|
-
data,
|
|
703
|
-
recordValue(data.usage),
|
|
704
|
-
recordValue(data.tokenUsage)
|
|
705
|
-
].some((receipt) => ["tokensKnown", "tokens_known"].some((key) => receipt?.[key] !== void 0 && receipt[key] !== true));
|
|
706
|
-
if (call.tokensIn === void 0 && call.tokensOut === void 0 && !call.tokensUnknownReason && !explicitTokensUnknown) delete result.tokensKnown;
|
|
707
|
-
let reason = mode === void 0 ? "usage receipt has no declared delta or cumulative semantics" : void 0;
|
|
708
|
-
if (event.id) {
|
|
709
|
-
const signature = JSON.stringify({
|
|
710
|
-
mode,
|
|
711
|
-
call,
|
|
712
|
-
supplemental
|
|
713
|
-
});
|
|
714
|
-
const previous = receipts.get(event.id);
|
|
715
|
-
if (previous === signature) return void 0;
|
|
716
|
-
if (previous !== void 0) return {
|
|
717
|
-
type: "llm_call",
|
|
718
|
-
model: call.model,
|
|
719
|
-
tokensKnown: false,
|
|
720
|
-
usdKnown: false,
|
|
721
|
-
tokensUnknownReason: `usage receipt ${event.id} changed after it was credited`
|
|
722
|
-
};
|
|
723
|
-
receipts.set(event.id, signature);
|
|
724
|
-
}
|
|
725
|
-
const credit = (key, value) => {
|
|
726
|
-
const previous = credited[key] ?? 0;
|
|
727
|
-
if (!Number.isFinite(value) || value < 0) {
|
|
728
|
-
reason = `usage receipt has an invalid ${key}`;
|
|
729
|
-
return 0;
|
|
730
|
-
}
|
|
731
|
-
if (mode === "delta") {
|
|
732
|
-
credited[key] = previous + value;
|
|
733
|
-
return value;
|
|
734
|
-
}
|
|
735
|
-
if (value < previous) reason = `cumulative usage ${key} is below already credited usage`;
|
|
736
|
-
credited[key] = Math.max(previous, value);
|
|
737
|
-
return Math.max(0, value - previous);
|
|
738
|
-
};
|
|
739
|
-
for (const key of [
|
|
740
|
-
"tokensIn",
|
|
741
|
-
"tokensOut",
|
|
742
|
-
"costUsd",
|
|
743
|
-
"estimatedCostUsd"
|
|
744
|
-
]) if (call[key] !== void 0) result[key] = credit(key, call[key]);
|
|
745
|
-
for (const [key, value] of Object.entries(supplemental)) if (typeof value !== "number") reason = `usage receipt has an invalid ${key}`;
|
|
746
|
-
else credit(key, value);
|
|
747
|
-
if (call.promptCache) result.promptCache = Object.fromEntries(Object.entries(call.promptCache).map(([key, value]) => [key, typeof value === "number" ? credit(`promptCache.${key}`, value) : value]));
|
|
748
|
-
if (reason !== void 0) {
|
|
749
|
-
result.usdKnown = false;
|
|
750
|
-
if (call.tokensIn !== void 0 || call.tokensOut !== void 0) {
|
|
751
|
-
result.tokensKnown = false;
|
|
752
|
-
result.tokensUnknownReason = reason;
|
|
753
|
-
}
|
|
754
|
-
}
|
|
755
|
-
onCredit(result, mode);
|
|
756
|
-
return result;
|
|
757
|
-
}
|
|
758
|
-
/** Cache partitions belong to the original receipt, not the smaller incremental charge. */
|
|
759
|
-
function refineCanonicalTokens(tokens, call, receipt, mode) {
|
|
760
|
-
const input = receipt.tokensIn ?? 0;
|
|
761
|
-
const output = receipt.tokensOut ?? 0;
|
|
762
|
-
const cache = promptCacheTokenClasses(call.tokensIn, call.promptCache);
|
|
763
|
-
if (mode !== "cumulative") {
|
|
764
|
-
addTokenUsage(tokens, {
|
|
765
|
-
input,
|
|
766
|
-
output,
|
|
767
|
-
...cache
|
|
768
|
-
});
|
|
769
|
-
return;
|
|
770
|
-
}
|
|
771
|
-
tokens.input += input;
|
|
772
|
-
tokens.output += output;
|
|
773
|
-
if (cache.freshInput !== void 0 && call.tokensIn === tokens.input && receipt.tokensKnown !== false) {
|
|
774
|
-
tokens.freshInput = cache.freshInput;
|
|
775
|
-
tokens.cacheRead = cache.cacheRead;
|
|
776
|
-
tokens.cacheWrite = cache.cacheWrite;
|
|
777
|
-
delete tokens.cacheBreakdownKnown;
|
|
778
|
-
return;
|
|
779
|
-
}
|
|
780
|
-
if (cache.cacheRead !== void 0) tokens.cacheRead = Math.max(tokens.cacheRead ?? 0, cache.cacheRead);
|
|
781
|
-
if (cache.cacheWrite !== void 0) tokens.cacheWrite = Math.max(tokens.cacheWrite ?? 0, cache.cacheWrite);
|
|
782
|
-
if ((tokens.freshInput ?? 0) + (tokens.cacheRead ?? 0) + (tokens.cacheWrite ?? 0) > tokens.input) {
|
|
783
|
-
delete tokens.freshInput;
|
|
784
|
-
delete tokens.cacheRead;
|
|
785
|
-
delete tokens.cacheWrite;
|
|
786
|
-
}
|
|
787
|
-
if (input > 0 || cache.cacheBreakdownKnown === false) tokens.cacheBreakdownKnown = false;
|
|
788
|
-
}
|
|
789
|
-
/**
|
|
790
|
-
* Fold harness-native usage reports into the canonical `llm_call` the accounting paths read.
|
|
791
|
-
*
|
|
792
|
-
* Every counter a `HarnessUsage` carries beside `input` and `output` CLASSIFIES one of those two
|
|
793
|
-
* totals, so none of them is added to the total it describes:
|
|
794
|
-
*
|
|
795
|
-
* - the prompt-cache counters classify `input` and ride `promptCache`, the convention the
|
|
796
|
-
* terminal `tokenUsage` branch above already follows and `promptCacheTokenClasses` folds,
|
|
797
|
-
* where `freshInput = input - cacheRead - cacheWrite`;
|
|
798
|
-
* - `reasoningOutput` classifies `output`. Codex counts its reasoning tokens INSIDE
|
|
799
|
-
* `output_tokens` — a turn reporting `output_tokens 1523` with `reasoning_output_tokens 1516`
|
|
800
|
-
* answered with about seven tokens of text — and `parseCodexUsageRecord` holds that as an
|
|
801
|
-
* invariant. The canonical `llm_call` carries no reasoning class, so the count is not
|
|
802
|
-
* forwarded to {@link buildLlmCall}. Canonical output totals already include reasoning.
|
|
803
|
-
*
|
|
804
|
-
* A counter no report carried stays absent, because a zero would claim the provider measured none.
|
|
805
|
-
*/
|
|
806
|
-
function harnessUsageLlmCall(reports, agentRunName) {
|
|
807
|
-
let inputTokens = 0;
|
|
808
|
-
let outputTokens = 0;
|
|
809
|
-
let readTokens;
|
|
810
|
-
let writeTokens;
|
|
811
|
-
for (const report of reports) {
|
|
812
|
-
inputTokens += report.input;
|
|
813
|
-
outputTokens += report.output;
|
|
814
|
-
if (report.cachedInput !== void 0) readTokens = (readTokens ?? 0) + report.cachedInput;
|
|
815
|
-
if (report.cacheWriteInput !== void 0) writeTokens = (writeTokens ?? 0) + report.cacheWriteInput;
|
|
816
|
-
}
|
|
817
|
-
const promptCache = {};
|
|
818
|
-
if (readTokens !== void 0) promptCache.readTokens = readTokens;
|
|
819
|
-
if (writeTokens !== void 0) promptCache.writeTokens = writeTokens;
|
|
820
|
-
return buildLlmCall({
|
|
821
|
-
inputTokens,
|
|
822
|
-
outputTokens,
|
|
823
|
-
...Object.keys(promptCache).length > 0 ? { promptCache } : {}
|
|
824
|
-
}, agentRunName);
|
|
825
|
-
}
|
|
826
|
-
/**
|
|
827
|
-
* Sum the token usage + USD cost of a sandbox turn's events — the one honest way to meter an
|
|
828
|
-
* `openSandboxRun` cell. Folds a {@link SandboxUsageLedger} over the stream, so it reads usage off
|
|
829
|
-
* EVERY backend event shape — the canonical events plus a harness that reports usage only in its
|
|
830
|
-
* own event — and a `runProfileMatrix` dispatch can report it to `ctx.cost`:
|
|
831
|
-
*
|
|
832
|
-
* receipt: (turn) => {
|
|
833
|
-
* const u = sumSandboxUsage(turn.events)
|
|
834
|
-
* return { model, inputTokens: u.input, outputTokens: u.output,
|
|
835
|
-
* ...(u.tokensKnown === false ? { usageUnknown: true } : {}),
|
|
836
|
-
* ...(u.usdKnown !== false && u.costUsd > 0 ? { actualCostUsd: u.costUsd } : {}),
|
|
837
|
-
* ...(u.usdKnown === false ? { costUnknown: true } : {}),
|
|
838
|
-
* ...(u.estimatedCostUsd !== undefined ? { estimatedCostUsd: u.estimatedCostUsd } : {}) }
|
|
839
|
-
* }
|
|
840
|
-
*
|
|
841
|
-
* Without this a cell reads `{tokens:0, cost:0}` and the backend-integrity guard correctly aborts the
|
|
842
|
-
* matrix as a stub. `agentRunName` is the fallback model label for cost-only events (default `'agent'`).
|
|
843
|
-
*
|
|
844
|
-
* Pure by contract, like the ledger it folds: it never throws. A harness receipt the ledger cannot
|
|
845
|
-
* read leaves the result at `tokensKnown: false` with `tokensUnknownReason` carrying the decode
|
|
846
|
-
* message — an unreadable receipt is a different fact from a turn that reported no usage, and a
|
|
847
|
-
* post-hoc reader that threw would lose the whole failed turn it exists to report.
|
|
848
|
-
*/
|
|
849
|
-
function sumSandboxUsage(events, agentRunName = "agent") {
|
|
850
|
-
let input = 0;
|
|
851
|
-
let output = 0;
|
|
852
|
-
let costUsd = 0;
|
|
853
|
-
let estimatedCostUsd = 0;
|
|
854
|
-
let sawEstimate = false;
|
|
855
|
-
let sawCall = false;
|
|
856
|
-
let sawTokenReceipt = false;
|
|
857
|
-
let tokensKnown = true;
|
|
858
|
-
let usdKnown = true;
|
|
859
|
-
let unreadable;
|
|
860
|
-
const ledger = createSandboxUsageLedger();
|
|
861
|
-
const credit = (call) => {
|
|
862
|
-
sawCall = true;
|
|
863
|
-
sawTokenReceipt ||= call.tokensIn !== void 0 || call.tokensOut !== void 0;
|
|
864
|
-
input += call.tokensIn ?? 0;
|
|
865
|
-
output += call.tokensOut ?? 0;
|
|
866
|
-
costUsd += call.costUsd ?? 0;
|
|
867
|
-
if (call.tokensKnown === false) tokensKnown = false;
|
|
868
|
-
if (call.usdKnown === false) usdKnown = false;
|
|
869
|
-
unreadable ??= call.tokensUnknownReason;
|
|
870
|
-
if (call.estimatedCostUsd !== void 0) {
|
|
871
|
-
estimatedCostUsd += call.estimatedCostUsd;
|
|
872
|
-
sawEstimate = true;
|
|
873
|
-
}
|
|
874
|
-
};
|
|
875
|
-
for (const ev of events) {
|
|
876
|
-
const call = ledger.observe(ev, agentRunName);
|
|
877
|
-
if (call) credit(call);
|
|
878
|
-
}
|
|
879
|
-
const harnessCall = ledger.settleTurn(agentRunName);
|
|
880
|
-
if (harnessCall) credit(harnessCall);
|
|
881
|
-
return {
|
|
882
|
-
input,
|
|
883
|
-
output,
|
|
884
|
-
costUsd,
|
|
885
|
-
...sawTokenReceipt && tokensKnown ? {} : { tokensKnown: false },
|
|
886
|
-
...sawCall && usdKnown ? {} : { usdKnown: false },
|
|
887
|
-
...sawEstimate ? { estimatedCostUsd } : {},
|
|
888
|
-
...unreadable === void 0 ? {} : { tokensUnknownReason: unreadable }
|
|
889
|
-
};
|
|
890
|
-
}
|
|
891
|
-
function usageReceiptData(envelope, usage = {}) {
|
|
892
|
-
const merged = {
|
|
893
|
-
...usage,
|
|
894
|
-
...envelope
|
|
895
|
-
};
|
|
896
|
-
for (const aliases of [
|
|
897
|
-
[
|
|
898
|
-
"tokensIn",
|
|
899
|
-
"inputTokens",
|
|
900
|
-
"prompt_tokens"
|
|
901
|
-
],
|
|
902
|
-
[
|
|
903
|
-
"tokensOut",
|
|
904
|
-
"outputTokens",
|
|
905
|
-
"completion_tokens"
|
|
906
|
-
],
|
|
907
|
-
[
|
|
908
|
-
"costUsd",
|
|
909
|
-
"totalCostUsd",
|
|
910
|
-
"cost_usd",
|
|
911
|
-
"cost"
|
|
912
|
-
],
|
|
913
|
-
["costProvenance", "cost_provenance"]
|
|
914
|
-
]) {
|
|
915
|
-
const key = aliases.find((alias) => envelope[alias] !== void 0);
|
|
916
|
-
if (key === void 0) continue;
|
|
917
|
-
for (const alias of aliases) delete merged[alias];
|
|
918
|
-
merged[key] = envelope[key];
|
|
919
|
-
}
|
|
920
|
-
for (const aliases of [["tokensKnown", "tokens_known"], [
|
|
921
|
-
"costKnown",
|
|
922
|
-
"cost_known",
|
|
923
|
-
"usdKnown",
|
|
924
|
-
"usd_known"
|
|
925
|
-
]]) {
|
|
926
|
-
const values = [usage, envelope].flatMap((receipt) => aliases.map((key) => receipt[key]).filter((value) => value !== void 0));
|
|
927
|
-
if (values.length > 0) merged[aliases[0]] = values.every((value) => value === true);
|
|
928
|
-
}
|
|
929
|
-
return merged;
|
|
930
|
-
}
|
|
931
|
-
function buildLlmCall(data, agentRunName) {
|
|
932
|
-
const tokensIn = pickFiniteNumber(data, [
|
|
933
|
-
"tokensIn",
|
|
934
|
-
"inputTokens",
|
|
935
|
-
"prompt_tokens"
|
|
936
|
-
]);
|
|
937
|
-
const outputTokens = pickFiniteNumber(data, [
|
|
938
|
-
"tokensOut",
|
|
939
|
-
"outputTokens",
|
|
940
|
-
"completion_tokens"
|
|
941
|
-
]);
|
|
942
|
-
const reasoningTokens = pickFiniteNumber(data, ["reasoningTokens"]);
|
|
943
|
-
const tokensOut = outputTokens ?? reasoningTokens;
|
|
944
|
-
const reportedCostUsd = pickFiniteNumber(data, [
|
|
945
|
-
"costUsd",
|
|
946
|
-
"totalCostUsd",
|
|
947
|
-
"cost_usd",
|
|
948
|
-
"cost"
|
|
949
|
-
]);
|
|
950
|
-
const explicitTokensKnown = data.tokensKnown ?? data.tokens_known;
|
|
951
|
-
const explicitCostKnown = data.costKnown ?? data.cost_known ?? data.usdKnown ?? data.usd_known;
|
|
952
|
-
const costProvenance = data.costProvenance ?? data.cost_provenance;
|
|
953
|
-
const explicitEstimate = pickFiniteNumber(data, ["estimatedCostUsd", "estimated_cost_usd"]);
|
|
954
|
-
const catalogEstimate = costProvenance === "catalog-estimate" ? explicitEstimate ?? reportedCostUsd : explicitEstimate;
|
|
955
|
-
const costUsd = costProvenance === "catalog-estimate" ? void 0 : reportedCostUsd;
|
|
956
|
-
const promptCache = readPromptCacheUsage(data);
|
|
957
|
-
const tokensKnown = explicitTokensKnown !== false && tokensIn !== void 0 && outputTokens !== void 0;
|
|
958
|
-
const usdKnown = explicitCostKnown !== false && costUsd !== void 0 && (costProvenance === void 0 || costProvenance === "provider-receipt" || costProvenance === "billing-receipt");
|
|
959
|
-
const tools = readOfferedTools(data.tools);
|
|
960
|
-
if (tokensIn === void 0 && tokensOut === void 0 && costUsd === void 0 && catalogEstimate === void 0 && promptCache === void 0 && tools === void 0 && explicitTokensKnown !== false && explicitCostKnown !== false) return;
|
|
961
|
-
const event = {
|
|
962
|
-
type: "llm_call",
|
|
963
|
-
model: typeof data.model === "string" && data.model.length > 0 ? data.model : agentRunName
|
|
964
|
-
};
|
|
965
|
-
if (tokensIn !== void 0) event.tokensIn = tokensIn;
|
|
966
|
-
if (tokensOut !== void 0) event.tokensOut = tokensOut;
|
|
967
|
-
if (!tokensKnown) event.tokensKnown = false;
|
|
968
|
-
if (costUsd !== void 0) event.costUsd = costUsd;
|
|
969
|
-
if (!usdKnown) event.usdKnown = false;
|
|
970
|
-
if (catalogEstimate !== void 0) event.estimatedCostUsd = catalogEstimate;
|
|
971
|
-
if (promptCache !== void 0) event.promptCache = promptCache;
|
|
972
|
-
if (tools !== void 0) event.tools = tools;
|
|
973
|
-
return event;
|
|
974
|
-
}
|
|
975
|
-
/** `data.tools`, when it is a non-empty array of non-empty tool-name strings. Anything else
|
|
976
|
-
* (absent, empty, wrong element type) is treated as "the producer did not report its offered
|
|
977
|
-
* set" — never a placeholder empty list, which would read as "offered zero tools" downstream. */
|
|
978
|
-
function readOfferedTools(value) {
|
|
979
|
-
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
980
|
-
const names = value.filter((v) => typeof v === "string" && v.length > 0);
|
|
981
|
-
return names.length === value.length ? names : void 0;
|
|
982
|
-
}
|
|
983
|
-
/**
|
|
984
|
-
* Every wire spelling of a prompt-cache READ counter, in precedence order: the canonical name
|
|
985
|
-
* first, then the spellings observed on the paths that reach this extractor — the sandbox terminal
|
|
986
|
-
* `tokenUsage` record, the cli-bridge OpenAI-compatible usage object (which normalizes every
|
|
987
|
-
* backend to Anthropic's `cache_read_input_tokens`), Anthropic verbatim, OpenAI's
|
|
988
|
-
* `prompt_tokens_details.cached_tokens`, and DeepSeek's `prompt_cache_hit_tokens`.
|
|
989
|
-
*/
|
|
990
|
-
const PROMPT_CACHE_READ_KEYS = [
|
|
991
|
-
"readTokens",
|
|
992
|
-
"read_tokens",
|
|
993
|
-
"cacheReadInputTokens",
|
|
994
|
-
"cache_read_input_tokens",
|
|
995
|
-
"cacheRead",
|
|
996
|
-
"cache_read",
|
|
997
|
-
"cachedInputTokens",
|
|
998
|
-
"cachedTokens",
|
|
999
|
-
"cached_tokens",
|
|
1000
|
-
"prompt_cache_hit_tokens"
|
|
1001
|
-
];
|
|
1002
|
-
/**
|
|
1003
|
-
* Every wire spelling of a prompt-cache WRITE counter. OpenAI reports no write counter at all, so
|
|
1004
|
-
* an OpenAI-shaped usage record matches nothing here and the write stays absent — which is the
|
|
1005
|
-
* point: absent is not zero.
|
|
1006
|
-
*/
|
|
1007
|
-
const PROMPT_CACHE_WRITE_KEYS = [
|
|
1008
|
-
"writeTokens",
|
|
1009
|
-
"write_tokens",
|
|
1010
|
-
"cacheWriteInputTokens",
|
|
1011
|
-
"cache_write_input_tokens",
|
|
1012
|
-
"cacheCreationInputTokens",
|
|
1013
|
-
"cache_creation_input_tokens",
|
|
1014
|
-
"cacheWrite",
|
|
1015
|
-
"cache_write"
|
|
1016
|
-
];
|
|
1017
|
-
const PROMPT_CACHE_MISS_KEYS = [
|
|
1018
|
-
"missTokens",
|
|
1019
|
-
"miss_tokens",
|
|
1020
|
-
"prompt_cache_miss_tokens"
|
|
1021
|
-
];
|
|
1022
|
-
const PROMPT_CACHE_SAVINGS_KEYS = ["readSavingsUsd", "read_savings_usd"];
|
|
1023
|
-
/**
|
|
1024
|
-
* Read the provider's own prompt-cache accounting off one usage-bearing event payload, in the
|
|
1025
|
-
* `PromptCacheUsage` vocabulary the router, driver, and chat-client paths already speak.
|
|
1026
|
-
*
|
|
1027
|
-
* Two sources, one record. A payload that already carries a `promptCache` / `prompt_cache` object
|
|
1028
|
-
* is forwarded VERBATIM — those fields are the provider's own report, and an unrecognized one must
|
|
1029
|
-
* stay visible rather than be bucketed away. On top of that, flat provider counters are translated
|
|
1030
|
-
* to the canonical names, filling only the names the verbatim object did not already define.
|
|
1031
|
-
*
|
|
1032
|
-
* A counter the payload does not carry is left out. Defaulting it to zero would assert the provider
|
|
1033
|
-
* measured no cache, which is a different fact from a provider that reported nothing — and it is
|
|
1034
|
-
* the fact the budget reads to decide whether its charge is a measurement or a bound.
|
|
1035
|
-
*/
|
|
1036
|
-
function readPromptCacheUsage(data) {
|
|
1037
|
-
const declared = finiteMetadata(data.promptCache ?? data.prompt_cache);
|
|
1038
|
-
const nested = recordValue(data.promptCache) ?? recordValue(data.prompt_cache);
|
|
1039
|
-
const sources = [
|
|
1040
|
-
nested,
|
|
1041
|
-
data,
|
|
1042
|
-
recordValue(data.prompt_tokens_details)
|
|
1043
|
-
].filter((s) => s !== void 0);
|
|
1044
|
-
const canonical = {};
|
|
1045
|
-
const readTokens = pickTokenCount(sources, PROMPT_CACHE_READ_KEYS);
|
|
1046
|
-
const writeTokens = pickTokenCount(sources, PROMPT_CACHE_WRITE_KEYS);
|
|
1047
|
-
const missTokens = pickTokenCount(sources, PROMPT_CACHE_MISS_KEYS);
|
|
1048
|
-
const readSavingsUsd = pickNonNegativeNumber(sources, PROMPT_CACHE_SAVINGS_KEYS);
|
|
1049
|
-
const status = nested !== void 0 && typeof nested.status === "string" ? nested.status : void 0;
|
|
1050
|
-
if (readTokens !== void 0) canonical.readTokens = readTokens;
|
|
1051
|
-
if (writeTokens !== void 0) canonical.writeTokens = writeTokens;
|
|
1052
|
-
if (missTokens !== void 0) canonical.missTokens = missTokens;
|
|
1053
|
-
if (readSavingsUsd !== void 0) canonical.readSavingsUsd = readSavingsUsd;
|
|
1054
|
-
if (status !== void 0) canonical.status = status;
|
|
1055
|
-
const merged = {
|
|
1056
|
-
...canonical,
|
|
1057
|
-
...declared
|
|
1058
|
-
};
|
|
1059
|
-
return Object.keys(merged).length > 0 ? merged : void 0;
|
|
1060
|
-
}
|
|
1061
|
-
function pickTokenCount(sources, keys) {
|
|
1062
|
-
for (const key of keys) for (const source of sources) {
|
|
1063
|
-
const value = source[key];
|
|
1064
|
-
if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) return value;
|
|
1065
|
-
}
|
|
1066
|
-
}
|
|
1067
|
-
function pickNonNegativeNumber(sources, keys) {
|
|
1068
|
-
for (const key of keys) for (const source of sources) {
|
|
1069
|
-
const value = source[key];
|
|
1070
|
-
if (typeof value === "number" && Number.isFinite(value) && value >= 0) return value;
|
|
1071
|
-
}
|
|
1072
|
-
}
|
|
1073
|
-
function finiteMetadata(value) {
|
|
1074
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
|
|
1075
|
-
const result = {};
|
|
1076
|
-
for (const [key, entry] of Object.entries(value)) if (typeof entry === "number" && Number.isFinite(entry) || typeof entry === "string") result[key] = entry;
|
|
1077
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
1078
|
-
}
|
|
1079
|
-
function pickFiniteNumber(data, keys) {
|
|
1080
|
-
for (const key of keys) {
|
|
1081
|
-
const value = data[key];
|
|
1082
|
-
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
1083
|
-
}
|
|
1084
|
-
}
|
|
1085
|
-
/**
|
|
1086
|
-
* Collect one turn's events, keeping only the newest snapshot of each streamed text or
|
|
1087
|
-
* reasoning part.
|
|
1088
|
-
*
|
|
1089
|
-
* opencode sends `message.part.updated` once per token, and every frame carries the part's
|
|
1090
|
-
* whole cumulative text, so keeping every frame holds O(L²) bytes per turn: two concurrent
|
|
1091
|
-
* audit workflows took the platform API from 459 MB to an OOM at 1.95 GB in 11 minutes.
|
|
1092
|
-
* A snapshot (a frame whose part carries `text`) whose text extends the previous snapshot
|
|
1093
|
-
* of the same part replaces it and moves to the newest position, which is the rule the
|
|
1094
|
-
* Sandbox SDK's text collectors already apply, so the collected text is unchanged. The kept
|
|
1095
|
-
* frame drops its `delta`, because its text already holds every earlier increment and a
|
|
1096
|
-
* reader that joins `delta ?? part.text` across frames would otherwise read only the last
|
|
1097
|
-
* one. Delta-only frames and every other event are kept as they arrived.
|
|
1098
|
-
*/
|
|
1099
|
-
function createSandboxTurnEventCollector() {
|
|
1100
|
-
const slots = [];
|
|
1101
|
-
const snapshots = /* @__PURE__ */ new Map();
|
|
1102
|
-
return {
|
|
1103
|
-
push(event) {
|
|
1104
|
-
const snapshot = partSnapshot(event);
|
|
1105
|
-
if (!snapshot) {
|
|
1106
|
-
slots.push(event);
|
|
1107
|
-
return;
|
|
1108
|
-
}
|
|
1109
|
-
const previous = snapshots.get(snapshot.key);
|
|
1110
|
-
const extends_ = previous !== void 0 && snapshot.text.startsWith(previous.text);
|
|
1111
|
-
if (extends_) slots[previous.at] = void 0;
|
|
1112
|
-
snapshots.set(snapshot.key, {
|
|
1113
|
-
at: slots.length,
|
|
1114
|
-
text: snapshot.text
|
|
1115
|
-
});
|
|
1116
|
-
slots.push(extends_ ? withoutDelta(event) : event);
|
|
1117
|
-
},
|
|
1118
|
-
events() {
|
|
1119
|
-
return slots.filter((event) => event !== void 0);
|
|
1120
|
-
}
|
|
1121
|
-
};
|
|
1122
|
-
}
|
|
1123
|
-
function partSnapshot(event) {
|
|
1124
|
-
if (event?.type !== "message.part.updated") return void 0;
|
|
1125
|
-
const data = asRecord(event.data);
|
|
1126
|
-
const part = asRecord(data?.part) ?? asRecord(asRecord(data?.properties)?.part);
|
|
1127
|
-
if (!part || typeof part.id !== "string") return void 0;
|
|
1128
|
-
if (part.type !== "text" && part.type !== "reasoning" && part.type !== "thinking") return void 0;
|
|
1129
|
-
const text = typeof part.text === "string" ? part.text : typeof part.content === "string" ? part.content : void 0;
|
|
1130
|
-
return text === void 0 ? void 0 : {
|
|
1131
|
-
key: `${part.type}:${part.id}`,
|
|
1132
|
-
text
|
|
1133
|
-
};
|
|
1134
|
-
}
|
|
1135
|
-
function withoutDelta(event) {
|
|
1136
|
-
const data = asRecord(event.data);
|
|
1137
|
-
if (!data || !("delta" in data)) return event;
|
|
1138
|
-
const { delta: _delta, ...rest } = data;
|
|
1139
|
-
return {
|
|
1140
|
-
...event,
|
|
1141
|
-
data: rest
|
|
1142
|
-
};
|
|
1143
|
-
}
|
|
1144
|
-
function asRecord(value) {
|
|
1145
|
-
return value !== null && typeof value === "object" ? value : void 0;
|
|
1146
|
-
}
|
|
1147
|
-
// src/runtime/timestamps.ts
|
|
1148
|
-
const validTimestamp = "2026-01-01T00:00:00.000Z";
|
|
1149
|
-
/** Validate a timestamp with the public runtime envelope contract. */
|
|
1150
|
-
function assertRuntimeTimestamp(value, label) {
|
|
1151
|
-
try {
|
|
1152
|
-
RuntimeEventEnvelopeSchema.parse({
|
|
1153
|
-
runId: "runtime",
|
|
1154
|
-
eventId: "event",
|
|
1155
|
-
sequence: 0,
|
|
1156
|
-
occurredAt: value,
|
|
1157
|
-
receivedAt: validTimestamp,
|
|
1158
|
-
event: {
|
|
1159
|
-
type: "status",
|
|
1160
|
-
status: "processing"
|
|
1161
|
-
}
|
|
1162
|
-
});
|
|
1163
|
-
} catch (error) {
|
|
1164
|
-
throw new Error(`${label} must be a valid ISO timestamp`, { cause: error });
|
|
1165
|
-
}
|
|
1166
|
-
}
|
|
1167
|
-
// src/runtime/sandbox-transport-events.ts
|
|
1168
|
-
/** Canonical Sandbox/provider wire-envelope decoding and transport identity. */
|
|
1169
|
-
const CANONICAL_STREAM_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1170
|
-
"child-task",
|
|
1171
|
-
"message.part.updated",
|
|
1172
|
-
"tool-heartbeat",
|
|
1173
|
-
"tool-slow",
|
|
1174
|
-
"model-processing",
|
|
1175
|
-
"status",
|
|
1176
|
-
"warning",
|
|
1177
|
-
"raw",
|
|
1178
|
-
"session.updated",
|
|
1179
|
-
"interaction",
|
|
1180
|
-
"interaction.cancel",
|
|
1181
|
-
"plan.submitted"
|
|
1182
|
-
]);
|
|
1183
|
-
/** Decode one known Agent Interface event from a Sandbox event. */
|
|
1184
|
-
function canonicalStreamEventFromSandboxEvent(event) {
|
|
1185
|
-
if (!event || typeof event !== "object") return void 0;
|
|
1186
|
-
const type = String(event.type ?? "");
|
|
1187
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
1188
|
-
const normalized = data.normalized;
|
|
1189
|
-
if (normalized === void 0 && !CANONICAL_STREAM_EVENT_TYPES.has(type)) return void 0;
|
|
1190
|
-
return parseCanonicalTransportEvent(type, data, normalized, "sandbox");
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* Transport type whose payload is the harness's OWN event, passed through verbatim.
|
|
1194
|
-
*
|
|
1195
|
-
* A `raw` payload names a harness-native type from a vocabulary the canonical schema does not
|
|
1196
|
-
* define (codex emits `thread.started` / `turn.started` / `turn.completed`). The transport type is
|
|
1197
|
-
* therefore the only canonical statement such an event makes, and its payload must never be read
|
|
1198
|
-
* as a canonical type: the mismatch guard below exists to catch a producer that MISLABELS a
|
|
1199
|
-
* canonical event, which a harness-native payload cannot do. A payload that does satisfy the
|
|
1200
|
-
* canonical `raw` shape (`backend` + `event`) still parses; anything else is not a canonical
|
|
1201
|
-
* event, and the caller reads it off the transport event instead.
|
|
1202
|
-
*/
|
|
1203
|
-
const harnessNativePayloadType = "raw";
|
|
1204
|
-
/** Parse canonical payload fields without treating transport identity as event data. */
|
|
1205
|
-
function parseCanonicalTransportEvent(type, data, normalized, source) {
|
|
1206
|
-
if (!isRecord(data)) throw new Error(`${source} emitted a canonical event without an object payload`);
|
|
1207
|
-
const outerType = String(type ?? "");
|
|
1208
|
-
const harnessNative = outerType === harnessNativePayloadType;
|
|
1209
|
-
if (normalized !== void 0) {
|
|
1210
|
-
if (!harnessNative && typeof normalized === "object" && normalized !== null && "type" in normalized && typeof normalized.type === "string" && normalized.type !== outerType) throw new Error(`${source} canonical event type "${normalized.type}" does not match transport type "${outerType}"`);
|
|
1211
|
-
const candidate = CanonicalStreamEventSchema.safeParse(normalized);
|
|
1212
|
-
if (!candidate.success) {
|
|
1213
|
-
if (harnessNative) return void 0;
|
|
1214
|
-
throw new Error(`${source} emitted an invalid normalized canonical event`, { cause: candidate.error });
|
|
1215
|
-
}
|
|
1216
|
-
return candidate.data;
|
|
1217
|
-
}
|
|
1218
|
-
const { type: embeddedType, eventId: _eventId, cursor: _cursor, sequence: _sequence, occurredAt: _occurredAt, normalized: _normalized, ...payload } = data;
|
|
1219
|
-
if (!harnessNative && embeddedType !== void 0 && embeddedType !== outerType) throw new Error(`${source} canonical event type "${String(embeddedType)}" does not match transport type "${outerType}"`);
|
|
1220
|
-
const candidate = CanonicalStreamEventSchema.safeParse({
|
|
1221
|
-
...payload,
|
|
1222
|
-
type: outerType
|
|
1223
|
-
});
|
|
1224
|
-
return candidate.success ? candidate.data : void 0;
|
|
1225
|
-
}
|
|
1226
|
-
/** Extract shared event identity without privileging one provider wire shape. */
|
|
1227
|
-
function extractTransportEventIdentity(event) {
|
|
1228
|
-
const record = isRecord(event) ? event : {};
|
|
1229
|
-
const data = isRecord(record.data) ? record.data : {};
|
|
1230
|
-
const providerEvent = isRecord(record.providerEvent) ? record.providerEvent : {};
|
|
1231
|
-
const providerData = isRecord(providerEvent.data) ? providerEvent.data : {};
|
|
1232
|
-
const eventId = canonicalEventId(record, data, providerEvent, providerData);
|
|
1233
|
-
const cursor = stableString(record.cursor ?? data.cursor ?? providerEvent.cursor ?? providerData.cursor);
|
|
1234
|
-
const sequence = optionalSequence(record.sequence ?? data.sequence ?? providerEvent.sequence ?? providerData.sequence);
|
|
1235
|
-
const occurredAt = optionalTimestamp(record.occurredAt ?? data.occurredAt ?? providerEvent.occurredAt ?? providerData.occurredAt);
|
|
1236
|
-
return {
|
|
1237
|
-
...eventId === void 0 ? {} : { eventId },
|
|
1238
|
-
...cursor === void 0 ? {} : { cursor },
|
|
1239
|
-
...sequence === void 0 ? {} : { sequence },
|
|
1240
|
-
...occurredAt === void 0 ? {} : { occurredAt }
|
|
1241
|
-
};
|
|
1242
|
-
}
|
|
1243
|
-
function canonicalEventId(record, data, providerEvent, providerData) {
|
|
1244
|
-
const present = [
|
|
1245
|
-
["data.eventId", data.eventId],
|
|
1246
|
-
["providerEvent.eventId", providerEvent.eventId],
|
|
1247
|
-
["providerData.eventId", providerData.eventId],
|
|
1248
|
-
["record.eventId", record.eventId]
|
|
1249
|
-
].filter(([, value]) => value !== void 0);
|
|
1250
|
-
if (present.length === 0) return stableString(record.id ?? providerEvent.id);
|
|
1251
|
-
const normalized = present.map(([source, value]) => {
|
|
1252
|
-
const eventId = stableString(value);
|
|
1253
|
-
if (eventId === void 0) throw new Error(`transport event ${source} must be a stable string`);
|
|
1254
|
-
return {
|
|
1255
|
-
source,
|
|
1256
|
-
eventId
|
|
1257
|
-
};
|
|
1258
|
-
});
|
|
1259
|
-
const first = normalized[0];
|
|
1260
|
-
if (normalized.some((candidate) => candidate.eventId !== first.eventId)) throw new Error("transport event canonical identities disagree");
|
|
1261
|
-
return first.eventId;
|
|
1262
|
-
}
|
|
1263
|
-
function stableString(value) {
|
|
1264
|
-
return typeof value === "string" && value.length > 0 && value.trim() === value ? value : void 0;
|
|
1265
|
-
}
|
|
1266
|
-
function finiteNonNegativeInteger(value) {
|
|
1267
|
-
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : void 0;
|
|
1268
|
-
}
|
|
1269
|
-
function optionalSequence(value) {
|
|
1270
|
-
if (value === void 0) return void 0;
|
|
1271
|
-
if (finiteNonNegativeInteger(value) === void 0) throw new Error("transport event sequence must be a non-negative safe integer");
|
|
1272
|
-
return value;
|
|
1273
|
-
}
|
|
1274
|
-
function optionalTimestamp(value) {
|
|
1275
|
-
if (value === void 0) return void 0;
|
|
1276
|
-
assertRuntimeTimestamp(value, "occurredAt");
|
|
1277
|
-
return value;
|
|
1278
|
-
}
|
|
1279
|
-
// src/runtime/sandbox-executor-output.ts
|
|
1280
|
-
/** Streamed and terminal Sandbox executor output, using one tool-part projector. */
|
|
1281
|
-
/**
|
|
1282
|
-
* Fresh per-turn {@link SandboxToolPartState} for {@link mapSandboxToolEvent} — an
|
|
1283
|
-
* empty call-status map so each turn projects tool frames independently.
|
|
1284
|
-
*
|
|
1285
|
-
* @experimental
|
|
1286
|
-
*/
|
|
1287
|
-
function createSandboxToolPartState() {
|
|
1288
|
-
return {
|
|
1289
|
-
statusByCall: /* @__PURE__ */ new Map(),
|
|
1290
|
-
seq: 0
|
|
1291
|
-
};
|
|
1292
|
-
}
|
|
1293
|
-
/** Statuses that settle one tool call as a failure. A failed tool call stays a tool result:
|
|
1294
|
-
* the run's terminal state comes from the public Sandbox outcome tracker, never from here. */
|
|
1295
|
-
const TERMINAL_FAILURE = /^(error|errored|failed|failure|cancelled|canceled|timeout|timed_out)$/i;
|
|
1296
|
-
/**
|
|
1297
|
-
* Project one `SandboxEvent` onto the `tool_call` / `tool_result` variants of
|
|
1298
|
-
* `RuntimeStreamEvent` — the tool-part projection `mapSandboxEvent`
|
|
1299
|
-
* deliberately does NOT perform. Opt-in and additive: `mapSandboxEvent`'s
|
|
1300
|
-
* default vocabulary (text/reasoning deltas + `llm_call`) is unchanged;
|
|
1301
|
-
* consumers that need the tool surface (chat UIs rendering tool activity)
|
|
1302
|
-
* compose this projector alongside it — `streamAgentTurn` does exactly that
|
|
1303
|
-
* under its `preserveToolParts` option.
|
|
1304
|
-
*
|
|
1305
|
-
* Handled shapes (observed on the opencode / claude-code sandbox backends):
|
|
1306
|
-
* - `message.part.updated` with `part.type === 'tool'` — stateful: a
|
|
1307
|
-
* `tool_call` on the call id's first frame (args from `state.input` or
|
|
1308
|
-
* `state.metadata.input`), a `tool_result` when the status transitions to
|
|
1309
|
-
* `completed` (result from `state.output` / `metadata.output`) or to a
|
|
1310
|
-
* terminal failure (result is `{ error, status, output? }` — the error
|
|
1311
|
-
* surfaced in-band, never dropped).
|
|
1312
|
-
* - bare `tool*` event types (`tool.call`, `tool_result`, …) — stateless:
|
|
1313
|
-
* `*result*` types project to `tool_result`, the rest to `tool_call`.
|
|
1314
|
-
* - codex `raw` events whose inner `item.started` / `item.updated` /
|
|
1315
|
-
* `item.completed` carries a command, MCP, collaboration, web search or
|
|
1316
|
-
* file change item — stateful on the item id, like a tool part. Other raw
|
|
1317
|
-
* events, including opencode's raw `tool_use` echoes, project nothing.
|
|
1318
|
-
*
|
|
1319
|
-
* Returns `[]` for every non-tool event.
|
|
1320
|
-
*
|
|
1321
|
-
* @experimental
|
|
1322
|
-
*/
|
|
1323
|
-
function mapSandboxToolEvent(event, state) {
|
|
1324
|
-
if (!event || typeof event !== "object") return [];
|
|
1325
|
-
const type = String(event.type ?? "");
|
|
1326
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
1327
|
-
if (type === "message.part.updated") {
|
|
1328
|
-
const part = data.part && typeof data.part === "object" ? data.part : {};
|
|
1329
|
-
if (String(part.type ?? "") !== "tool") return [];
|
|
1330
|
-
return projectToolPart(part, state, typeof event.id === "string" ? event.id : void 0);
|
|
1331
|
-
}
|
|
1332
|
-
if (type === "raw") return projectCodexItem(data, state);
|
|
1333
|
-
if (CANONICAL_STREAM_EVENT_TYPES.has(type)) return [];
|
|
1334
|
-
if (type.includes("tool")) {
|
|
1335
|
-
const callId = pickString(data, [
|
|
1336
|
-
"toolCallId",
|
|
1337
|
-
"tool_use_id",
|
|
1338
|
-
"id"
|
|
1339
|
-
]) ?? (typeof event.id === "string" ? event.id : void 0) ?? `sandbox-tool-${++state.seq}`;
|
|
1340
|
-
const toolName = pickString(data, [
|
|
1341
|
-
"name",
|
|
1342
|
-
"toolName",
|
|
1343
|
-
"tool"
|
|
1344
|
-
]) ?? "sandbox_tool";
|
|
1345
|
-
if (type.includes("result")) return [{
|
|
1346
|
-
type: "tool_result",
|
|
1347
|
-
toolName,
|
|
1348
|
-
toolCallId: callId,
|
|
1349
|
-
result: data.output ?? data.result ?? data.content ?? data
|
|
1350
|
-
}];
|
|
1351
|
-
return [{
|
|
1352
|
-
type: "tool_call",
|
|
1353
|
-
toolName,
|
|
1354
|
-
toolCallId: callId,
|
|
1355
|
-
args: data.input ?? data.args ?? {}
|
|
1356
|
-
}];
|
|
1357
|
-
}
|
|
1358
|
-
return [];
|
|
1359
|
-
}
|
|
1360
|
-
function projectToolPart(part, state, eventId) {
|
|
1361
|
-
const callId = pickString(part, [
|
|
1362
|
-
"callID",
|
|
1363
|
-
"callId",
|
|
1364
|
-
"toolCallId",
|
|
1365
|
-
"id"
|
|
1366
|
-
]) ?? eventId ?? `sandbox-tool-${++state.seq}`;
|
|
1367
|
-
const toolName = pickString(part, [
|
|
1368
|
-
"tool",
|
|
1369
|
-
"toolName",
|
|
1370
|
-
"name"
|
|
1371
|
-
]) ?? "sandbox_tool";
|
|
1372
|
-
const toolState = part.state && typeof part.state === "object" ? part.state : {};
|
|
1373
|
-
const metadata = toolState.metadata && typeof toolState.metadata === "object" ? toolState.metadata : {};
|
|
1374
|
-
const status = pickString(toolState, ["status"]) ?? "updated";
|
|
1375
|
-
const previous = state.statusByCall.get(callId);
|
|
1376
|
-
if (previous === "completed" || previous !== void 0 && TERMINAL_FAILURE.test(previous)) return [];
|
|
1377
|
-
const out = [];
|
|
1378
|
-
if (previous === void 0) out.push({
|
|
1379
|
-
type: "tool_call",
|
|
1380
|
-
toolName,
|
|
1381
|
-
toolCallId: callId,
|
|
1382
|
-
args: toolState.input ?? metadata.input ?? {}
|
|
1383
|
-
});
|
|
1384
|
-
state.statusByCall.set(callId, status);
|
|
1385
|
-
if (status === "completed") out.push({
|
|
1386
|
-
type: "tool_result",
|
|
1387
|
-
toolName,
|
|
1388
|
-
toolCallId: callId,
|
|
1389
|
-
result: toolState.output ?? metadata.output ?? ""
|
|
1390
|
-
});
|
|
1391
|
-
else if (TERMINAL_FAILURE.test(status)) {
|
|
1392
|
-
const message = pickString(toolState, ["error", "message"]) ?? pickString(metadata, ["error", "message"]) ?? `sandbox tool ended with status ${status}`;
|
|
1393
|
-
const output = toolState.output ?? metadata.output;
|
|
1394
|
-
out.push({
|
|
1395
|
-
type: "tool_result",
|
|
1396
|
-
toolName,
|
|
1397
|
-
toolCallId: callId,
|
|
1398
|
-
result: {
|
|
1399
|
-
error: message,
|
|
1400
|
-
status,
|
|
1401
|
-
...output !== void 0 ? { output } : {}
|
|
1402
|
-
}
|
|
1403
|
-
});
|
|
1404
|
-
}
|
|
1405
|
-
return out;
|
|
1406
|
-
}
|
|
1407
|
-
/** Codex item types that are tool calls. Reasoning, messages and todo lists are not. */
|
|
1408
|
-
const CODEX_TOOL_ITEMS = /* @__PURE__ */ new Set([
|
|
1409
|
-
"command_execution",
|
|
1410
|
-
"mcp_tool_call",
|
|
1411
|
-
"collab_tool_call",
|
|
1412
|
-
"web_search",
|
|
1413
|
-
"file_change"
|
|
1414
|
-
]);
|
|
1415
|
-
/** Item fields that describe the call's progress or outcome rather than what it was asked to do. */
|
|
1416
|
-
const CODEX_ITEM_OUTCOME_FIELDS = /* @__PURE__ */ new Set([
|
|
1417
|
-
"id",
|
|
1418
|
-
"type",
|
|
1419
|
-
"status",
|
|
1420
|
-
"result",
|
|
1421
|
-
"error",
|
|
1422
|
-
"aggregated_output",
|
|
1423
|
-
"exit_code"
|
|
1424
|
-
]);
|
|
1425
|
-
/**
|
|
1426
|
-
* Project a codex `item.started` / `item.updated` / `item.completed` tool item through the same
|
|
1427
|
-
* per-call state as a tool part, keyed on the item id: one `tool_call` on its first frame and one
|
|
1428
|
-
* `tool_result` when it completes. Tool names follow Eval's codex intake: `server/tool` for MCP.
|
|
1429
|
-
*/
|
|
1430
|
-
function projectCodexItem(data, state) {
|
|
1431
|
-
if (data.backend !== "codex" || !data.event || typeof data.event !== "object") return [];
|
|
1432
|
-
const inner = data.event;
|
|
1433
|
-
const phase = inner.type;
|
|
1434
|
-
if (phase !== "item.started" && phase !== "item.updated" && phase !== "item.completed") return [];
|
|
1435
|
-
if (!inner.item || typeof inner.item !== "object") return [];
|
|
1436
|
-
const item = inner.item;
|
|
1437
|
-
const itemType = pickString(item, ["type"]);
|
|
1438
|
-
if (itemType === void 0 || !CODEX_TOOL_ITEMS.has(itemType)) return [];
|
|
1439
|
-
const error = item.error && typeof item.error === "object" ? pickString(item.error, ["message"]) ?? `${itemType} failed` : void 0;
|
|
1440
|
-
const failed = item.status === "failed" || error !== void 0 || itemType === "command_execution" && typeof item.exit_code === "number" && item.exit_code !== 0;
|
|
1441
|
-
const status = phase !== "item.completed" ? "running" : failed ? "failed" : "completed";
|
|
1442
|
-
const input = Object.fromEntries(Object.entries(item).filter(([key]) => !CODEX_ITEM_OUTCOME_FIELDS.has(key)));
|
|
1443
|
-
const output = item.result ?? item.aggregated_output ?? item.changes;
|
|
1444
|
-
return projectToolPart({
|
|
1445
|
-
...typeof item.id === "string" ? { callID: item.id } : {},
|
|
1446
|
-
tool: codexToolName(itemType, item),
|
|
1447
|
-
state: {
|
|
1448
|
-
status,
|
|
1449
|
-
input,
|
|
1450
|
-
...output === void 0 || output === null ? {} : { output },
|
|
1451
|
-
...status === "failed" ? { error: error ?? `${itemType} failed` } : {}
|
|
1452
|
-
}
|
|
1453
|
-
}, state, void 0);
|
|
1454
|
-
}
|
|
1455
|
-
function codexToolName(itemType, item) {
|
|
1456
|
-
if (itemType === "mcp_tool_call") {
|
|
1457
|
-
const name = [pickString(item, ["server"]), pickString(item, ["tool"])].filter((part) => part !== void 0).join("/");
|
|
1458
|
-
return name === "" ? "mcp" : name;
|
|
1459
|
-
}
|
|
1460
|
-
if (itemType === "collab_tool_call") return pickString(item, ["tool"]) ?? "subagent";
|
|
1461
|
-
if (itemType === "web_search" || itemType === "file_change") return itemType;
|
|
1462
|
-
return pickString(item, ["name"]) ?? itemType;
|
|
1463
|
-
}
|
|
1464
|
-
function pickString(data, keys) {
|
|
1465
|
-
for (const key of keys) {
|
|
1466
|
-
const value = data[key];
|
|
1467
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
/**
|
|
1471
|
-
* Project one `SandboxEvent` onto the `RuntimeStreamEvent` chat-UX vocabulary,
|
|
1472
|
-
* for runtimes that bridge a sandbox `streamPrompt` into the
|
|
1473
|
-
* `AgentRuntime.act` streaming contract. Returns `undefined` for events that
|
|
1474
|
-
* have no faithful projection — the raw stream is preserved separately for the
|
|
1475
|
-
* `OutputAdapter`, so an unmapped event never loses data.
|
|
1476
|
-
*
|
|
1477
|
-
* Mapped (the task-optional incremental variants — no synthesized task
|
|
1478
|
-
* lifecycle, no guessed tool-part shapes):
|
|
1479
|
-
* - `message.part.updated` text part → `text_delta`
|
|
1480
|
-
* - `message.part.updated` reasoning/thinking part → `reasoning_delta`
|
|
1481
|
-
* - cost-bearing events → `llm_call` (shared with the ledger extractor)
|
|
1482
|
-
*
|
|
1483
|
-
* Tool parts are deliberately NOT mapped here (unchanged default) — compose
|
|
1484
|
-
* {@link mapSandboxToolEvent} alongside when a consumer needs them.
|
|
1485
|
-
*
|
|
1486
|
-
* The opencode backend emits incremental text as
|
|
1487
|
-
* `{ type: 'message.part.updated', data: { part: { type, text }, delta } }`;
|
|
1488
|
-
* `delta` is the increment, `part.text` the running accumulation.
|
|
1489
|
-
*/
|
|
1490
|
-
function mapSandboxEvent(event, opts = {}) {
|
|
1491
|
-
if (!event || typeof event !== "object") return void 0;
|
|
1492
|
-
const type = String(event.type ?? "");
|
|
1493
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
1494
|
-
if (type === "message.part.updated") {
|
|
1495
|
-
const part = data.part && typeof data.part === "object" ? data.part : {};
|
|
1496
|
-
const partType = String(part.type ?? "");
|
|
1497
|
-
const text = (typeof data.delta === "string" ? data.delta : void 0) ?? (typeof part.text === "string" ? part.text : void 0);
|
|
1498
|
-
if (text === void 0) return void 0;
|
|
1499
|
-
if (partType === "text") return {
|
|
1500
|
-
type: "text_delta",
|
|
1501
|
-
text
|
|
1502
|
-
};
|
|
1503
|
-
if (partType === "reasoning" || partType === "thinking") return {
|
|
1504
|
-
type: "reasoning_delta",
|
|
1505
|
-
text
|
|
1506
|
-
};
|
|
1507
|
-
return;
|
|
1508
|
-
}
|
|
1509
|
-
return extractLlmCallEvent(event, opts.agentRunName ?? "agent");
|
|
1510
|
-
}
|
|
1511
|
-
/**
|
|
1512
|
-
* Project one `SandboxEvent` onto Runtime's executor progress vocabulary: incremental text and
|
|
1513
|
-
* reasoning, tool calls and results, and an interaction request. It composes the existing
|
|
1514
|
-
* projections ({@link mapSandboxEvent}, {@link mapSandboxToolEvent}, and the canonical Agent
|
|
1515
|
-
* Interface decode) so every sandbox-shaped executor publishes live output through one reader.
|
|
1516
|
-
* Usage-bearing events project to nothing here — accounting stays on the `tokens`/`cost`
|
|
1517
|
-
* channels.
|
|
1518
|
-
*
|
|
1519
|
-
* Pass one {@link SandboxToolPartState} per turn so a multi-frame tool call yields one call and
|
|
1520
|
-
* at most one result.
|
|
1521
|
-
*
|
|
1522
|
-
* @experimental
|
|
1523
|
-
*/
|
|
1524
|
-
function sandboxProgressEvents(event, state) {
|
|
1525
|
-
const canonical = canonicalStreamEventFromSandboxEvent(event);
|
|
1526
|
-
if (canonical?.type === "interaction") return [{
|
|
1527
|
-
kind: "interaction",
|
|
1528
|
-
request: canonical.request
|
|
1529
|
-
}];
|
|
1530
|
-
if (canonical?.type === "child-task") return [{
|
|
1531
|
-
kind: "child_task",
|
|
1532
|
-
event: canonical
|
|
1533
|
-
}];
|
|
1534
|
-
const tools = mapSandboxToolEvent(event, state).map((projected) => projected.type === "tool_call" ? {
|
|
1535
|
-
kind: "tool_call",
|
|
1536
|
-
toolName: projected.toolName,
|
|
1537
|
-
...projected.toolCallId === void 0 ? {} : { toolCallId: projected.toolCallId },
|
|
1538
|
-
...projected.args === void 0 ? {} : { args: projected.args }
|
|
1539
|
-
} : {
|
|
1540
|
-
kind: "tool_result",
|
|
1541
|
-
toolName: projected.toolName,
|
|
1542
|
-
...projected.toolCallId === void 0 ? {} : { toolCallId: projected.toolCallId },
|
|
1543
|
-
...projected.result === void 0 ? {} : { result: projected.result }
|
|
1544
|
-
});
|
|
1545
|
-
if (tools.length > 0) return tools;
|
|
1546
|
-
const mapped = mapSandboxEvent(event);
|
|
1547
|
-
if (mapped?.type === "text_delta") return [{
|
|
1548
|
-
kind: "text_delta",
|
|
1549
|
-
text: mapped.text
|
|
1550
|
-
}];
|
|
1551
|
-
if (mapped?.type === "reasoning_delta") return [{
|
|
1552
|
-
kind: "reasoning_delta",
|
|
1553
|
-
text: mapped.text
|
|
1554
|
-
}];
|
|
1555
|
-
return [];
|
|
1556
|
-
}
|
|
1557
|
-
/** Project the complete Sandbox event stream into the standard executor result shape. */
|
|
1558
|
-
function sandboxLeafOutputFromEvents(events) {
|
|
1559
|
-
const content = collectAgentFinalMessageText(events) ?? collectAgentResponseText(events);
|
|
1560
|
-
const toolCalls = sandboxToolCalls(events);
|
|
1561
|
-
const servedBackend = lastServedBackend(events);
|
|
1562
|
-
return {
|
|
1563
|
-
events,
|
|
1564
|
-
content,
|
|
1565
|
-
output: sandboxOutputMarker(content, events.some(carriesText)),
|
|
1566
|
-
...servedBackend === void 0 ? {} : { servedBackend },
|
|
1567
|
-
...toolCalls.length === 0 ? {} : { toolCalls }
|
|
1568
|
-
};
|
|
1569
|
-
}
|
|
1570
|
-
/**
|
|
1571
|
-
* Classify observed text into the explicit output marker. `sawTextEvent` says whether the stream
|
|
1572
|
-
* carried a text-bearing event at all, which is what separates a box that answered nothing from a
|
|
1573
|
-
* box whose answer was never observed.
|
|
1574
|
-
*/
|
|
1575
|
-
function sandboxOutputMarker(content, sawTextEvent = content !== void 0) {
|
|
1576
|
-
if (content !== void 0 && content.length > 0) return {
|
|
1577
|
-
kind: "text",
|
|
1578
|
-
bytes: Buffer.byteLength(content, "utf8")
|
|
1579
|
-
};
|
|
1580
|
-
return sawTextEvent ? { kind: "empty" } : { kind: "absent" };
|
|
1581
|
-
}
|
|
1582
|
-
/** True when the event carries a text field a turn's answer could come from, empty or not. */
|
|
1583
|
-
function carriesText(event) {
|
|
1584
|
-
if (!event || typeof event !== "object") return false;
|
|
1585
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
1586
|
-
if (typeof data.finalText === "string" || typeof data.text === "string") return true;
|
|
1587
|
-
if (typeof data.delta === "string") return true;
|
|
1588
|
-
const part = data.part && typeof data.part === "object" ? data.part : void 0;
|
|
1589
|
-
return part !== void 0 && part.type === "text" && typeof part.text === "string";
|
|
1590
|
-
}
|
|
1591
|
-
/** The last served identity the platform reported across a turn's events, or `undefined`. */
|
|
1592
|
-
function lastServedBackend(events) {
|
|
1593
|
-
let served;
|
|
1594
|
-
for (const event of events) {
|
|
1595
|
-
const next = sandboxEventServedBackend(event);
|
|
1596
|
-
if (next !== void 0) served = next;
|
|
1597
|
-
}
|
|
1598
|
-
return served;
|
|
1599
|
-
}
|
|
1600
|
-
function sandboxToolCalls(events) {
|
|
1601
|
-
const state = createSandboxToolPartState();
|
|
1602
|
-
return events.flatMap((event) => mapSandboxToolEvent(event, state).flatMap((projected) => projected.type === "tool_call" ? [{
|
|
1603
|
-
...projected.toolCallId === void 0 ? {} : { id: projected.toolCallId },
|
|
1604
|
-
name: projected.toolName,
|
|
1605
|
-
arguments: projected.args ?? {}
|
|
1606
|
-
}] : []));
|
|
1607
|
-
}
|
|
1608
|
-
/** Read the final text off a terminal sandbox event, when present. */
|
|
1609
|
-
function terminalTextFromSandboxEvent(event) {
|
|
1610
|
-
if (!event || typeof event !== "object") return void 0;
|
|
1611
|
-
if (!isSandboxTerminalEvent(String(event.type ?? ""))) return void 0;
|
|
1612
|
-
const data = event.data && typeof event.data === "object" ? event.data : {};
|
|
1613
|
-
for (const key of [
|
|
1614
|
-
"finalText",
|
|
1615
|
-
"text",
|
|
1616
|
-
"response",
|
|
1617
|
-
"content"
|
|
1618
|
-
]) {
|
|
1619
|
-
const value = data[key];
|
|
1620
|
-
if (typeof value === "string") return value;
|
|
1621
|
-
}
|
|
1622
|
-
}
|
|
1623
|
-
// src/redact.ts
|
|
1624
|
-
/**
|
|
1625
|
-
* Redaction for values that may leave the Runtime process. The built-in
|
|
1626
|
-
* redactor is agent-eval's redaction core (`@tangle-network/agent-eval/traces`,
|
|
1627
|
-
* default profile): credentials found by field name or value shape are
|
|
1628
|
-
* replaced whole, email, card, SSN and phone values in place, and token counts
|
|
1629
|
-
* such as `inputTokens` or `max_tokens` are kept. A customer with
|
|
1630
|
-
* domain-specific PII supplies their own `redact` hook.
|
|
1631
|
-
*
|
|
1632
|
-
* This is narrower than `src/sanitize.ts`, which drops fields of the runtime's
|
|
1633
|
-
* *event envelope* unless the caller opts in: here the value is opaque customer
|
|
1634
|
-
* payload, so the scrub is value-shaped, not schema-shaped.
|
|
1635
|
-
*
|
|
1636
|
-
* @experimental
|
|
1637
|
-
*/
|
|
1638
|
-
/**
|
|
1639
|
-
* The built-in redactor. Cycle-safe, depth-bounded and total: it never throws
|
|
1640
|
-
* on customer input and never mutates it.
|
|
1641
|
-
*/
|
|
1642
|
-
function defaultRedactor(value) {
|
|
1643
|
-
return redact(value).value;
|
|
1644
|
-
}
|
|
1645
|
-
/** Stable identity input for saved work that depends on built-in redaction behavior. */
|
|
1646
|
-
function defaultRedactorIdentityMaterial() {
|
|
1647
|
-
return {
|
|
1648
|
-
core: "@tangle-network/agent-eval/traces redact",
|
|
1649
|
-
profile: "default",
|
|
1650
|
-
version: REDACTION_VERSION$1
|
|
1651
|
-
};
|
|
1652
|
-
}
|
|
1653
|
-
/**
|
|
1654
|
-
* Resolve the redactor a client uses. A caller-supplied hook handles
|
|
1655
|
-
* domain-specific values first, then the built-in scrubber still removes
|
|
1656
|
-
* credentials and personal data. Returning `false` is the explicit opt-out for
|
|
1657
|
-
* already-reviewed public values.
|
|
1658
|
-
*/
|
|
1659
|
-
function resolveRedactor(redact) {
|
|
1660
|
-
if (redact === false) return (value) => value;
|
|
1661
|
-
if (!redact) return defaultRedactor;
|
|
1662
|
-
return (value) => defaultRedactor(redact(value));
|
|
1663
|
-
}
|
|
1664
|
-
// src/runtime/supervise/spend-validation.ts
|
|
1665
|
-
/** Reject malformed ceilings before they can mint capacity through negative reservations. */
|
|
1666
|
-
function assertValidBudget(budget, label = "budget") {
|
|
1667
|
-
const safeInteger = (value, field) => {
|
|
1668
|
-
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label}.${field} must be a non-negative safe integer`);
|
|
1669
|
-
};
|
|
1670
|
-
const finiteNonNegative = (value, field) => {
|
|
1671
|
-
if (!Number.isFinite(value) || value < 0) throw new Error(`${label}.${field} must be a non-negative finite number`);
|
|
1672
|
-
};
|
|
1673
|
-
assertResources(budget.resources, "limit", `${label}.resources`);
|
|
1674
|
-
safeInteger(budget.maxIterations, "maxIterations");
|
|
1675
|
-
safeInteger(budget.maxTokens, "maxTokens");
|
|
1676
|
-
if (budget.maxUsd !== void 0) finiteNonNegative(budget.maxUsd, "maxUsd");
|
|
1677
|
-
if (budget.deadlineMs !== void 0) finiteNonNegative(budget.deadlineMs, "deadlineMs");
|
|
1678
|
-
}
|
|
1679
|
-
function assertValidSpend(spend, label) {
|
|
1680
|
-
assertResources(spend.resources, "amount", `${label}.resources`);
|
|
1681
|
-
if (!Number.isSafeInteger(spend.iterations) || spend.iterations < 0) throw new Error(`${label}.iterations must be a non-negative safe integer`);
|
|
1682
|
-
for (const [field, value] of [
|
|
1683
|
-
["input", spend.tokens.input],
|
|
1684
|
-
["output", spend.tokens.output],
|
|
1685
|
-
["freshInput", spend.tokens.freshInput],
|
|
1686
|
-
["cacheRead", spend.tokens.cacheRead],
|
|
1687
|
-
["cacheWrite", spend.tokens.cacheWrite]
|
|
1688
|
-
]) {
|
|
1689
|
-
if (value === void 0) continue;
|
|
1690
|
-
if (!Number.isSafeInteger(value) || value < 0) throw new Error(`${label}.tokens.${field} must be a non-negative safe integer`);
|
|
1691
|
-
}
|
|
1692
|
-
for (const [field, value] of [["tokensKnown", spend.tokens.tokensKnown], ["cacheBreakdownKnown", spend.tokens.cacheBreakdownKnown]]) if (value !== void 0 && value !== false) throw new Error(`${label}.tokens.${field} must be false when present`);
|
|
1693
|
-
const { freshInput, cacheRead, cacheWrite } = spend.tokens;
|
|
1694
|
-
if (freshInput !== void 0 && cacheRead !== void 0 && cacheWrite !== void 0) {
|
|
1695
|
-
const classified = freshInput + cacheRead + cacheWrite;
|
|
1696
|
-
if (spend.tokens.cacheBreakdownKnown !== false) {
|
|
1697
|
-
if (classified !== spend.tokens.input) throw new Error(`${label}.tokens cache classes must sum to input`);
|
|
1698
|
-
} else if (classified > spend.tokens.input) throw new Error(`${label}.tokens cache classes must not exceed input`);
|
|
1699
|
-
}
|
|
1700
|
-
for (const [field, value] of [["usd", spend.usd], ["ms", spend.ms]]) if (!Number.isFinite(value) || value < 0) throw new Error(`${label}.${field} must be a non-negative finite number`);
|
|
1701
|
-
if (spend.usdEstimated !== void 0) {
|
|
1702
|
-
if (!Number.isFinite(spend.usdEstimated) || spend.usdEstimated < 0) throw new Error(`${label}.usdEstimated must be a non-negative finite number`);
|
|
1703
|
-
if (spend.usdEstimated > spend.usd) throw new Error(`${label}.usdEstimated must not exceed ${label}.usd`);
|
|
1704
|
-
if (spend.usdKnown !== false) throw new Error(`${label}.usdEstimated requires ${label}.usdKnown false`);
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
// src/runtime/supervise/budget-ledger.ts
|
|
1708
|
-
/**
|
|
1709
|
-
* The balances of one conserved budget pool, and the arithmetic that moves them. `createBudgetPool`
|
|
1710
|
-
* (`./budget`) owns tickets, the wait queue and the public contract; this module owns the numbers:
|
|
1711
|
-
* `total ≡ free + reserved + committed` per channel, the taint flags that say a balance is a
|
|
1712
|
-
* ceiling rather than a measurement, and the named-resource states.
|
|
1713
|
-
*/
|
|
1714
|
-
function openBudgetLedger(root, runStartedAtMs) {
|
|
1715
|
-
return {
|
|
1716
|
-
usdCapped: root.maxUsd !== void 0,
|
|
1717
|
-
absoluteDeadlineMs: root.deadlineMs === void 0 ? 0 : runStartedAtMs + root.deadlineMs,
|
|
1718
|
-
resources: new Map(Object.entries(root.resources ?? {}).map(([name, value]) => [name, {
|
|
1719
|
-
...value,
|
|
1720
|
-
remaining: value.limit,
|
|
1721
|
-
reserved: 0,
|
|
1722
|
-
committed: 0,
|
|
1723
|
-
known: true
|
|
1724
|
-
}])),
|
|
1725
|
-
freeTokens: root.maxTokens,
|
|
1726
|
-
reservedTokens: 0,
|
|
1727
|
-
committedTokens: 0,
|
|
1728
|
-
tokensTainted: false,
|
|
1729
|
-
cacheBreakdownTainted: false,
|
|
1730
|
-
freeUsd: root.maxUsd ?? 0,
|
|
1731
|
-
reservedUsd: 0,
|
|
1732
|
-
committedUsd: 0,
|
|
1733
|
-
usdTainted: false,
|
|
1734
|
-
usdMeasured: true,
|
|
1735
|
-
freeIterations: root.maxIterations,
|
|
1736
|
-
reservedIterations: 0,
|
|
1737
|
-
committedIterations: 0
|
|
1738
|
-
};
|
|
1739
|
-
}
|
|
1740
|
-
function ledgerValidateUnits(l, spend) {
|
|
1741
|
-
for (const [name, value] of Object.entries(spend.resources ?? {})) {
|
|
1742
|
-
const limit = l.resources.get(name);
|
|
1743
|
-
if (limit && limit.unit !== value.unit) throw new Error(`resource ${name}: unit mismatch`);
|
|
1744
|
-
}
|
|
1745
|
-
}
|
|
1746
|
-
/** Record that `spend` carried an unreported or unclassified channel. */
|
|
1747
|
-
function noteUnknowns(l, spend) {
|
|
1748
|
-
if (spend.tokensKnown === false) l.tokensTainted = true;
|
|
1749
|
-
if (!hasCompleteCacheBreakdown(spend.tokens)) l.cacheBreakdownTainted = true;
|
|
1750
|
-
if (spend.usdKnown === false) l.usdMeasured = false;
|
|
1751
|
-
}
|
|
1752
|
-
/**
|
|
1753
|
-
* Commit named-resource spend and classify it. `allocations` is the settling reservation's own
|
|
1754
|
-
* allocations, or `undefined` for spend holding no reservation (`observe`, restore). Unverifiable
|
|
1755
|
-
* usage is a `fault` (the remaining balance is consumed so admission closes); measured usage above
|
|
1756
|
-
* an allocation is `overspent`; without allocations, usage leaving a root limit overdrawn is
|
|
1757
|
-
* `rootExceeded`. A reservation within its own allocation is never charged with a root overdraw:
|
|
1758
|
-
* a negative balance at its settlement was caused, and reported, by earlier spend.
|
|
1759
|
-
*/
|
|
1760
|
-
function ledgerCommitResources(l, spend, allocations, requireAll = true) {
|
|
1761
|
-
let fault;
|
|
1762
|
-
let rootExceeded;
|
|
1763
|
-
const overspent = [];
|
|
1764
|
-
for (const [name, state] of l.resources) {
|
|
1765
|
-
const value = spend.resources?.[name];
|
|
1766
|
-
if (!requireAll && value === void 0) continue;
|
|
1767
|
-
const allocation = allocations?.[name]?.limit ?? 0;
|
|
1768
|
-
const amount = value?.amount ?? 0;
|
|
1769
|
-
state.reserved -= allocation;
|
|
1770
|
-
const committed = state.committed + amount;
|
|
1771
|
-
const overflow = !Number.isSafeInteger(committed);
|
|
1772
|
-
state.committed = Math.min(committed, Number.MAX_SAFE_INTEGER);
|
|
1773
|
-
state.remaining = state.limit - state.reserved - state.committed;
|
|
1774
|
-
if (value?.known !== true || overflow) {
|
|
1775
|
-
state.known = false;
|
|
1776
|
-
const retained = Math.max(0, state.remaining);
|
|
1777
|
-
state.committed += retained;
|
|
1778
|
-
state.remaining -= retained;
|
|
1779
|
-
fault ??= overflow ? `resource ${name}: amount overflow under an enforced limit` : `resource ${name}: unknown usage under an enforced limit`;
|
|
1780
|
-
} else if (allocations !== void 0) {
|
|
1781
|
-
if (amount > allocation) overspent.push({
|
|
1782
|
-
channel: `resource:${name}`,
|
|
1783
|
-
reserved: allocation,
|
|
1784
|
-
spent: amount
|
|
1785
|
-
});
|
|
1786
|
-
} else if (state.remaining < 0) rootExceeded ??= `resource ${name}: exceeded root limit ${state.limit}`;
|
|
1787
|
-
}
|
|
1788
|
-
return {
|
|
1789
|
-
...fault ? { fault } : {},
|
|
1790
|
-
overspent,
|
|
1791
|
-
...rootExceeded ? { rootExceeded } : {}
|
|
1792
|
-
};
|
|
1793
|
-
}
|
|
1794
|
-
/** Every channel `b` does not fit, measured against the free balance less `keep`. */
|
|
1795
|
-
function ledgerShortfalls(l, b, keep) {
|
|
1796
|
-
const wantTokens = b.maxTokens;
|
|
1797
|
-
const wantUsd = b.maxUsd ?? 0;
|
|
1798
|
-
const wantIterations = b.maxIterations;
|
|
1799
|
-
const shortfalls = [];
|
|
1800
|
-
const short = (channel, requested, free, held, closedByUnknownSpend = false) => {
|
|
1801
|
-
shortfalls.push({
|
|
1802
|
-
channel,
|
|
1803
|
-
requested,
|
|
1804
|
-
free: closedByUnknownSpend ? 0 : Math.max(0, free),
|
|
1805
|
-
...closedByUnknownSpend || held <= 0 ? {} : { held },
|
|
1806
|
-
...closedByUnknownSpend ? { closedByUnknownSpend: true } : {}
|
|
1807
|
-
});
|
|
1808
|
-
};
|
|
1809
|
-
for (const [name, state] of l.resources) {
|
|
1810
|
-
const limit = b.resources[name].limit;
|
|
1811
|
-
if (!state.known) short(`resource:${name}`, limit, 0, 0, true);
|
|
1812
|
-
else if (limit > state.remaining) short(`resource:${name}`, limit, state.remaining, state.reserved);
|
|
1813
|
-
}
|
|
1814
|
-
if (l.usdCapped && l.usdTainted) short("usd", wantUsd, 0, 0, true);
|
|
1815
|
-
const tokensFree = l.freeTokens - (keep?.tokens ?? 0);
|
|
1816
|
-
const iterationsFree = l.freeIterations - (keep?.iterations ?? 0);
|
|
1817
|
-
if (wantTokens > tokensFree) short("tokens", wantTokens, tokensFree, l.reservedTokens);
|
|
1818
|
-
if (wantIterations > iterationsFree) short("iterations", wantIterations, iterationsFree, l.reservedIterations);
|
|
1819
|
-
const usdFree = l.freeUsd - (keep?.usd ?? 0);
|
|
1820
|
-
if (l.usdCapped && !l.usdTainted && wantUsd > usdFree) short("usd", wantUsd, usdFree, l.reservedUsd);
|
|
1821
|
-
return shortfalls;
|
|
1822
|
-
}
|
|
1823
|
-
/** Move `b` from free to reserved. The caller has checked that it fits. */
|
|
1824
|
-
function ledgerTake(l, b) {
|
|
1825
|
-
for (const [name, state] of l.resources) {
|
|
1826
|
-
const amount = b.resources[name].limit;
|
|
1827
|
-
state.remaining -= amount;
|
|
1828
|
-
state.reserved += amount;
|
|
1829
|
-
}
|
|
1830
|
-
l.freeTokens -= b.maxTokens;
|
|
1831
|
-
l.reservedTokens += b.maxTokens;
|
|
1832
|
-
l.freeIterations -= b.maxIterations;
|
|
1833
|
-
l.reservedIterations += b.maxIterations;
|
|
1834
|
-
const wantUsd = b.maxUsd ?? 0;
|
|
1835
|
-
if (wantUsd > 0) {
|
|
1836
|
-
l.freeUsd -= wantUsd;
|
|
1837
|
-
l.reservedUsd += wantUsd;
|
|
1838
|
-
}
|
|
1839
|
-
}
|
|
1840
|
-
/**
|
|
1841
|
-
* The overspends a settlement of `spent` against `held` decides, before any balance moves. An
|
|
1842
|
-
* OVERSPEND is measured spend above the reservation; unknown dollars under a dollar cap (a fault
|
|
1843
|
-
* the caller reports) are flagged `unknownUnderCap`. `usdBudgeted` is whether the child declared a
|
|
1844
|
-
* dollar ceiling: without `maxUsd` it reserved NO dollars, so under a capped root its dollars are
|
|
1845
|
-
* OBSERVED spend, committed and debited from `free` like `observe`d inference.
|
|
1846
|
-
*/
|
|
1847
|
-
function ledgerClassifyOverspend(l, held, usdBudgeted, spent) {
|
|
1848
|
-
const unknownUnderCap = l.usdCapped && spent.usdKnown === false;
|
|
1849
|
-
const spentTokens = chargedTokens(spent.tokens);
|
|
1850
|
-
const overspent = [];
|
|
1851
|
-
if (spentTokens > held.tokens) overspent.push({
|
|
1852
|
-
channel: "tokens",
|
|
1853
|
-
reserved: held.tokens,
|
|
1854
|
-
spent: spentTokens
|
|
1855
|
-
});
|
|
1856
|
-
if (spent.iterations > held.iterations) overspent.push({
|
|
1857
|
-
channel: "iterations",
|
|
1858
|
-
reserved: held.iterations,
|
|
1859
|
-
spent: spent.iterations
|
|
1860
|
-
});
|
|
1861
|
-
if (!unknownUnderCap && l.usdCapped && usdBudgeted && spent.usd > held.usd) overspent.push({
|
|
1862
|
-
channel: "usd",
|
|
1863
|
-
reserved: held.usd,
|
|
1864
|
-
spent: spent.usd
|
|
1865
|
-
});
|
|
1866
|
-
return {
|
|
1867
|
-
overspent,
|
|
1868
|
-
unknownUnderCap
|
|
1869
|
-
};
|
|
1870
|
-
}
|
|
1871
|
-
/**
|
|
1872
|
-
* Release the whole reservation `held`, then commit actual spend; the difference is the refund
|
|
1873
|
-
* that flows back to `free`. Unconditional arithmetic: nothing here throws on spend it can read.
|
|
1874
|
-
*/
|
|
1875
|
-
function ledgerSettle(l, held, spent, unknownUnderCap) {
|
|
1876
|
-
noteUnknowns(l, spent);
|
|
1877
|
-
const spentTokens = chargedTokens(spent.tokens);
|
|
1878
|
-
l.reservedTokens -= held.tokens;
|
|
1879
|
-
l.committedTokens += spentTokens;
|
|
1880
|
-
l.freeTokens += held.tokens - spentTokens;
|
|
1881
|
-
l.reservedIterations -= held.iterations;
|
|
1882
|
-
l.committedIterations += spent.iterations;
|
|
1883
|
-
l.freeIterations += held.iterations - spent.iterations;
|
|
1884
|
-
if (l.usdCapped) {
|
|
1885
|
-
l.reservedUsd -= held.usd;
|
|
1886
|
-
l.committedUsd += spent.usd;
|
|
1887
|
-
if (unknownUnderCap) {
|
|
1888
|
-
l.usdTainted = true;
|
|
1889
|
-
l.freeUsd = 0;
|
|
1890
|
-
} else l.freeUsd += held.usd - spent.usd;
|
|
1891
|
-
} else l.committedUsd += spent.usd;
|
|
1892
|
-
return ledgerCommitResources(l, spent, held.resources ?? {});
|
|
1893
|
-
}
|
|
1894
|
-
/**
|
|
1895
|
-
* Direct free → committed debit (no ticket). `free` may go negative on overspend: the readout then
|
|
1896
|
-
* reports exhaustion and the in-loop guard halts the driver. Work with an unreported token count
|
|
1897
|
-
* still debits what IS known and marks the balance incomplete; refusing to record it is the "free
|
|
1898
|
-
* turn" the pool must never see.
|
|
1899
|
-
*/
|
|
1900
|
-
function ledgerObserve(l, spend, unknownUnderCap, requireAllResources) {
|
|
1901
|
-
noteUnknowns(l, spend);
|
|
1902
|
-
const tokens = chargedTokens(spend.tokens);
|
|
1903
|
-
l.freeTokens -= tokens;
|
|
1904
|
-
l.committedTokens += tokens;
|
|
1905
|
-
l.freeIterations -= spend.iterations;
|
|
1906
|
-
l.committedIterations += spend.iterations;
|
|
1907
|
-
l.committedUsd += spend.usd;
|
|
1908
|
-
if (l.usdCapped) l.freeUsd -= spend.usd;
|
|
1909
|
-
if (unknownUnderCap) l.usdTainted = true;
|
|
1910
|
-
return ledgerCommitResources(l, spend, void 0, requireAllResources);
|
|
1911
|
-
}
|
|
1912
|
-
/**
|
|
1913
|
-
* Reconstruct the balances from a prior process's durable record. Measured prior spend is debited
|
|
1914
|
-
* exactly once; a started-without-terminal child is charged at its entire reservation, so restart
|
|
1915
|
-
* never mints capacity and the false known flags stay visible. Direct arithmetic, not `observe`:
|
|
1916
|
-
* the prior spend ALREADY happened, and refusing to record it would be a zero-cost restart.
|
|
1917
|
-
*/
|
|
1918
|
-
function ledgerRestore(l, root, restore) {
|
|
1919
|
-
if (restore.committed !== void 0) {
|
|
1920
|
-
const committed = restore.committed;
|
|
1921
|
-
assertValidSpend(committed, "budget restore committed");
|
|
1922
|
-
ledgerValidateUnits(l, committed);
|
|
1923
|
-
ledgerCommitResources(l, committed);
|
|
1924
|
-
noteUnknowns(l, committed);
|
|
1925
|
-
const tokens = chargedTokens(committed.tokens);
|
|
1926
|
-
l.freeTokens -= tokens;
|
|
1927
|
-
l.committedTokens += tokens;
|
|
1928
|
-
l.freeIterations -= committed.iterations;
|
|
1929
|
-
l.committedIterations += committed.iterations;
|
|
1930
|
-
l.committedUsd += committed.usd;
|
|
1931
|
-
if (l.usdCapped) {
|
|
1932
|
-
l.freeUsd -= committed.usd;
|
|
1933
|
-
if (committed.usdKnown === false) {
|
|
1934
|
-
l.usdTainted = true;
|
|
1935
|
-
if (l.freeUsd > 0) l.committedUsd += l.freeUsd;
|
|
1936
|
-
l.freeUsd = 0;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
for (const [index, uncertain] of (restore.uncertainReservations ?? []).entries()) {
|
|
1941
|
-
assertValidBudget(uncertain, `budget restore uncertainReservations[${index}]`);
|
|
1942
|
-
ledgerCommitResources(l, withBudgetResources({
|
|
1943
|
-
iterations: 0,
|
|
1944
|
-
tokens: zeroTokenUsage(),
|
|
1945
|
-
usd: 0,
|
|
1946
|
-
ms: 0
|
|
1947
|
-
}, root));
|
|
1948
|
-
l.tokensTainted = true;
|
|
1949
|
-
l.usdMeasured = false;
|
|
1950
|
-
l.freeTokens -= uncertain.maxTokens;
|
|
1951
|
-
l.committedTokens += uncertain.maxTokens;
|
|
1952
|
-
l.freeIterations -= uncertain.maxIterations;
|
|
1953
|
-
l.committedIterations += uncertain.maxIterations;
|
|
1954
|
-
if (!l.usdCapped) continue;
|
|
1955
|
-
if (uncertain.maxUsd === void 0) {
|
|
1956
|
-
l.usdTainted = true;
|
|
1957
|
-
l.committedUsd += l.freeUsd > 0 ? l.freeUsd : 0;
|
|
1958
|
-
l.freeUsd = 0;
|
|
1959
|
-
} else {
|
|
1960
|
-
l.freeUsd -= uncertain.maxUsd;
|
|
1961
|
-
l.committedUsd += uncertain.maxUsd;
|
|
1962
|
-
}
|
|
1963
|
-
}
|
|
1964
|
-
}
|
|
1965
|
-
// src/runtime/supervise/usage-metering.ts
|
|
1966
|
-
/** Fold a normalized `UsageEvent` array into a `Spend`. Tokens and usd are separate
|
|
1967
|
-
* channels; iterations come from `'iteration'` events. Pure; `ms` stays zero (the
|
|
1968
|
-
* pool does not read wall-clock). */
|
|
1969
|
-
function spendFromUsageEvents(events) {
|
|
1970
|
-
const totals = newUsageTotals();
|
|
1971
|
-
for (const ev of events) meterUsageEvent(totals, ev);
|
|
1972
|
-
return spendFromUsageTotals(totals);
|
|
1973
|
-
}
|
|
1974
|
-
/** Fresh accounting totals, optionally restored from verified committed spend. */
|
|
1975
|
-
function newUsageTotals(priorSpent) {
|
|
1976
|
-
if (priorSpent !== void 0) assertValidSpend(priorSpent, "prior committed usage");
|
|
1977
|
-
return {
|
|
1978
|
-
...addResourceSpend(priorSpent?.resources),
|
|
1979
|
-
tokens: priorSpent === void 0 ? zeroTokenUsage() : cloneTokenUsage(priorSpent.tokens),
|
|
1980
|
-
tokensKnown: priorSpent?.tokensKnown !== false,
|
|
1981
|
-
...priorSpent?.tokensProvenance === void 0 ? {} : { tokensProvenance: priorSpent.tokensProvenance },
|
|
1982
|
-
usd: priorSpent?.usd ?? 0,
|
|
1983
|
-
usdEstimated: priorSpent?.usdEstimated ?? 0,
|
|
1984
|
-
usdKnown: priorSpent?.usdKnown !== false,
|
|
1985
|
-
iterations: priorSpent?.iterations ?? 0
|
|
1986
|
-
};
|
|
1987
|
-
}
|
|
1988
|
-
/**
|
|
1989
|
-
* Fold ONE usage event into running totals — the single place that decides which event kinds
|
|
1990
|
-
* meter. `progress` carries observed output rather than accounting, so it meters nothing; only
|
|
1991
|
-
* `iteration` advances the iteration count.
|
|
1992
|
-
*/
|
|
1993
|
-
function meterUsageEvent(totals, ev) {
|
|
1994
|
-
if (ev.kind === "resource") {
|
|
1995
|
-
totals.resources = addResourceSpend(totals.resources, Object.fromEntries([[ev.name, {
|
|
1996
|
-
unit: ev.unit,
|
|
1997
|
-
amount: ev.amount,
|
|
1998
|
-
known: ev.known
|
|
1999
|
-
}]])).resources;
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
if (ev.kind === "tokens") {
|
|
2003
|
-
if (ev.mode === "cumulative") {
|
|
2004
|
-
if (ev.input < totals.tokens.input || ev.output < totals.tokens.output) throw new ValidationError$1("cumulative executor tokens must not decrease");
|
|
2005
|
-
const snapshot = zeroTokenUsage();
|
|
2006
|
-
addTokenUsage(snapshot, {
|
|
2007
|
-
...ev,
|
|
2008
|
-
...ev.freshInput === void 0 && totals.tokens.freshInput !== void 0 ? { freshInput: totals.tokens.freshInput } : {},
|
|
2009
|
-
...ev.cacheRead === void 0 && totals.tokens.cacheRead !== void 0 ? { cacheRead: totals.tokens.cacheRead } : {},
|
|
2010
|
-
...ev.cacheWrite === void 0 && totals.tokens.cacheWrite !== void 0 ? { cacheWrite: totals.tokens.cacheWrite } : {},
|
|
2011
|
-
...totals.tokens.tokensKnown === false ? { tokensKnown: false } : {}
|
|
2012
|
-
});
|
|
2013
|
-
totals.tokens = snapshot;
|
|
2014
|
-
} else addTokenUsage(totals.tokens, ev);
|
|
2015
|
-
if (ev.tokensKnown === false) totals.tokensKnown = false;
|
|
2016
|
-
const source = ev.provenance ?? "stream-receipt";
|
|
2017
|
-
totals.tokensProvenance = totals.tokensProvenance === void 0 || totals.tokensProvenance === source ? source : "mixed";
|
|
2018
|
-
return;
|
|
2019
|
-
}
|
|
2020
|
-
if (ev.kind === "cost") {
|
|
2021
|
-
totals.usd += ev.usd;
|
|
2022
|
-
if (ev.usdKnown === false) {
|
|
2023
|
-
totals.usdEstimated += ev.usdEstimated ?? 0;
|
|
2024
|
-
totals.usdKnown = false;
|
|
2025
|
-
}
|
|
2026
|
-
return;
|
|
2027
|
-
}
|
|
2028
|
-
if (ev.kind === "iteration") totals.iterations += 1;
|
|
2029
|
-
}
|
|
2030
|
-
/** Project folded totals onto the conserved-pool `Spend`. `ms` stays zero — the pool does not
|
|
2031
|
-
* read wall-clock. */
|
|
2032
|
-
function spendFromUsageTotals(totals) {
|
|
2033
|
-
return {
|
|
2034
|
-
...addResourceSpend(totals.resources),
|
|
2035
|
-
iterations: totals.iterations,
|
|
2036
|
-
tokens: totals.tokens,
|
|
2037
|
-
...totals.tokensKnown ? {} : { tokensKnown: false },
|
|
2038
|
-
usd: totals.usd,
|
|
2039
|
-
...totals.usdKnown ? {} : { usdKnown: false },
|
|
2040
|
-
...totals.usdEstimated > 0 ? { usdEstimated: totals.usdEstimated } : {},
|
|
2041
|
-
...totals.tokensProvenance === void 0 || totals.tokensProvenance === "stream-receipt" ? {} : { tokensProvenance: totals.tokensProvenance },
|
|
2042
|
-
ms: 0
|
|
2043
|
-
};
|
|
2044
|
-
}
|
|
2045
|
-
async function foldUsage(events) {
|
|
2046
|
-
if (Array.isArray(events)) return spendFromUsageEvents(events);
|
|
2047
|
-
const totals = newUsageTotals();
|
|
2048
|
-
for await (const ev of events) meterUsageEvent(totals, ev);
|
|
2049
|
-
return spendFromUsageTotals(totals);
|
|
2050
|
-
}
|
|
2051
|
-
// src/runtime/supervise/budget.ts
|
|
2052
|
-
/**
|
|
2053
|
-
* The conserved budget reservation pool — the invariant the whole instrument rests on (critique
|
|
2054
|
-
* M5/B3). One root `Budget` becomes a pool of standard quantities (tokens, usd, iterations) and
|
|
2055
|
-
* caller-named resources plus an absolute deadline. Children reserve atomically at spawn and
|
|
2056
|
-
* reconcile at settle:
|
|
2057
|
-
*
|
|
2058
|
-
* total ≡ free + reserved + committed (for every known quantity)
|
|
2059
|
-
*
|
|
2060
|
-
* `reserve` moves a child's whole ceiling `free` → `reserved` and fails closed when `free` can't
|
|
2061
|
-
* cover it (never read-then-spawn overcommit, so `Σk(treatment) ≡ Σk(blind)` by construction).
|
|
2062
|
-
* `reconcile` releases the reservation, commits ACTUAL spend, and refunds the remainder to `free`.
|
|
2063
|
-
* Tokens and usd are separate channels; iterations are conserved alongside them.
|
|
2064
|
-
*
|
|
2065
|
-
* The token unit is `chargedTokens`: every token counted ONCE, when it first enters the context
|
|
2066
|
-
* (`input − cacheRead + output`, which is `freshInput + cacheWrite + output` under a complete
|
|
2067
|
-
* split). A cached prefix was already charged when written; charging a cache read again made a
|
|
2068
|
-
* declared cap bite about 56x early on a real fleet (cache is 98% of the rolled-up prompt total).
|
|
2069
|
-
* No price weight enters the token channel; money is budgeted on `maxUsd`. The unit is additive,
|
|
2070
|
-
* so a rolled-up report agrees with the pool. Prompt tokens the provider never classified are
|
|
2071
|
-
* charged in full and `readout().cacheBreakdownKnown` reads false (an upper bound on newly-
|
|
2072
|
-
* presented work). The pool trusts reported usage: it is an accounting unit, not a trust boundary.
|
|
2073
|
-
*
|
|
2074
|
-
* Named resources require explicit units, ceilings, and complete executor measurements; missing
|
|
2075
|
-
* or unknown enforced measurements close admission for that resource. The pool trusts those
|
|
2076
|
-
* receipts and meters no external system. `Spend.boxMinutes` is evidence only, with no implicit
|
|
2077
|
-
* resource ceiling; never present estimated box lifetime as a measured resource receipt.
|
|
2078
|
-
*
|
|
2079
|
-
* Pure and deterministic: the run's start instant is supplied; no I/O, wall-clock or RNG read. A
|
|
2080
|
-
* ticket is single-use (fail-loud on double or unknown reconcile). Reconciling an OPEN ticket
|
|
2081
|
-
* always closes it: a fail-loud condition settles the reservation first and throws afterwards, so
|
|
2082
|
-
* no error path strands a reservation past `assertNoOpenTickets`. Measured spend above a
|
|
2083
|
-
* reservation is not fail-loud: the pool commits it, returns a `BudgetViolation`, and refuses
|
|
2084
|
-
* later reservations from the reduced free balance. A child that declared no `maxUsd` reserved no
|
|
2085
|
-
* dollars, so its real dollars are committed as observed spend against the root, not as an
|
|
2086
|
-
* overspend of a $0 ceiling. Unknowable dollar cost under a dollar limit closes the known
|
|
2087
|
-
* token/iteration work, marks the dollar channel unusable and refuses later reservations. An
|
|
2088
|
-
* unknowable TOKEN count records the work, debits what is known, and marks
|
|
2089
|
-
* `readout().tokensKnown` false (a ceiling, not a measurement); tokens are always capped, so one
|
|
2090
|
-
* unreported turn must not end the run.
|
|
2091
|
-
*
|
|
2092
|
-
* A reservation the free balance cannot cover now can WAIT (`reserve(b, holder, { wait: true })`)
|
|
2093
|
-
* when it would fit once open reservations return. It holds nothing while waiting, so
|
|
2094
|
-
* conservation is untouched; waiters are granted in asking order and refused once no open
|
|
2095
|
-
* reservation is left to return any.
|
|
2096
|
-
*
|
|
2097
|
-
* A pool can be RESTORED from a prior process's durable record: measured committed spend is
|
|
2098
|
-
* debited once, and each child journaled as started but never settled is charged at its full
|
|
2099
|
-
* declared ceiling. Restart never mints capacity, unknown flags stay visible, and the original
|
|
2100
|
-
* absolute deadline never slides.
|
|
2101
|
-
*
|
|
2102
|
-
* @experimental
|
|
2103
|
-
*/
|
|
2104
|
-
/** A reconciliation whose spend the pool cannot verify, thrown after the reservation settled.
|
|
2105
|
-
* `budgetViolation` carries any measured overspend on the same settlement. */
|
|
2106
|
-
var BudgetReconcileFault = class extends Error {
|
|
2107
|
-
budgetViolation;
|
|
2108
|
-
constructor(message, budgetViolation) {
|
|
2109
|
-
super(message);
|
|
2110
|
-
this.name = "BudgetReconcileFault";
|
|
2111
|
-
if (budgetViolation !== void 0) this.budgetViolation = budgetViolation;
|
|
2112
|
-
}
|
|
2113
|
-
};
|
|
2114
|
-
/**
|
|
2115
|
-
* A waiting reservation that can never be granted: every open reservation settled and the free
|
|
2116
|
-
* balance still cannot cover it. `shortfalls` are the channels that did not fit at that moment.
|
|
2117
|
-
*/
|
|
2118
|
-
var ReservationWaitRefused = class extends Error {
|
|
2119
|
-
reason = "budget-exhausted";
|
|
2120
|
-
shortfalls;
|
|
2121
|
-
constructor(shortfalls) {
|
|
2122
|
-
super(`budget-exhausted: nothing left running could return enough budget for this worker (${shortfalls.map((shortfall) => `${shortfall.channel} requested ${shortfall.requested}, free ${shortfall.free}`).join("; ")})`);
|
|
2123
|
-
this.name = "ReservationWaitRefused";
|
|
2124
|
-
this.shortfalls = shortfalls;
|
|
2125
|
-
}
|
|
2126
|
-
};
|
|
2127
|
-
/**
|
|
2128
|
-
* Create a conserved reservation pool from a root `Budget`. `runStartedAtMs` is the WALL-CLOCK
|
|
2129
|
-
* instant the root was recorded; the deadline is `runStartedAtMs + root.deadlineMs` (`0` when
|
|
2130
|
-
* none), an absolute instant. A resumed pool passes the ORIGINAL instant.
|
|
2131
|
-
*/
|
|
2132
|
-
function createBudgetPool(root, runStartedAtMs, restore = {}) {
|
|
2133
|
-
assertValidBudget(root, "root budget");
|
|
2134
|
-
if (!Number.isFinite(runStartedAtMs) || runStartedAtMs < 0) throw new Error("budget runStartedAtMs must be a non-negative finite number");
|
|
2135
|
-
const ledger = openBudgetLedger(root, runStartedAtMs);
|
|
2136
|
-
let nextTicketId = 0;
|
|
2137
|
-
const open = /* @__PURE__ */ new Map();
|
|
2138
|
-
/** Tickets admitted to wait for budget, in the order they asked. Each is also in `open`. */
|
|
2139
|
-
const waiting = [];
|
|
2140
|
-
/** Open tickets that hold nothing: waiting, or refused and not yet reconciled. */
|
|
2141
|
-
const holdingNothing = /* @__PURE__ */ new Set();
|
|
2142
|
-
/** Grant waiting tickets in the order they asked while the head fits. When the head does not
|
|
2143
|
-
* fit and no reservation is held that could return budget, nothing can ever cover it: refuse
|
|
2144
|
-
* it, and go on with the next. */
|
|
2145
|
-
function grantWaiting() {
|
|
2146
|
-
while (waiting.length > 0) {
|
|
2147
|
-
const head = waiting[0];
|
|
2148
|
-
const shortfalls = ledgerShortfalls(ledger, head.budget, head.keep);
|
|
2149
|
-
if (shortfalls.length === 0) {
|
|
2150
|
-
waiting.shift();
|
|
2151
|
-
ledgerTake(ledger, head.budget);
|
|
2152
|
-
holdingNothing.delete(head.ticket.id);
|
|
2153
|
-
head.grant();
|
|
2154
|
-
continue;
|
|
2155
|
-
}
|
|
2156
|
-
if (open.size > holdingNothing.size) return;
|
|
2157
|
-
waiting.shift();
|
|
2158
|
-
head.refuse(new ReservationWaitRefused(shortfalls));
|
|
2159
|
-
}
|
|
2160
|
-
}
|
|
2161
|
-
/** Admit `b` to wait: it holds nothing until settlements return enough, in the order asked. */
|
|
2162
|
-
function admitToWait(b, holder, keep) {
|
|
2163
|
-
const id = nextTicketId++;
|
|
2164
|
-
open.set(id, holder);
|
|
2165
|
-
const ticket = ticketFor(id, b);
|
|
2166
|
-
let grant;
|
|
2167
|
-
let refuse;
|
|
2168
|
-
const granted = new Promise((resolve, reject) => {
|
|
2169
|
-
grant = resolve;
|
|
2170
|
-
refuse = reject;
|
|
2171
|
-
});
|
|
2172
|
-
granted.catch(() => void 0);
|
|
2173
|
-
holdingNothing.add(id);
|
|
2174
|
-
waiting.push({
|
|
2175
|
-
ticket,
|
|
2176
|
-
budget: b,
|
|
2177
|
-
keep,
|
|
2178
|
-
grant,
|
|
2179
|
-
refuse
|
|
2180
|
-
});
|
|
2181
|
-
return {
|
|
2182
|
-
ok: true,
|
|
2183
|
-
ticket,
|
|
2184
|
-
granted
|
|
2185
|
-
};
|
|
2186
|
-
}
|
|
2187
|
-
function reserve(b, holder = { stage: "admitted" }, options = {}) {
|
|
2188
|
-
assertValidBudget(b, "reservation budget");
|
|
2189
|
-
assertResourceDeclarations(ledger, b);
|
|
2190
|
-
const wantUsd = b.maxUsd ?? 0;
|
|
2191
|
-
const keep = options.keep;
|
|
2192
|
-
const shortfalls = ledgerShortfalls(ledger, b, keep);
|
|
2193
|
-
if (shortfalls.length === 0 && wantUsd > 0 && !ledger.usdCapped) return {
|
|
2194
|
-
ok: false,
|
|
2195
|
-
reason: "usd-unbudgeted"
|
|
2196
|
-
};
|
|
2197
|
-
const queueAhead = options.wait === true && waiting.length > 0;
|
|
2198
|
-
if (shortfalls.length === 0 && !queueAhead) {
|
|
2199
|
-
ledgerTake(ledger, b);
|
|
2200
|
-
const id = nextTicketId++;
|
|
2201
|
-
open.set(id, holder);
|
|
2202
|
-
return {
|
|
2203
|
-
ok: true,
|
|
2204
|
-
ticket: ticketFor(id, b)
|
|
2205
|
-
};
|
|
2206
|
-
}
|
|
2207
|
-
if (options.wait !== true || !fitsAfterRefunds(shortfalls)) return {
|
|
2208
|
-
ok: false,
|
|
2209
|
-
reason: "budget-exhausted",
|
|
2210
|
-
shortfalls
|
|
2211
|
-
};
|
|
2212
|
-
return admitToWait(b, holder, keep);
|
|
2213
|
-
}
|
|
2214
|
-
function attribute(ticket, holder) {
|
|
2215
|
-
const current = open.get(ticket.id);
|
|
2216
|
-
if (current === void 0) return;
|
|
2217
|
-
open.set(ticket.id, {
|
|
2218
|
-
...current,
|
|
2219
|
-
...holder
|
|
2220
|
-
});
|
|
2221
|
-
}
|
|
2222
|
-
function reconcile(ticket, spent) {
|
|
2223
|
-
if (!open.has(ticket.id)) throw new Error(`budget pool: reconcile of unknown or already-settled ticket ${ticket.id}`);
|
|
2224
|
-
assertValidSpend(spent, `budget pool ticket ${ticket.id} spend`);
|
|
2225
|
-
ledgerValidateUnits(ledger, spent);
|
|
2226
|
-
const heldNothing = holdingNothing.delete(ticket.id);
|
|
2227
|
-
if (heldNothing) {
|
|
2228
|
-
const index = waiting.findIndex((entry) => entry.ticket.id === ticket.id);
|
|
2229
|
-
if (index >= 0) waiting.splice(index, 1);
|
|
2230
|
-
}
|
|
2231
|
-
const held = heldNothing ? {
|
|
2232
|
-
tokens: 0,
|
|
2233
|
-
usd: 0,
|
|
2234
|
-
iterations: 0,
|
|
2235
|
-
resources: void 0
|
|
2236
|
-
} : ticket.reserved;
|
|
2237
|
-
const usdBudgeted = ticket.reserved.usdBudgeted !== false;
|
|
2238
|
-
const { overspent, unknownUnderCap } = ledgerClassifyOverspend(ledger, held, usdBudgeted, spent);
|
|
2239
|
-
let fault = unknownUnderCap ? `ticket ${ticket.id} reported unknown dollar cost under a dollar-capped budget` : void 0;
|
|
2240
|
-
open.delete(ticket.id);
|
|
2241
|
-
const resourceSettlement = ledgerSettle(ledger, held, spent, unknownUnderCap);
|
|
2242
|
-
fault ??= resourceSettlement.fault;
|
|
2243
|
-
overspent.push(...resourceSettlement.overspent);
|
|
2244
|
-
grantWaiting();
|
|
2245
|
-
const violation = overspent.length === 0 ? void 0 : Object.freeze({ overspent: Object.freeze(overspent.map((entry) => Object.freeze(entry))) });
|
|
2246
|
-
if (fault !== void 0) throw new BudgetReconcileFault(`budget pool: ${fault}`, violation);
|
|
2247
|
-
return violation;
|
|
2248
|
-
}
|
|
2249
|
-
function observe(spend, options = {}) {
|
|
2250
|
-
assertValidSpend(spend, "observed spend");
|
|
2251
|
-
ledgerValidateUnits(ledger, spend);
|
|
2252
|
-
const unknownUnderCap = ledger.usdCapped && spend.usdKnown === false;
|
|
2253
|
-
if (unknownUnderCap && options.alreadySpent !== true) throw new ValidationError$1("budget pool: cannot observe unknown dollar cost under a dollar-capped budget");
|
|
2254
|
-
const settlement = ledgerObserve(ledger, spend, unknownUnderCap, !options.partial);
|
|
2255
|
-
const violation = settlement.fault ?? settlement.rootExceeded;
|
|
2256
|
-
if (violation) throw new ValidationError$1(`budget pool: ${violation}`);
|
|
2257
|
-
}
|
|
2258
|
-
function readout() {
|
|
2259
|
-
return {
|
|
2260
|
-
...ledger.resources.size ? { resources: Object.fromEntries([...ledger.resources].map(([name, value]) => [name, { ...value }])) } : {},
|
|
2261
|
-
tokensLeft: ledger.freeTokens,
|
|
2262
|
-
tokensKnown: !ledger.tokensTainted,
|
|
2263
|
-
cacheBreakdownKnown: !ledger.cacheBreakdownTainted,
|
|
2264
|
-
usdLeft: ledger.usdCapped ? ledger.usdTainted ? 0 : ledger.freeUsd : 0,
|
|
2265
|
-
usdCapped: ledger.usdCapped,
|
|
2266
|
-
usdKnown: ledger.usdMeasured && !ledger.usdTainted,
|
|
2267
|
-
iterationsLeft: ledger.freeIterations,
|
|
2268
|
-
deadlineMs: ledger.absoluteDeadlineMs,
|
|
2269
|
-
reservedTokens: ledger.reservedTokens
|
|
2270
|
-
};
|
|
2271
|
-
}
|
|
2272
|
-
function openReservations() {
|
|
2273
|
-
return [...open].map(([ticketId, holder]) => Object.freeze({
|
|
2274
|
-
ticketId,
|
|
2275
|
-
...holder
|
|
2276
|
-
}));
|
|
2277
|
-
}
|
|
2278
|
-
function assertNoOpenTickets() {
|
|
2279
|
-
if (open.size > 0) throw new Error(`budget pool: ${open.size} reservation(s) still open at join barrier (${openReservations().map(describeLeakedReservation).join("; ")}) — conserved-pool invariant violated`);
|
|
2280
|
-
}
|
|
2281
|
-
ledgerRestore(ledger, root, restore);
|
|
2282
|
-
return {
|
|
2283
|
-
reserve,
|
|
2284
|
-
attribute,
|
|
2285
|
-
reconcile,
|
|
2286
|
-
spendFrom: foldUsage,
|
|
2287
|
-
readout,
|
|
2288
|
-
observe,
|
|
2289
|
-
assertNoOpenTickets,
|
|
2290
|
-
openReservations
|
|
2291
|
-
};
|
|
2292
|
-
}
|
|
2293
|
-
/** A reservation must name a limit for exactly the resources the root declared, in its units. */
|
|
2294
|
-
function assertResourceDeclarations(ledger, b) {
|
|
2295
|
-
for (const [name, state] of ledger.resources) {
|
|
2296
|
-
const wanted = b.resources?.[name];
|
|
2297
|
-
if (!wanted) throw new ValidationError$1(`resource ${name}: child must declare its limit`);
|
|
2298
|
-
if (wanted.unit !== state.unit) throw new ValidationError$1(`resource ${name}: unit mismatch`);
|
|
2299
|
-
}
|
|
2300
|
-
for (const name of Object.keys(b.resources ?? {})) if (!ledger.resources.has(name)) throw new ValidationError$1(`resource ${name}: root must declare its limit`);
|
|
2301
|
-
}
|
|
2302
|
-
/** Whether every short channel would fit once the open reservations return what they hold. */
|
|
2303
|
-
function fitsAfterRefunds(shortfalls) {
|
|
2304
|
-
return shortfalls.every((shortfall) => shortfall.closedByUnknownSpend !== true && shortfall.requested <= shortfall.free + (shortfall.held ?? 0));
|
|
2305
|
-
}
|
|
2306
|
-
function ticketFor(id, b) {
|
|
2307
|
-
return {
|
|
2308
|
-
id,
|
|
2309
|
-
reserved: {
|
|
2310
|
-
...b.resources === void 0 ? {} : { resources: Object.fromEntries(Object.entries(b.resources).map(([name, value]) => [name, { ...value }])) },
|
|
2311
|
-
tokens: b.maxTokens,
|
|
2312
|
-
usd: b.maxUsd ?? 0,
|
|
2313
|
-
iterations: b.maxIterations,
|
|
2314
|
-
usdBudgeted: b.maxUsd !== void 0
|
|
2315
|
-
}
|
|
2316
|
-
};
|
|
2317
|
-
}
|
|
2318
|
-
/** Name one leaked reservation by its holder. */
|
|
2319
|
-
function describeLeakedReservation(leak) {
|
|
2320
|
-
const parts = [`ticket ${leak.ticketId}`];
|
|
2321
|
-
if (leak.childId !== void 0) parts.push(`child ${leak.childId}`);
|
|
2322
|
-
if (leak.label !== void 0) parts.push(`label ${leak.label}`);
|
|
2323
|
-
if (leak.assignment !== void 0) parts.push(`assignment ${leak.assignment}`);
|
|
2324
|
-
parts.push(`stage ${leak.stage}`);
|
|
2325
|
-
return parts.join(", ");
|
|
2326
|
-
}
|
|
2327
|
-
// src/runtime/supervise/error-message.ts
|
|
2328
|
-
const maxCauseDepth = 4;
|
|
2329
|
-
const maxMessageLength = 32768;
|
|
2330
|
-
const maxPartLength = 2048;
|
|
2331
|
-
/** A persisted failure must explain its wrappers without exporting credentials or unbounded data. */
|
|
2332
|
-
function errorText(value) {
|
|
2333
|
-
let text;
|
|
2334
|
-
try {
|
|
2335
|
-
text = typeof value === "string" ? value : JSON.stringify(value) ?? String(value);
|
|
2336
|
-
} catch {
|
|
2337
|
-
text = "[unprintable rejection]";
|
|
2338
|
-
}
|
|
2339
|
-
return String(defaultRedactor(text)).slice(0, maxMessageLength);
|
|
2340
|
-
}
|
|
2341
|
-
function errorProperty(error, field) {
|
|
2342
|
-
try {
|
|
2343
|
-
const value = error[field];
|
|
2344
|
-
return value === void 0 ? void 0 : errorText(value).slice(0, field === "name" ? 256 : maxMessageLength);
|
|
2345
|
-
} catch {
|
|
2346
|
-
return `[unreadable error ${field}]`;
|
|
2347
|
-
}
|
|
2348
|
-
}
|
|
2349
|
-
/** An HTTP status carried on a thrown error, read without trusting its getter. Provider SDKs put
|
|
2350
|
-
* one on their own error classes, so this is how a refusal is recognised outside the transport
|
|
2351
|
-
* taxonomy — for the persisted message here, and for the driver's retry verdict. */
|
|
2352
|
-
function errorHttpStatus(error) {
|
|
2353
|
-
try {
|
|
2354
|
-
const status = Reflect.get(error, "status");
|
|
2355
|
-
return typeof status === "number" && Number.isInteger(status) && status >= 100 && status <= 599 ? status : void 0;
|
|
2356
|
-
} catch {
|
|
2357
|
-
return;
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
/** Shared by child settlements, driver attempts, and the final no-winner result. */
|
|
2361
|
-
function errMessage(error) {
|
|
2362
|
-
const parts = [];
|
|
2363
|
-
const seen = /* @__PURE__ */ new Set();
|
|
2364
|
-
let current = error;
|
|
2365
|
-
for (let depth = 0; depth <= maxCauseDepth; depth += 1) try {
|
|
2366
|
-
if (!(current instanceof Error)) {
|
|
2367
|
-
parts.push(errorText(current).slice(0, maxPartLength));
|
|
2368
|
-
break;
|
|
2369
|
-
}
|
|
2370
|
-
if (seen.has(current)) {
|
|
2371
|
-
parts.push("[circular]");
|
|
2372
|
-
break;
|
|
2373
|
-
}
|
|
2374
|
-
seen.add(current);
|
|
2375
|
-
const status = errorHttpStatus(current);
|
|
2376
|
-
const message = (status === void 0 ? "" : `HTTP ${status}: `) + (errorProperty(current, "message") ?? "");
|
|
2377
|
-
parts.push((depth === 0 ? message : `${errorProperty(current, "name")}: ${message}`).slice(0, maxPartLength));
|
|
2378
|
-
current = current.cause;
|
|
2379
|
-
if (current === void 0 || current === null) break;
|
|
2380
|
-
if (depth === maxCauseDepth) parts.push("[cause chain truncated]");
|
|
2381
|
-
} catch {
|
|
2382
|
-
parts.push("[unreadable error cause]");
|
|
2383
|
-
break;
|
|
2384
|
-
}
|
|
2385
|
-
return parts.join(": caused by ").slice(0, maxMessageLength);
|
|
2386
|
-
}
|
|
2387
|
-
/** The error and its causes (`cause`, and `errors` members), bounded and cycle-safe. */
|
|
2388
|
-
function causeChain(error) {
|
|
2389
|
-
const chain = [];
|
|
2390
|
-
const queue = [error];
|
|
2391
|
-
while (queue.length > 0 && chain.length < 12) {
|
|
2392
|
-
const value = queue.shift();
|
|
2393
|
-
if (typeof value !== "object" || value === null || chain.includes(value)) continue;
|
|
2394
|
-
chain.push(value);
|
|
2395
|
-
try {
|
|
2396
|
-
const errors = Reflect.get(value, "errors");
|
|
2397
|
-
if (Array.isArray(errors)) queue.push(...errors);
|
|
2398
|
-
queue.push(Reflect.get(value, "cause"));
|
|
2399
|
-
} catch {}
|
|
2400
|
-
}
|
|
2401
|
-
return chain;
|
|
2402
|
-
}
|
|
2403
|
-
// src/runtime/supervise/credential-refusal.ts
|
|
2404
|
-
/**
|
|
2405
|
-
* The account owner refused an unsupported credential route before dispatch. This is the
|
|
2406
|
-
* provider SDK's machine contract, not an HTTP status, class-name guess, or prose match.
|
|
2407
|
-
*/
|
|
2408
|
-
function isCredentialRefusal(error) {
|
|
2409
|
-
if (!(error instanceof Error)) return false;
|
|
2410
|
-
try {
|
|
2411
|
-
return Object.getOwnPropertyDescriptor(error, "code")?.value === "model_credential_refused";
|
|
2412
|
-
} catch {
|
|
2413
|
-
return false;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
/**
|
|
2417
|
-
* The message prefix of agent-provider-tangle's generic credential command failure. The provider
|
|
2418
|
-
* exports no type or constant for it, so the kernel credential test raises it through the
|
|
2419
|
-
* provider's own command resolver and fails when the message drifts.
|
|
2420
|
-
*/
|
|
2421
|
-
const CREDENTIAL_COMMAND_FAILURE = "Tangle credential command failed (";
|
|
2422
|
-
/**
|
|
2423
|
-
* The exit code of agent-provider-tangle's credential command (`acct sandbox resolve`) in the
|
|
2424
|
-
* error or its causes, or undefined.
|
|
2425
|
-
*
|
|
2426
|
-
* The provider reports the command's generic failure as `Tangle credential command failed
|
|
2427
|
-
* (exit N)` with the process's `exitCode` and `terminationSignal` as own fields; a refusal it can
|
|
2428
|
-
* parse (exit 75 or 78) arrives typed instead and is classified on its code. Before any dispatch
|
|
2429
|
-
* this failure ran nothing, and counting it as an unresolved pending invocation stopped 10 nodes
|
|
2430
|
-
* and lost 7 runs between 2026-10-12 and 2026-10-19 (test-acceptance-canary-20261015c met 26 in
|
|
2431
|
-
* 2.2 minutes). It pauses like a capacity refusal, bounded by the deadline, the budget and
|
|
2432
|
-
* cancellation as every pause is.
|
|
2433
|
-
*/
|
|
2434
|
-
function credentialCommandExit(error) {
|
|
2435
|
-
for (const link of causeChain(error)) try {
|
|
2436
|
-
if (!(link instanceof Error) || !link.message.startsWith(CREDENTIAL_COMMAND_FAILURE) || !Object.hasOwn(link, "terminationSignal")) continue;
|
|
2437
|
-
const exitCode = Reflect.get(link, "exitCode");
|
|
2438
|
-
if (typeof exitCode === "number" && Number.isInteger(exitCode) && exitCode > 0) return exitCode;
|
|
2439
|
-
} catch {}
|
|
2440
|
-
}
|
|
2441
|
-
// src/runtime/json-content-hash.ts
|
|
2442
|
-
/**
|
|
2443
|
-
* Stable hash for the trace payload. Not cryptographic — only used so
|
|
2444
|
-
* downstream eval pipelines can group iterations whose task / output is the
|
|
2445
|
-
* same. Bare structural hash; non-JSON values stringify via their `toString`.
|
|
2446
|
-
*/
|
|
2447
|
-
function hashJson(value) {
|
|
2448
|
-
const str = jsonContentText(value);
|
|
2449
|
-
return fnv1a32Hex(str);
|
|
2450
|
-
}
|
|
2451
|
-
/** JSON text with String fallback, including quoted JSON strings and cyclic values. */
|
|
2452
|
-
function jsonContentText(value) {
|
|
2453
|
-
let str;
|
|
2454
|
-
try {
|
|
2455
|
-
str = JSON.stringify(value) ?? String(value);
|
|
2456
|
-
} catch {
|
|
2457
|
-
str = String(value);
|
|
2458
|
-
}
|
|
2459
|
-
return str;
|
|
2460
|
-
}
|
|
2461
|
-
// src/runtime/supervise/executor-outcome.ts
|
|
2462
|
-
const outcomeSchema = AgentTurnResultSchema.pick({
|
|
2463
|
-
success: true,
|
|
2464
|
-
error: true
|
|
2465
|
-
}).extend({
|
|
2466
|
-
errorCode: AgentTurnResultSchema.shape.error,
|
|
2467
|
-
statusCode: z.number().int().min(400).max(599).optional()
|
|
2468
|
-
}).strict();
|
|
2469
|
-
/** Read only the executor envelope; application output and grades are not execution status. */
|
|
2470
|
-
function executorFailure(result) {
|
|
2471
|
-
if (result.outcome === void 0) return void 0;
|
|
2472
|
-
const outcome = outcomeSchema.parse(result.outcome);
|
|
2473
|
-
if (outcome.success) return void 0;
|
|
2474
|
-
return {
|
|
2475
|
-
error: outcome.error ?? "Agent execution failed",
|
|
2476
|
-
...outcome.errorCode === void 0 ? {} : { errorCode: outcome.errorCode },
|
|
2477
|
-
...outcome.statusCode === void 0 ? {} : { statusCode: outcome.statusCode }
|
|
2478
|
-
};
|
|
2479
|
-
}
|
|
2480
|
-
/** The failure text alone, for callers that record a settlement reason. */
|
|
2481
|
-
function executorFailureReason(result) {
|
|
2482
|
-
return executorFailure(result)?.error;
|
|
2483
|
-
}
|
|
2484
|
-
/**
|
|
2485
|
-
* The platform's own failure vocabulary: machine codes and lifecycle messages that the sandbox
|
|
2486
|
-
* runtime, the router, or the egress path put in an executor envelope when the platform, not
|
|
2487
|
-
* the agent's work, ended the execution. Each entry names its evidence: the count is how many
|
|
2488
|
-
* down children carried it on the 2026-09-20 fleet corpus (277 down settlements across 691
|
|
2489
|
-
* runs), every one of them stamped `infra: false` by the hard-coded call site this table
|
|
2490
|
-
* replaced. The table is deliberately the platform's fixed strings, not the agent's prose.
|
|
2491
|
-
*/
|
|
2492
|
-
const PLATFORM_FAILURE_CODES = new Set([
|
|
2493
|
-
"QUOTA_EXCEEDED",
|
|
2494
|
-
"provider_quota_exceeded",
|
|
2495
|
-
"provider_quota_exhausted",
|
|
2496
|
-
"buffer.overflow",
|
|
2497
|
-
"TIMEOUT",
|
|
2498
|
-
"ECONNRESET",
|
|
2499
|
-
"ECONNREFUSED",
|
|
2500
|
-
"ETIMEDOUT",
|
|
2501
|
-
"EPIPE",
|
|
2502
|
-
"EADDRINUSE"
|
|
2503
|
-
].map((code) => code.toLowerCase()));
|
|
2504
|
-
const PLATFORM_FAILURE_MESSAGES = [
|
|
2505
|
-
/^terminated$/u,
|
|
2506
|
-
/Execution exceeded its time limit/u,
|
|
2507
|
-
/Execution interrupted: the agent runtime restarted/u,
|
|
2508
|
-
/Platform key[- ]verification/u,
|
|
2509
|
-
/Platform key delegation failed/u,
|
|
2510
|
-
/Bad Gateway/iu,
|
|
2511
|
-
/Cannot connect to API/u,
|
|
2512
|
-
/Failed to connect to Sandbox API/u,
|
|
2513
|
-
/fetch failed/u,
|
|
2514
|
-
/A sandbox lifecycle operation is already in progress/u,
|
|
2515
|
-
/exceeded its string bound/u,
|
|
2516
|
-
/exceeds its JSON bound/u,
|
|
2517
|
-
/No provider served model/u,
|
|
2518
|
-
/Invalid API key/u,
|
|
2519
|
-
/provider_quota_exceeded|provider_quota_exhausted|QUOTA_EXCEEDED|rate limit/iu,
|
|
2520
|
-
/is at its box cap/u,
|
|
2521
|
-
/could not materialize the seat credential/u,
|
|
2522
|
-
/OpenCode exited while a model step was still in progress/u,
|
|
2523
|
-
/the agent runtime restarted/u
|
|
2524
|
-
];
|
|
2525
|
-
/**
|
|
2526
|
-
* Whether an executor envelope's failure was the platform's, as far as the envelope can say.
|
|
2527
|
-
*
|
|
2528
|
-
* `true` when the code or the message is in the platform's own vocabulary above. `undefined`
|
|
2529
|
-
* otherwise: the harness reported a failure and nothing in the envelope attributes it, so the
|
|
2530
|
-
* settlement carries no `infra` claim at all rather than a false one. This path never answers
|
|
2531
|
-
* `false`, because an executor envelope cannot prove the agent's work was at fault; the throw
|
|
2532
|
-
* path in `scope.ts` still answers `false` for an ordinary thrown result, where the runtime does
|
|
2533
|
-
* know. Until this function existed both envelope sites stamped `infra: false` unconditionally,
|
|
2534
|
-
* and the flag caught 1 of the 78 platform losses it was checked against on the fleet corpus.
|
|
2535
|
-
*/
|
|
2536
|
-
function executorFailureInfra(failure) {
|
|
2537
|
-
const code = failure.errorCode?.toLowerCase();
|
|
2538
|
-
if (code !== void 0 && PLATFORM_FAILURE_CODES.has(code)) return true;
|
|
2539
|
-
return PLATFORM_FAILURE_MESSAGES.some((pattern) => pattern.test(failure.error)) ? true : void 0;
|
|
2540
|
-
}
|
|
2541
|
-
/** Preserve deterministic result pointers across built-in executors and replay.
|
|
2542
|
-
* FNV-1a is an identity convention here, not a cryptographic integrity check. */
|
|
2543
|
-
function contentRef(prefix, value) {
|
|
2544
|
-
const str = jsonContentText(value);
|
|
2545
|
-
return `${prefix}:${fnv1a32Hex(str)}`;
|
|
2546
|
-
}
|
|
2547
|
-
// src/durable/content-address.ts
|
|
2548
|
-
/** Stable content address shared by result and trace artifacts. */
|
|
2549
|
-
function contentAddress(artifact) {
|
|
2550
|
-
return `sha256:${createHash("sha256").update(stableStringify(artifact), "utf-8").digest("hex")}`;
|
|
2551
|
-
}
|
|
2552
|
-
function stableStringify(value) {
|
|
2553
|
-
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
2554
|
-
if (Array.isArray(value)) return `[${value.map(stableStringify).join(",")}]`;
|
|
2555
|
-
return `{${Object.entries(value).filter(([, v]) => v !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0).map(([k, v]) => `${JSON.stringify(k)}:${stableStringify(v)}`).join(",")}}`;
|
|
2556
|
-
}
|
|
2557
|
-
// src/runtime/supervise/snapshot.ts
|
|
2558
|
-
/** Deeply detach and freeze untrusted data at a runtime decision boundary. The clone prevents the
|
|
2559
|
-
* caller from mutating it later; the freeze prevents downstream code from mutating the snapshot.
|
|
2560
|
-
* Strings are immutable, so the snapshot shares the caller's strings instead of copying them. */
|
|
2561
|
-
function detachedSnapshot(value, context) {
|
|
2562
|
-
let clone;
|
|
2563
|
-
try {
|
|
2564
|
-
clone = structuredClone(value);
|
|
2565
|
-
} catch (error) {
|
|
2566
|
-
throw new ValidationError$1(`${context}: input must be structured-cloneable`, { cause: error });
|
|
2567
|
-
}
|
|
2568
|
-
return deepFreeze(shareSourceStrings(value, clone));
|
|
2569
|
-
}
|
|
2570
|
-
/** Detach a value from its caller and freeze it, without the `ValidationError` framing
|
|
2571
|
-
* {@link detachedSnapshot} adds. For a boundary that has already validated its input. */
|
|
2572
|
-
function detachedFrozen(value) {
|
|
2573
|
-
return deepFreeze(shareSourceStrings(value, structuredClone(value)));
|
|
2574
|
-
}
|
|
2575
|
-
/**
|
|
2576
|
-
* Point each string in a fresh structured clone at the equal string in its source.
|
|
2577
|
-
*
|
|
2578
|
-
* `structuredClone` copies every string, and a copied string buys no isolation: only objects and
|
|
2579
|
-
* arrays can change through a shared reference. A spawned profile passes several boundaries, and
|
|
2580
|
-
* each one kept its own copy: in a reproduction of Discovery's hosted coordinator, a child profile
|
|
2581
|
-
* that mounts a 5.3 MB file packet was held four times per spawned child. A scope keeps every
|
|
2582
|
-
* child it spawned, so the heap grew about 22 MB per spawn, and hosted coordinators died with V8
|
|
2583
|
-
* heap OOM after 37 spawns.
|
|
2584
|
-
*
|
|
2585
|
-
* The walk pairs only own data properties of plain objects and arrays. Accessors, Maps, Sets and
|
|
2586
|
-
* other cloned types keep the clone's values. A string is replaced only by a string equal to it,
|
|
2587
|
-
* so the snapshot holds exactly the clone's value. The snapshot keeps the source's strings alive.
|
|
2588
|
-
*/
|
|
2589
|
-
function shareSourceStrings(source, clone) {
|
|
2590
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2591
|
-
const visit = (from, to) => {
|
|
2592
|
-
if (from === null || to === null || typeof from !== "object" || typeof to !== "object") return;
|
|
2593
|
-
const array = Array.isArray(to);
|
|
2594
|
-
if (array !== Array.isArray(from)) return;
|
|
2595
|
-
if (!array && Object.getPrototypeOf(to) !== Object.prototype) return;
|
|
2596
|
-
if (visited.has(to)) return;
|
|
2597
|
-
visited.add(to);
|
|
2598
|
-
const target = to;
|
|
2599
|
-
for (const key of Object.keys(target)) {
|
|
2600
|
-
const own = Object.getOwnPropertyDescriptor(from, key);
|
|
2601
|
-
if (own === void 0 || !("value" in own)) continue;
|
|
2602
|
-
const cloned = target[key];
|
|
2603
|
-
if (typeof cloned === "string") {
|
|
2604
|
-
if (own.value === cloned) target[key] = own.value;
|
|
2605
|
-
} else visit(own.value, cloned);
|
|
2606
|
-
}
|
|
2607
|
-
};
|
|
2608
|
-
visit(source, clone);
|
|
2609
|
-
return clone;
|
|
2610
|
-
}
|
|
2611
|
-
/**
|
|
2612
|
-
* Replace each string in a freshly parsed JSON value with an equal string already in `pool`, and
|
|
2613
|
-
* add the new ones. One recovery pass parses every interrupted child's profile from its own blob,
|
|
2614
|
-
* so children that carry the same file packet would otherwise hold one copy each. The value is
|
|
2615
|
-
* updated in place, so it must belong to the caller; frozen objects are left as they are.
|
|
2616
|
-
*/
|
|
2617
|
-
function shareParsedStrings(value, pool) {
|
|
2618
|
-
const visit = (node) => {
|
|
2619
|
-
if (node === null || typeof node !== "object" || Object.isFrozen(node)) return;
|
|
2620
|
-
if (!Array.isArray(node) && Object.getPrototypeOf(node) !== Object.prototype) return;
|
|
2621
|
-
const target = node;
|
|
2622
|
-
for (const key of Object.keys(target)) {
|
|
2623
|
-
const item = target[key];
|
|
2624
|
-
if (typeof item !== "string") {
|
|
2625
|
-
visit(item);
|
|
2626
|
-
continue;
|
|
2627
|
-
}
|
|
2628
|
-
const shared = pool.get(item);
|
|
2629
|
-
if (shared === void 0) pool.set(item, item);
|
|
2630
|
-
else target[key] = shared;
|
|
2631
|
-
}
|
|
2632
|
-
};
|
|
2633
|
-
visit(value);
|
|
2634
|
-
return value;
|
|
2635
|
-
}
|
|
2636
|
-
function deepFreeze(value, seen = /* @__PURE__ */ new Set()) {
|
|
2637
|
-
if (value === null || typeof value !== "object" || ArrayBuffer.isView(value) || seen.has(value)) return value;
|
|
2638
|
-
seen.add(value);
|
|
2639
|
-
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
2640
|
-
return Object.freeze(value);
|
|
2641
|
-
}
|
|
2642
|
-
// src/runtime/supervise/trace-root.ts
|
|
2643
|
-
/**
|
|
2644
|
-
* Trace roots: one Merkle root over the events a turn archived, so a single event can be proven
|
|
2645
|
-
* against the hash its journal record carries without reading the rest of the archive.
|
|
2646
|
-
*
|
|
2647
|
-
* The leaves are the archived events in stored order, each hashed with its own random salt, then
|
|
2648
|
-
* one summary leaf that counts the events and the tool calls among them. The artifact keeps every
|
|
2649
|
-
* leaf and salt and is stored under its content address in the run's result blob store, beside
|
|
2650
|
-
* the archive it commits to. A verifier recomputes leaf `i` from `events[i]` of the `outRef` blob
|
|
2651
|
-
* and `salts[i]`, checks it against `leaves[i]`, and checks `merkleRoot(leaves)` against the root
|
|
2652
|
-
* the journal recorded. Leaf identity is `(outRef, index)`: an event's own `id` is a session
|
|
2653
|
-
* buffer offset and is absent on terminal events.
|
|
2654
|
-
*
|
|
2655
|
-
* One event array has one root per node. A result always gets its own. Evidence whose events the
|
|
2656
|
-
* node already rooted gets none: a retained owner's deferred workspace capture stores the turn it
|
|
2657
|
-
* just committed again, with the workspace fields added, under another `outRef`. A second root
|
|
2658
|
-
* over the same events would give one turn two count candidates and two run-root leaves.
|
|
2659
|
-
*
|
|
2660
|
-
* Salts are random, so a trace root can never be recomputed from the archive alone. Recovery and
|
|
2661
|
-
* replay copy the recorded receipt; they never derive a new one.
|
|
2662
|
-
*/
|
|
2663
|
-
/** Schema identifier of a stored {@link TraceRootArtifact}. */
|
|
2664
|
-
const TRACE_ROOT_SCHEMA = "agent-runtime.trace-root.v1";
|
|
2665
|
-
/** Schema identifier of the {@link TraceSummary} carried by a trace root's last leaf. */
|
|
2666
|
-
const TRACE_SUMMARY_SCHEMA = "agent-runtime.trace-summary.v1";
|
|
2667
|
-
/** @internal */
|
|
2668
|
-
function traceRootWriter(blobs, nodeId) {
|
|
2669
|
-
const roots = /* @__PURE__ */ new Map();
|
|
2670
|
-
/** Digests of the event arrays this node holds a stored root for. */
|
|
2671
|
-
const rooted = /* @__PURE__ */ new Set();
|
|
2672
|
-
const persist = async (outRef, out, evidence) => {
|
|
2673
|
-
const events = archivedEvents(out);
|
|
2674
|
-
if (events === void 0) return void 0;
|
|
2675
|
-
if (evidence) {
|
|
2676
|
-
await Promise.allSettled([...roots.values()]);
|
|
2677
|
-
if (rooted.has(await eventsDigest(events))) return void 0;
|
|
2678
|
-
}
|
|
2679
|
-
const stored = await persistTraceRoot(blobs, nodeId, outRef, events);
|
|
2680
|
-
if (stored === void 0) return void 0;
|
|
2681
|
-
rooted.add(stored.digest);
|
|
2682
|
-
return stored.receipt;
|
|
2683
|
-
};
|
|
2684
|
-
const memoized = (outRef, out, evidence) => {
|
|
2685
|
-
const known = roots.get(outRef);
|
|
2686
|
-
if (known !== void 0) return known;
|
|
2687
|
-
const pending = persist(outRef, out, evidence);
|
|
2688
|
-
roots.set(outRef, pending);
|
|
2689
|
-
pending.catch(() => roots.delete(outRef));
|
|
2690
|
-
return pending;
|
|
2691
|
-
};
|
|
2692
|
-
return {
|
|
2693
|
-
write: (outRef, out) => memoized(outRef, out, false),
|
|
2694
|
-
writeEvidence: (outRef, out) => memoized(outRef, out, true),
|
|
2695
|
-
async recorded(outRef) {
|
|
2696
|
-
if (outRef === void 0) return void 0;
|
|
2697
|
-
return roots.get(outRef)?.catch(() => void 0);
|
|
2698
|
-
}
|
|
2699
|
-
};
|
|
2700
|
-
}
|
|
2701
|
-
async function persistTraceRoot(blobs, nodeId, outRef, events) {
|
|
2702
|
-
let built;
|
|
2703
|
-
try {
|
|
2704
|
-
built = await traceRootArtifact(nodeId, outRef, events);
|
|
2705
|
-
} catch (error) {
|
|
2706
|
-
if (error instanceof LedgerCanonicalizationError) return void 0;
|
|
2707
|
-
throw error;
|
|
2708
|
-
}
|
|
2709
|
-
const { artifact, digest } = built;
|
|
2710
|
-
await nextTurn();
|
|
2711
|
-
const ref = contentAddress(artifact);
|
|
2712
|
-
await nextTurn();
|
|
2713
|
-
await blobs.put(ref, artifact);
|
|
2714
|
-
return {
|
|
2715
|
-
receipt: {
|
|
2716
|
-
root: artifact.root,
|
|
2717
|
-
ref,
|
|
2718
|
-
leafCount: artifact.leafCount
|
|
2719
|
-
},
|
|
2720
|
-
digest
|
|
2721
|
-
};
|
|
2722
|
-
}
|
|
2723
|
-
function archivedEvents(out) {
|
|
2724
|
-
if (out === null || typeof out !== "object" || Array.isArray(out)) return void 0;
|
|
2725
|
-
const events = out.events;
|
|
2726
|
-
return Array.isArray(events) ? events : void 0;
|
|
2727
|
-
}
|
|
2728
|
-
/** The longest stretch of hashing between yields to the event loop. Hashing runs about 40 µs per
|
|
2729
|
-
* event, and the largest archived turn holds 65,276 events: unbroken, that blocked the process
|
|
2730
|
-
* for 2.7 s while sibling turns, coordination calls and heartbeats waited. A slice is measured
|
|
2731
|
-
* in time, not events, because one event can be megabytes. */
|
|
2732
|
-
const SLICE_MS = 10;
|
|
2733
|
-
/** Yields to the event loop once a slice has run `SLICE_MS`; `undefined` while it has not. */
|
|
2734
|
-
function slicer() {
|
|
2735
|
-
let started = performance.now();
|
|
2736
|
-
return () => {
|
|
2737
|
-
if (performance.now() - started < SLICE_MS) return void 0;
|
|
2738
|
-
return nextTurn().then(() => {
|
|
2739
|
-
started = performance.now();
|
|
2740
|
-
});
|
|
2741
|
-
};
|
|
2742
|
-
}
|
|
2743
|
-
function nextTurn() {
|
|
2744
|
-
return new Promise((resolve) => setImmediate(resolve));
|
|
2745
|
-
}
|
|
2746
|
-
/** The identity of an event array as stored: the digest of each event's stored JSON, in order.
|
|
2747
|
-
* Two arrays with one digest have the same leaves' content. */
|
|
2748
|
-
async function eventsDigest(events) {
|
|
2749
|
-
const hash = createHash("sha256");
|
|
2750
|
-
const slice = slicer();
|
|
2751
|
-
for (const event of events) {
|
|
2752
|
-
const yielded = slice();
|
|
2753
|
-
if (yielded !== void 0) await yielded;
|
|
2754
|
-
hash.update(storedText(event)).update("\n");
|
|
2755
|
-
}
|
|
2756
|
-
return hash.digest("hex");
|
|
2757
|
-
}
|
|
2758
|
-
async function traceRootArtifact(nodeId, outRef, archived) {
|
|
2759
|
-
const salts = [];
|
|
2760
|
-
const leaves = [];
|
|
2761
|
-
const tree = merkleTree();
|
|
2762
|
-
const identity = createHash("sha256");
|
|
2763
|
-
const tools = createSandboxToolPartState();
|
|
2764
|
-
const toolCalls = /* @__PURE__ */ new Map();
|
|
2765
|
-
const slice = slicer();
|
|
2766
|
-
for (const event of archived) {
|
|
2767
|
-
const yielded = slice();
|
|
2768
|
-
if (yielded !== void 0) await yielded;
|
|
2769
|
-
const text = storedText(event);
|
|
2770
|
-
identity.update(text).update("\n");
|
|
2771
|
-
const stored = JSON.parse(text);
|
|
2772
|
-
const salt = merkleSalt();
|
|
2773
|
-
const leaf = merkleLeafHash(canonicalString(jsonDocument(stored)), salt);
|
|
2774
|
-
salts.push(salt);
|
|
2775
|
-
leaves.push(leaf);
|
|
2776
|
-
tree.push(leaf);
|
|
2777
|
-
for (const projected of mapSandboxToolEvent(stored, tools)) {
|
|
2778
|
-
if (projected.type !== "tool_call") continue;
|
|
2779
|
-
toolCalls.set(projected.toolName, (toolCalls.get(projected.toolName) ?? 0) + 1);
|
|
2780
|
-
}
|
|
2781
|
-
}
|
|
2782
|
-
const summary = {
|
|
2783
|
-
schema: TRACE_SUMMARY_SCHEMA,
|
|
2784
|
-
events: archived.length,
|
|
2785
|
-
toolCalls: Object.fromEntries(toolCalls)
|
|
2786
|
-
};
|
|
2787
|
-
const salt = merkleSalt();
|
|
2788
|
-
const leaf = merkleLeafHash(canonicalString(summary), salt);
|
|
2789
|
-
salts.push(salt);
|
|
2790
|
-
leaves.push(leaf);
|
|
2791
|
-
tree.push(leaf);
|
|
2792
|
-
return {
|
|
2793
|
-
artifact: {
|
|
2794
|
-
schema: TRACE_ROOT_SCHEMA,
|
|
2795
|
-
nodeId,
|
|
2796
|
-
outRef,
|
|
2797
|
-
root: tree.root(),
|
|
2798
|
-
leafCount: leaves.length,
|
|
2799
|
-
salts,
|
|
2800
|
-
leaves,
|
|
2801
|
-
summary
|
|
2802
|
-
},
|
|
2803
|
-
digest: identity.digest("hex")
|
|
2804
|
-
};
|
|
2805
|
-
}
|
|
2806
|
-
/**
|
|
2807
|
-
* `merkleRoot(leaves)` built as the leaves arrive, so its interior hashes run inside the sliced
|
|
2808
|
-
* leaf loop instead of in one block after it (0.4 s for 65,277 leaves). It keeps the perfect
|
|
2809
|
-
* subtrees of RFC 9162, largest first, and joins two with ledger-core's own interior hash,
|
|
2810
|
-
* `merkleRoot([left, right])`; the root folds them from the right, as RFC 9162 splits at the
|
|
2811
|
-
* largest power of two.
|
|
2812
|
-
*/
|
|
2813
|
-
function merkleTree() {
|
|
2814
|
-
const peaks = [];
|
|
2815
|
-
return {
|
|
2816
|
-
push(leaf) {
|
|
2817
|
-
let hash = leaf;
|
|
2818
|
-
let size = 1;
|
|
2819
|
-
while (peaks.at(-1)?.size === size) {
|
|
2820
|
-
hash = merkleRoot([peaks.pop().hash, hash]);
|
|
2821
|
-
size *= 2;
|
|
2822
|
-
}
|
|
2823
|
-
peaks.push({
|
|
2824
|
-
hash,
|
|
2825
|
-
size
|
|
2826
|
-
});
|
|
2827
|
-
},
|
|
2828
|
-
root() {
|
|
2829
|
-
let root = peaks.at(-1).hash;
|
|
2830
|
-
for (let index = peaks.length - 2; index >= 0; index -= 1) root = merkleRoot([peaks[index].hash, root]);
|
|
2831
|
-
return root;
|
|
2832
|
-
}
|
|
2833
|
-
};
|
|
2834
|
-
}
|
|
2835
|
-
/** One array item as `JSON.stringify` writes it: `JSON.parse` of it is what `events[i]` holds. */
|
|
2836
|
-
function storedText(event) {
|
|
2837
|
-
return JSON.stringify(event) ?? "null";
|
|
2838
|
-
}
|
|
2839
|
-
// src/runtime/supervise/upstream-unavailable.ts
|
|
2840
|
-
/**
|
|
2841
|
-
* An upstream that cannot serve NOW: how Runtime recognizes one, and how long an agent waits for it.
|
|
2842
|
-
*
|
|
2843
|
-
* A model provider's exhausted quota, a rate limit, an overload, or the router's own refused
|
|
2844
|
-
* credential is not a fault in the agent: the same request succeeds when capacity returns. A
|
|
2845
|
-
* driver that meets one re-enters after a pause (`runDriverWithRetry`), and a leaf continues in
|
|
2846
|
-
* its environment after the same pause (`providerAsExecutor`). Both read the refusal and the pause
|
|
2847
|
-
* from here, so an outage costs a director and its workers the same number of turns.
|
|
2848
|
-
*
|
|
2849
|
-
* This module imports nothing, so the provider executor can read it without the error classes of
|
|
2850
|
-
* `../../errors`.
|
|
2851
|
-
*/
|
|
2852
|
-
/**
|
|
2853
|
-
* Machine codes that say the upstream cannot serve now rather than that the request is wrong.
|
|
2854
|
-
* Each is a code an upstream publishes, not prose: the router's
|
|
2855
|
-
* (`lib/model-substitution.ts`, `lib/upstream-error-triage.ts`) and the OpenAI- and Anthropic-shaped
|
|
2856
|
-
* error types a relayed body carries.
|
|
2857
|
-
*/
|
|
2858
|
-
const UNAVAILABLE_CODES = /* @__PURE__ */ new Set([
|
|
2859
|
-
"provider_quota_exhausted",
|
|
2860
|
-
"provider_quota_exceeded",
|
|
2861
|
-
"provider_rate_limit",
|
|
2862
|
-
"upstream_unavailable",
|
|
2863
|
-
"provider_key_invalid",
|
|
2864
|
-
"rate_limit_exceeded",
|
|
2865
|
-
"rate_limit_error",
|
|
2866
|
-
"overloaded_error"
|
|
2867
|
-
]);
|
|
2868
|
-
/** 429 Too Many Requests, 503 Service Unavailable, and 529, which Anthropic uses for overload. */
|
|
2869
|
-
const UNAVAILABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
2870
|
-
429,
|
|
2871
|
-
503,
|
|
2872
|
-
529
|
|
2873
|
-
]);
|
|
2874
|
-
/**
|
|
2875
|
-
* A harness that flattens the router's JSON body into its own failure text leaves the router's
|
|
2876
|
-
* code in that text: `opencode execution failed: No provider served model "..."
|
|
2877
|
-
* (provider_quota_exhausted)`. The code is matched as a whole token, and a status only in the
|
|
2878
|
-
* `status code <n>` framing a harness prints, so prose that merely mentions a quota does not match.
|
|
2879
|
-
*/
|
|
2880
|
-
function unavailableSignalInText(text) {
|
|
2881
|
-
for (const token of text.toLowerCase().matchAll(/[a-z_]+/gu)) if (UNAVAILABLE_CODES.has(token[0])) return token[0];
|
|
2882
|
-
const status = text.match(/\bstatus code (\d{3})\b/u)?.[1];
|
|
2883
|
-
return status !== void 0 && UNAVAILABLE_STATUSES.has(Number(status)) ? `http-${status}` : void 0;
|
|
2884
|
-
}
|
|
2885
|
-
/**
|
|
2886
|
-
* The code or status that marks a failed turn outcome as an upstream capacity refusal, or
|
|
2887
|
-
* `undefined`. The outcome's own `errorCode` decides first; the failure text is read only when no
|
|
2888
|
-
* code or numeric status decided, because a harness CLI can report the router's refusal as text.
|
|
2889
|
-
*/
|
|
2890
|
-
function unavailableSignalOfFailure(failure) {
|
|
2891
|
-
const code = failure.errorCode?.toLowerCase();
|
|
2892
|
-
if (code !== void 0 && UNAVAILABLE_CODES.has(code)) return code;
|
|
2893
|
-
if (failure.statusCode !== void 0) return UNAVAILABLE_STATUSES.has(failure.statusCode) ? `http-${failure.statusCode}` : void 0;
|
|
2894
|
-
return unavailableSignalInText(failure.error);
|
|
2895
|
-
}
|
|
2896
|
-
const DEFAULT_UNAVAILABLE_PAUSE_MS = 15e3;
|
|
2897
|
-
const DEFAULT_MAX_UNAVAILABLE_PAUSE_MS = 3e5;
|
|
2898
|
-
/**
|
|
2899
|
-
* The pause after the `consecutive`-th refusal in a row (1-based): the first pause, doubled per
|
|
2900
|
-
* earlier refusal, up to the ceiling. The one rule for a driver that re-enters and a leaf that
|
|
2901
|
-
* continues in its environment, so an outage costs both the same number of turns.
|
|
2902
|
-
*/
|
|
2903
|
-
function unavailablePauseMs(consecutive, policy = {}) {
|
|
2904
|
-
return cappedDoublingMs(consecutive, policy.unavailablePauseMs ?? DEFAULT_UNAVAILABLE_PAUSE_MS, policy.maxUnavailablePauseMs ?? DEFAULT_MAX_UNAVAILABLE_PAUSE_MS);
|
|
2905
|
-
}
|
|
2906
|
-
/** `first` doubled per earlier consecutive wait (`consecutive` is 1-based), up to `ceiling`. */
|
|
2907
|
-
function cappedDoublingMs(consecutive, first, ceiling) {
|
|
2908
|
-
const start = Math.max(0, first);
|
|
2909
|
-
return Math.min(Math.max(start, ceiling), start * 2 ** Math.max(0, consecutive - 1));
|
|
2910
|
-
}
|
|
2911
|
-
/** The longest hold a refusal's own reset time may ask for: a 5-hour plan window plus a margin. */
|
|
2912
|
-
const MAX_RESET_HOLD_MS = 216e5;
|
|
2913
|
-
/** How often a refusal whose reset is beyond {@link MAX_RESET_HOLD_MS} is probed again. */
|
|
2914
|
-
const FAR_RESET_PROBE_MS = 36e5;
|
|
2915
|
-
/**
|
|
2916
|
-
* When a refusal says its quota resets, as epoch ms, or `undefined` when it names no usable time.
|
|
2917
|
-
* The Z.ai Coding Plan's opencode refusal names one in Beijing time with no zone:
|
|
2918
|
-
* `Usage limit reached for 5 hour. Your limit will reset at 2026-10-18 15:17:33`. On Discovery run
|
|
2919
|
-
* magnetar-rust-20261018b, 219 of 324 driver attempts ended on it between 06:04Z and 22:18Z, and
|
|
2920
|
-
* the last refusal came at 22:18Z against `reset at 2026-10-19 06:18:14`, which is 22:18:14Z at
|
|
2921
|
-
* +08:00. A time with its own zone is read as written. A time in the past is no reset time: the
|
|
2922
|
-
* caller keeps its doubling pause. A time more than MAX_RESET_HOLD_MS ahead is held for only
|
|
2923
|
-
* FAR_RESET_PROBE_MS at a time, so a misread zone cannot hold a seat for a day, yet the spent seat
|
|
2924
|
-
* is not asked again every 5 minutes: on magnetar-rust-20261019c a reset beyond 6 h drew about 12
|
|
2925
|
-
* attempts an hour against the refusing seat.
|
|
2926
|
-
*/
|
|
2927
|
-
function unavailableResumeAt(text, nowMs) {
|
|
2928
|
-
const match = text.match(/\breset at (\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}:\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?/u);
|
|
2929
|
-
if (match === null) return void 0;
|
|
2930
|
-
const zone = match[3] ?? (/Usage limit reached for \d+ hour/u.test(text) ? "+08:00" : void 0);
|
|
2931
|
-
if (zone === void 0) return void 0;
|
|
2932
|
-
const at = Date.parse(`${match[1]}T${match[2]}${zone.length === 5 ? `${zone.slice(0, 3)}:${zone.slice(3)}` : zone}`);
|
|
2933
|
-
if (!Number.isFinite(at) || at <= nowMs) return void 0;
|
|
2934
|
-
return at - nowMs > MAX_RESET_HOLD_MS ? nowMs + FAR_RESET_PROBE_MS : at;
|
|
2935
|
-
}
|
|
2936
|
-
/**
|
|
2937
|
-
* Spread a wait over `[ms / 2, ms]`. Every driver a Platform restart cut off fails within the same
|
|
2938
|
-
* seconds; without jitter their doubling schedules stay aligned and they all re-enter together
|
|
2939
|
-
* when the service returns. `random` returns a value in `[0, 1)`; 0 keeps the full wait.
|
|
2940
|
-
*/
|
|
2941
|
-
function jitteredMs(ms, random) {
|
|
2942
|
-
return ms - Math.floor(ms / 2 * Math.min(1, Math.max(0, random())));
|
|
2943
|
-
}
|
|
2944
|
-
// src/runtime/supervise/retained-executor.ts
|
|
2945
|
-
/** Scope owns the durable writer; provider executors only publish sanitized admissions. */
|
|
2946
|
-
const retainedExecutorSeamKey = "runtime.retainedExecutor";
|
|
2947
|
-
/** @internal A capture may finish after cancellation; its durable citation remains appendable. */
|
|
2948
|
-
function executorEvidenceWriter(args) {
|
|
2949
|
-
const traceRoots = args.traceRoots ?? traceRootWriter(args.blobs, args.nodeId);
|
|
2950
|
-
return async (out) => {
|
|
2951
|
-
const snapshot = detachedSnapshot(out, "executor evidence");
|
|
2952
|
-
const outRef = contentAddress(snapshot);
|
|
2953
|
-
await args.blobs.put(outRef, snapshot);
|
|
2954
|
-
const traceRoot = await traceRoots.writeEvidence(outRef, snapshot);
|
|
2955
|
-
await args.journal.appendEvent(args.rootId, {
|
|
2956
|
-
kind: "execution-evidence",
|
|
2957
|
-
id: args.nodeId,
|
|
2958
|
-
outRef,
|
|
2959
|
-
...traceRoot === void 0 ? {} : { traceRoot },
|
|
2960
|
-
seq: args.nextSequence(),
|
|
2961
|
-
at: new Date(args.now()).toISOString()
|
|
2962
|
-
});
|
|
2963
|
-
};
|
|
2964
|
-
}
|
|
2965
|
-
/** @internal The journal writers for `onWorkspaceCapture` and `onWorkspaceCaptureStarted`;
|
|
2966
|
-
* `nextSequence` is the node's own. */
|
|
2967
|
-
function workspaceCaptureWriter(args) {
|
|
2968
|
-
return {
|
|
2969
|
-
onWorkspaceCapture: async (capture) => {
|
|
2970
|
-
await args.journal.appendEvent(args.rootId, {
|
|
2971
|
-
kind: "workspace-capture",
|
|
2972
|
-
id: args.nodeId,
|
|
2973
|
-
...detachedSnapshot(capture, "workspace capture timing"),
|
|
2974
|
-
seq: args.nextSequence(),
|
|
2975
|
-
at: new Date(args.now()).toISOString()
|
|
2976
|
-
});
|
|
2977
|
-
},
|
|
2978
|
-
onWorkspaceCaptureStarted: async (capture) => {
|
|
2979
|
-
await args.journal.appendEvent(args.rootId, {
|
|
2980
|
-
kind: "workspace-capture-started",
|
|
2981
|
-
id: args.nodeId,
|
|
2982
|
-
...detachedSnapshot(capture, "workspace capture start"),
|
|
2983
|
-
seq: args.nextSequence(),
|
|
2984
|
-
at: new Date(args.now()).toISOString()
|
|
2985
|
-
});
|
|
2986
|
-
}
|
|
2987
|
-
};
|
|
2988
|
-
}
|
|
2989
|
-
function retainedExecutorContext(ctx) {
|
|
2990
|
-
return ctx.seams[retainedExecutorSeamKey];
|
|
2991
|
-
}
|
|
2992
|
-
const causeMessages = {
|
|
2993
|
-
unobservable: "retained provider execution requires reconciliation before replacement",
|
|
2994
|
-
"provider-contract": "retained provider response violated its contract; original execution remains unresolved",
|
|
2995
|
-
"request-rejected": "retained provider execution was refused before it ran; nothing to reconcile",
|
|
2996
|
-
transport: "retained provider execution lost its transport; status in doubt",
|
|
2997
|
-
"nested-recovery": "retained nested execution requires recovery before replacement"
|
|
2998
|
-
};
|
|
2999
|
-
/**
|
|
3000
|
-
* agent-provider-tangle's `TangleDispatchNotAdmittedError` (since 3.6.11): Sandbox answered a
|
|
3001
|
-
* dispatch with `dispatched: false` and named another execution that owns the session or turn.
|
|
3002
|
-
* Nothing ran for the request.
|
|
3003
|
-
*/
|
|
3004
|
-
const DISPATCH_NOT_ADMITTED_CODE = "DISPATCH_NOT_ADMITTED";
|
|
3005
|
-
/**
|
|
3006
|
-
* The Sandbox SDK's codes for a box that cannot be provisioned for this profile, sent with HTTP
|
|
3007
|
-
* 422 when the failure is deterministic: a profile file mount outside the box's writable roots,
|
|
3008
|
-
* refused before any box exists, and a profile-file materialization that would fail the same way
|
|
3009
|
-
* again. On Discovery run queens21-h22-capture-root-1-20261019c every child profile mounted
|
|
3010
|
-
* `/work/queens21_count.c` on a read-only root filesystem, and 154 attempts across 4 workers were
|
|
3011
|
-
* retried as a lost transport. The same codes at another status may be transient and are not
|
|
3012
|
-
* read here.
|
|
3013
|
-
*/
|
|
3014
|
-
const PROVISIONING_REFUSAL_CODES = /* @__PURE__ */ new Set(["PROFILE_FILE_MOUNT_OUTSIDE_ROOTS", "PROFILE_FILES_MATERIALIZE_FAILED"]);
|
|
3015
|
-
/** True for an error that is itself a deterministic provisioning refusal (see above). */
|
|
3016
|
-
function isProvisioningRefusal(value) {
|
|
3017
|
-
try {
|
|
3018
|
-
const code = readString(value, "code");
|
|
3019
|
-
const status = readNumber(value, "status") ?? readNumber(value, "statusCode");
|
|
3020
|
-
return code !== void 0 && status === 422 && PROVISIONING_REFUSAL_CODES.has(code);
|
|
3021
|
-
} catch {
|
|
3022
|
-
return false;
|
|
3023
|
-
}
|
|
3024
|
-
}
|
|
3025
|
-
/** The contract-error codes that wrap a failed READ rather than name a broken answer. */
|
|
3026
|
-
const readFailureCodes = /* @__PURE__ */ new Set(["RETAINED_RESULT_READ_FAILED", "RETAINED_CONTROL_REF_READ_FAILED"]);
|
|
3027
|
-
/** Socket-level failures, by the codes Node and undici assign, plus the Sandbox SDK's own. */
|
|
3028
|
-
const TRANSPORT_ERROR_CODES = /* @__PURE__ */ new Set([
|
|
3029
|
-
"ECONNRESET",
|
|
3030
|
-
"ECONNREFUSED",
|
|
3031
|
-
"ETIMEDOUT",
|
|
3032
|
-
"ENOTFOUND",
|
|
3033
|
-
"EAI_AGAIN",
|
|
3034
|
-
"EPIPE",
|
|
3035
|
-
"UND_ERR_SOCKET",
|
|
3036
|
-
"UND_ERR_CONNECT_TIMEOUT",
|
|
3037
|
-
"UND_ERR_HEADERS_TIMEOUT",
|
|
3038
|
-
"UND_ERR_BODY_TIMEOUT",
|
|
3039
|
-
"NETWORK_ERROR"
|
|
3040
|
-
]);
|
|
3041
|
-
function readNumber(value, key) {
|
|
3042
|
-
const read = Reflect.get(value, key);
|
|
3043
|
-
return typeof read === "number" && Number.isFinite(read) ? read : void 0;
|
|
3044
|
-
}
|
|
3045
|
-
function readString(value, key) {
|
|
3046
|
-
const read = Reflect.get(value, key);
|
|
3047
|
-
return typeof read === "string" ? read : void 0;
|
|
3048
|
-
}
|
|
3049
|
-
const credentialAdmissionRefusals = /* @__PURE__ */ new WeakSet();
|
|
3050
|
-
/**
|
|
3051
|
-
* Walk the cause chain (bounded, cycle-safe) and name the first cause whose structure decides.
|
|
3052
|
-
*
|
|
3053
|
-
* `phase` says whether a 4xx or a client deadline can mean "refused before it ran": only at
|
|
3054
|
-
* admission. After admission the same status means the provider cannot resolve what it
|
|
3055
|
-
* admitted, and the honest name is the refusal.
|
|
3056
|
-
*/
|
|
3057
|
-
function classifyRetainedPendingCause(cause, phase = "execution") {
|
|
3058
|
-
return classifyPendingCause(cause, phase);
|
|
3059
|
-
}
|
|
3060
|
-
function classifyPendingCause(cause, phase, onCredentialRefusal) {
|
|
3061
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3062
|
-
const queue = [{
|
|
3063
|
-
value: cause,
|
|
3064
|
-
phase
|
|
3065
|
-
}];
|
|
3066
|
-
let steps = 0;
|
|
3067
|
-
while (queue.length > 0 && steps < 12) {
|
|
3068
|
-
steps += 1;
|
|
3069
|
-
const { value, phase: at } = queue.shift();
|
|
3070
|
-
if (typeof value !== "object" || value === null || seen.has(value)) continue;
|
|
3071
|
-
seen.add(value);
|
|
3072
|
-
if (value instanceof RetainedExecutionPendingError) {
|
|
3073
|
-
if (credentialAdmissionRefusals.has(value)) {
|
|
3074
|
-
if (at === "execution") return "unobservable";
|
|
3075
|
-
onCredentialRefusal?.();
|
|
3076
|
-
}
|
|
3077
|
-
return value.pendingCause;
|
|
3078
|
-
}
|
|
3079
|
-
const name = readString(value, "name");
|
|
3080
|
-
const code = readString(value, "code");
|
|
3081
|
-
const status = readNumber(value, "status") ?? readNumber(value, "statusCode");
|
|
3082
|
-
if (name === "RetainedRunProviderContractError") {
|
|
3083
|
-
if (code === void 0 || !readFailureCodes.has(code)) return "provider-contract";
|
|
3084
|
-
queue.unshift({
|
|
3085
|
-
value: Reflect.get(value, "cause"),
|
|
3086
|
-
phase: "execution"
|
|
3087
|
-
});
|
|
3088
|
-
continue;
|
|
3089
|
-
}
|
|
3090
|
-
if (name === "ZodError" || Array.isArray(Reflect.get(value, "issues")) || code === "JSON_BOUND_VIOLATION") return at === "admission" ? "request-rejected" : "provider-contract";
|
|
3091
|
-
if (code === "DISPATCH_NOT_ADMITTED") return at === "admission" ? "request-rejected" : "provider-contract";
|
|
3092
|
-
if (isProvisioningRefusal(value)) return "request-rejected";
|
|
3093
|
-
if (isCredentialRefusal(value)) {
|
|
3094
|
-
if (at === "execution") return "unobservable";
|
|
3095
|
-
onCredentialRefusal?.();
|
|
3096
|
-
return "request-rejected";
|
|
3097
|
-
}
|
|
3098
|
-
if (code !== void 0 && TRANSPORT_ERROR_CODES.has(code)) return "transport";
|
|
3099
|
-
if (name === "NetworkError" || name === "ServerError") return "transport";
|
|
3100
|
-
if (status === void 0 && at === "admission" && code !== void 0 && UNAVAILABLE_CODES.has(code.toLowerCase())) return "request-rejected";
|
|
3101
|
-
if (status !== void 0) {
|
|
3102
|
-
if (status >= 500) return "transport";
|
|
3103
|
-
if (at === "admission" && status !== 408) return "request-rejected";
|
|
3104
|
-
return "unobservable";
|
|
3105
|
-
}
|
|
3106
|
-
const errors = Reflect.get(value, "errors");
|
|
3107
|
-
if (Array.isArray(errors)) for (const member of errors) queue.push({
|
|
3108
|
-
value: member,
|
|
3109
|
-
phase: at
|
|
3110
|
-
});
|
|
3111
|
-
queue.push({
|
|
3112
|
-
value: Reflect.get(value, "cause"),
|
|
3113
|
-
phase: at
|
|
3114
|
-
});
|
|
3115
|
-
}
|
|
3116
|
-
return "unobservable";
|
|
3117
|
-
}
|
|
3118
|
-
/**
|
|
3119
|
-
* Local observation stopped; the retained provider execution has no accepted terminal result.
|
|
3120
|
-
*
|
|
3121
|
-
* The message names WHICH of the situations in {@link RetainedPendingCause} this is, and the
|
|
3122
|
-
* `pendingCause` field carries it as a value the settlement records, so a reader never splits
|
|
3123
|
-
* the population on the message's wording. The cause chain is kept for the `caused by` receipt.
|
|
3124
|
-
*/
|
|
3125
|
-
var RetainedExecutionPendingError = class extends Error {
|
|
3126
|
-
pendingCause;
|
|
3127
|
-
phase;
|
|
3128
|
-
constructor(cause, pendingCause = "execution") {
|
|
3129
|
-
let credentialRefused = false;
|
|
3130
|
-
const resolved = pendingCause === "admission" || pendingCause === "execution" ? classifyPendingCause(cause, pendingCause, () => {
|
|
3131
|
-
credentialRefused = true;
|
|
3132
|
-
}) : pendingCause;
|
|
3133
|
-
super(causeMessages[resolved], { cause });
|
|
3134
|
-
this.name = "RetainedExecutionPendingError";
|
|
3135
|
-
this.pendingCause = resolved;
|
|
3136
|
-
this.phase = pendingCause === "admission" ? "admission" : "execution";
|
|
3137
|
-
if (credentialRefused) credentialAdmissionRefusals.add(this);
|
|
3138
|
-
}
|
|
3139
|
-
};
|
|
3140
|
-
/** The instruction a keyed continuation runs in the held session: the node's task is unchanged,
|
|
3141
|
-
* and its conversation and workspace are where the budget stop left them. */
|
|
3142
|
-
const BUDGET_CONTINUATION_TASK = "Your previous turn reached its budget and stopped. Your manager gave you more budget to continue the same task in the same session: your conversation so far and the files in your workspace are where you left them. Continue from where you stopped. Do not start over.";
|
|
3143
|
-
/** The execution id of a leaf's later invocation, derived from its `execution-input` sequence so
|
|
3144
|
-
* a recovery derives the same one. A first invocation keeps the node id. */
|
|
3145
|
-
function leafContinuationExecutionId(nodeId, inputSeq) {
|
|
3146
|
-
return `${nodeId}:input:${inputSeq}`;
|
|
3147
|
-
}
|
|
3148
|
-
const retainedPreparations = /* @__PURE__ */ new WeakMap();
|
|
3149
|
-
/** Reconstruct an admitted worker through its original composition boundary. @internal */
|
|
3150
|
-
function registerRetainedExecutorPreparation(factory, prepare) {
|
|
3151
|
-
retainedPreparations.set(factory, prepare);
|
|
3152
|
-
return factory;
|
|
3153
|
-
}
|
|
3154
|
-
function prepareRetainedExecutor(factory, input) {
|
|
3155
|
-
return retainedPreparations.get(factory)?.(input);
|
|
3156
|
-
}
|
|
3157
|
-
// src/runtime/failure-cause.ts
|
|
3158
|
-
const MAX_CAUSE_CHARS = 2048;
|
|
3159
|
-
const MAX_CHAIN_STEPS = 12;
|
|
3160
|
-
const MACHINE_CODE_PATTERN = /^[A-Za-z][A-Za-z0-9_.-]{0,63}$/u;
|
|
3161
|
-
/** @internal Validate Runtime-owned failure values on observation transports. */
|
|
3162
|
-
const runtimeFailureCauseSchema = z.object({
|
|
3163
|
-
code: z.enum([
|
|
3164
|
-
"aborted",
|
|
3165
|
-
"deadline-exceeded",
|
|
3166
|
-
"budget-exhausted",
|
|
3167
|
-
"credential-refused",
|
|
3168
|
-
"request-rejected",
|
|
3169
|
-
"upstream-unavailable",
|
|
3170
|
-
"transport",
|
|
3171
|
-
"provider-contract",
|
|
3172
|
-
"retained-unobservable",
|
|
3173
|
-
"nested-recovery",
|
|
3174
|
-
"platform",
|
|
3175
|
-
"work-failed",
|
|
3176
|
-
"unknown"
|
|
3177
|
-
]),
|
|
3178
|
-
owner: z.enum([
|
|
3179
|
-
"provider",
|
|
3180
|
-
"platform",
|
|
3181
|
-
"runtime",
|
|
3182
|
-
"work",
|
|
3183
|
-
"caller",
|
|
3184
|
-
"unknown"
|
|
3185
|
-
]),
|
|
3186
|
-
cause: z.string().max(MAX_CAUSE_CHARS),
|
|
3187
|
-
/** The upstream or provider machine code the failure carried, when it carried one. */
|
|
3188
|
-
providerCode: z.string().regex(MACHINE_CODE_PATTERN).optional()
|
|
3189
|
-
}).readonly();
|
|
3190
|
-
const retainedFailures = {
|
|
3191
|
-
unobservable: {
|
|
3192
|
-
code: "retained-unobservable",
|
|
3193
|
-
owner: "provider"
|
|
3194
|
-
},
|
|
3195
|
-
"provider-contract": {
|
|
3196
|
-
code: "provider-contract",
|
|
3197
|
-
owner: "provider"
|
|
3198
|
-
},
|
|
3199
|
-
"request-rejected": {
|
|
3200
|
-
code: "request-rejected",
|
|
3201
|
-
owner: "caller"
|
|
3202
|
-
},
|
|
3203
|
-
transport: {
|
|
3204
|
-
code: "transport",
|
|
3205
|
-
owner: "platform"
|
|
3206
|
-
},
|
|
3207
|
-
"nested-recovery": {
|
|
3208
|
-
code: "nested-recovery",
|
|
3209
|
-
owner: "runtime"
|
|
3210
|
-
}
|
|
3211
|
-
};
|
|
3212
|
-
function boundedCause(text) {
|
|
3213
|
-
return String(defaultRedactor(text)).slice(0, MAX_CAUSE_CHARS);
|
|
3214
|
-
}
|
|
3215
|
-
function read(value, key) {
|
|
3216
|
-
try {
|
|
3217
|
-
return Reflect.get(value, key);
|
|
3218
|
-
} catch {
|
|
3219
|
-
return;
|
|
3220
|
-
}
|
|
3221
|
-
}
|
|
3222
|
-
/** Every object in a failure's cause chain and aggregate members, bounded and cycle-safe. */
|
|
3223
|
-
function chain(error) {
|
|
3224
|
-
const out = [];
|
|
3225
|
-
const seen = /* @__PURE__ */ new Set();
|
|
3226
|
-
const queue = [error];
|
|
3227
|
-
while (queue.length > 0 && out.length < MAX_CHAIN_STEPS) {
|
|
3228
|
-
const value = queue.shift();
|
|
3229
|
-
if (typeof value !== "object" || value === null || seen.has(value)) continue;
|
|
3230
|
-
seen.add(value);
|
|
3231
|
-
out.push(value);
|
|
3232
|
-
const members = read(value, "errors");
|
|
3233
|
-
if (Array.isArray(members)) queue.push(...members);
|
|
3234
|
-
queue.push(read(value, "cause"));
|
|
3235
|
-
}
|
|
3236
|
-
return out;
|
|
3237
|
-
}
|
|
3238
|
-
const UNATTRIBUTED = {
|
|
3239
|
-
code: "unknown",
|
|
3240
|
-
owner: "unknown"
|
|
3241
|
-
};
|
|
3242
|
-
/** One link's own structure: budget refusal, credential refusal, machine code, HTTP status. */
|
|
3243
|
-
function structuralFailure(link) {
|
|
3244
|
-
if (link instanceof ReservationWaitRefused || read(link, "reason") === "budget-exhausted") return {
|
|
3245
|
-
code: "budget-exhausted",
|
|
3246
|
-
owner: "caller"
|
|
3247
|
-
};
|
|
3248
|
-
if (isCredentialRefusal(link)) return {
|
|
3249
|
-
code: "credential-refused",
|
|
3250
|
-
owner: "caller"
|
|
3251
|
-
};
|
|
3252
|
-
const code = upstreamMachineCode(link);
|
|
3253
|
-
const status = read(link, "status") ?? read(link, "statusCode");
|
|
3254
|
-
if (code === "SEAT_NOT_LOGGED_IN") return {
|
|
3255
|
-
code: "credential-refused",
|
|
3256
|
-
owner: "caller"
|
|
3257
|
-
};
|
|
3258
|
-
if (code === "MODEL_NOT_FOUND") return {
|
|
3259
|
-
code: "request-rejected",
|
|
3260
|
-
owner: "caller"
|
|
3261
|
-
};
|
|
3262
|
-
if (typeof code === "string" && UNAVAILABLE_CODES.has(code.toLowerCase())) return {
|
|
3263
|
-
code: "upstream-unavailable",
|
|
3264
|
-
owner: "platform"
|
|
3265
|
-
};
|
|
3266
|
-
if (typeof status === "number" && Number.isInteger(status)) {
|
|
3267
|
-
if (UNAVAILABLE_STATUSES.has(status)) return {
|
|
3268
|
-
code: "upstream-unavailable",
|
|
3269
|
-
owner: "platform"
|
|
3270
|
-
};
|
|
3271
|
-
if (status >= 500 || status === 0 || status === 408) return {
|
|
3272
|
-
code: "transport",
|
|
3273
|
-
owner: "platform"
|
|
3274
|
-
};
|
|
3275
|
-
if (status === 401 || status === 403) return {
|
|
3276
|
-
code: "credential-refused",
|
|
3277
|
-
owner: "caller"
|
|
3278
|
-
};
|
|
3279
|
-
if (status >= 400) return {
|
|
3280
|
-
code: "request-rejected",
|
|
3281
|
-
owner: "caller"
|
|
3282
|
-
};
|
|
3283
|
-
}
|
|
3284
|
-
if (typeof code === "string" && TRANSPORT_ERROR_CODES.has(code)) return {
|
|
3285
|
-
code: "transport",
|
|
3286
|
-
owner: "platform"
|
|
3287
|
-
};
|
|
3288
|
-
if (typeof code === "string" && PLATFORM_FAILURE_CODES.has(code.toLowerCase())) return {
|
|
3289
|
-
code: "platform",
|
|
3290
|
-
owner: "platform"
|
|
3291
|
-
};
|
|
3292
|
-
const name = read(link, "name");
|
|
3293
|
-
if (name === "NetworkError" || name === "ServerError") return {
|
|
3294
|
-
code: "transport",
|
|
3295
|
-
owner: "platform"
|
|
3296
|
-
};
|
|
3297
|
-
}
|
|
3298
|
-
function machineCode(value) {
|
|
3299
|
-
return typeof value === "string" && MACHINE_CODE_PATTERN.test(value) ? value : void 0;
|
|
3300
|
-
}
|
|
3301
|
-
function upstreamMachineCode(link) {
|
|
3302
|
-
return machineCode(read(link, "upstreamCode")) ?? machineCode(read(link, "errorCode")) ?? machineCode(read(link, "code"));
|
|
3303
|
-
}
|
|
3304
|
-
/**
|
|
3305
|
-
* Classify a thrown failure. Pass the caller's signal so a cancellation the caller asked for is
|
|
3306
|
-
* attributed to the caller even when the provider surfaced it as an ordinary error, and say when
|
|
3307
|
-
* the operation's own deadline expired so a blown deadline is never reported as a cancellation.
|
|
3308
|
-
*/
|
|
3309
|
-
function runtimeFailureCause(error, context = {}) {
|
|
3310
|
-
const links = chain(error);
|
|
3311
|
-
const providerCode = [
|
|
3312
|
-
"upstreamCode",
|
|
3313
|
-
"errorCode",
|
|
3314
|
-
"code"
|
|
3315
|
-
].flatMap((field) => links.map((link) => machineCode(read(link, field)))).find((code) => code !== void 0);
|
|
3316
|
-
const failure = ({ code, owner }) => ({
|
|
3317
|
-
code,
|
|
3318
|
-
owner,
|
|
3319
|
-
cause: boundedCause(errMessage(error)),
|
|
3320
|
-
...providerCode === void 0 ? {} : { providerCode }
|
|
3321
|
-
});
|
|
3322
|
-
if (context.signal?.aborted) return failure({
|
|
3323
|
-
code: "aborted",
|
|
3324
|
-
owner: "caller"
|
|
3325
|
-
});
|
|
3326
|
-
if (context.deadlineExpired) return failure({
|
|
3327
|
-
code: "deadline-exceeded",
|
|
3328
|
-
owner: "caller"
|
|
3329
|
-
});
|
|
3330
|
-
if (context.signal === void 0 && links.some((link) => read(link, "name") === "AbortError")) return failure({
|
|
3331
|
-
code: "aborted",
|
|
3332
|
-
owner: "caller"
|
|
3333
|
-
});
|
|
3334
|
-
for (const link of links) {
|
|
3335
|
-
if (link instanceof RetainedExecutionPendingError) return failure(retainedFailures[link.pendingCause]);
|
|
3336
|
-
if (read(link, "name") === "RetainedRunProviderContractError") return failure(retainedFailures[classifyRetainedPendingCause(link)]);
|
|
3337
|
-
}
|
|
3338
|
-
for (const link of links) {
|
|
3339
|
-
const classified = structuralFailure(link);
|
|
3340
|
-
if (classified !== void 0) return failure(classified);
|
|
3341
|
-
}
|
|
3342
|
-
return failure(UNATTRIBUTED);
|
|
3343
|
-
}
|
|
3344
|
-
/**
|
|
3345
|
-
* Classify a failure the execution reported without throwing — a Sandbox outcome or executor
|
|
3346
|
-
* envelope with `success: false` — from its machine code and HTTP status. A harness that reports
|
|
3347
|
-
* a failure with neither does not say who must act, so it is `unknown`, never the work's fault.
|
|
3348
|
-
*/
|
|
3349
|
-
function reportedFailureCause(outcome) {
|
|
3350
|
-
return runtimeFailureCause(Object.assign(new Error(outcome.error ?? "execution reported a failure"), {
|
|
3351
|
-
errorCode: outcome.errorCode,
|
|
3352
|
-
statusCode: outcome.statusCode
|
|
3353
|
-
}));
|
|
3354
|
-
}
|
|
3355
|
-
/**
|
|
3356
|
-
* Classify a down settlement from the typed facts Runtime recorded while the cause was still a
|
|
3357
|
-
* value: the retained cause, the failure classified where the executor threw, then `infra: false`,
|
|
3358
|
-
* the throw path's statement that the work itself failed. `infra: true` is not an owner: Runtime
|
|
3359
|
-
* also sets it for cancellations, budget reconcile faults and validation errors, and from the
|
|
3360
|
-
* platform's failure messages, so a settlement with nothing else typed is `unknown`.
|
|
3361
|
-
*/
|
|
3362
|
-
function settlementFailureCause(down) {
|
|
3363
|
-
const cause = boundedCause(down.reason);
|
|
3364
|
-
if (down.retainedPendingCause !== void 0) return {
|
|
3365
|
-
...retainedFailures[down.retainedPendingCause],
|
|
3366
|
-
cause
|
|
3367
|
-
};
|
|
3368
|
-
if (down.failure !== void 0 && down.failure.code !== "unknown") return {
|
|
3369
|
-
...down.failure,
|
|
3370
|
-
cause
|
|
3371
|
-
};
|
|
3372
|
-
return {
|
|
3373
|
-
...down.infra === false ? {
|
|
3374
|
-
code: "work-failed",
|
|
3375
|
-
owner: "work"
|
|
3376
|
-
} : UNATTRIBUTED,
|
|
3377
|
-
cause,
|
|
3378
|
-
...down.failure?.providerCode === void 0 ? {} : { providerCode: down.failure.providerCode }
|
|
3379
|
-
};
|
|
3380
|
-
}
|
|
3381
315
|
// src/runtime/sandbox-outcome.ts
|
|
3382
316
|
/** Map the public Sandbox outcome to Runtime's stream and verdict vocabulary. */
|
|
3383
317
|
function projectSandboxOutcome(outcome) {
|
|
@@ -3446,7 +380,8 @@ function executableAgentProfileSnapshot(raw, context) {
|
|
|
3446
380
|
*/
|
|
3447
381
|
function authoredAgentProfileSnapshot(raw, context) {
|
|
3448
382
|
const input = detachedSnapshot(raw, `${context}: AgentProfile input`);
|
|
3449
|
-
|
|
383
|
+
const profile = agentProfileSchema.parse(input);
|
|
384
|
+
return detachedSnapshot(profile, `${context}: parsed AgentProfile`);
|
|
3450
385
|
}
|
|
3451
386
|
/** Snapshot an executable spec while retaining trusted callbacks by reference. */
|
|
3452
387
|
function executableAgentSpecSnapshot(raw, context) {
|
|
@@ -4248,6 +1183,7 @@ const CODEX_REPRODUCIBLE_ARGS = [
|
|
|
4248
1183
|
"--strict-config"
|
|
4249
1184
|
];
|
|
4250
1185
|
const CODEX_PERMISSION_PROFILE = "agent_runtime_reproducible";
|
|
1186
|
+
// src/mcp/local-harness-invocation.ts
|
|
4251
1187
|
/**
|
|
4252
1188
|
* Default per-harness command + arg shape. `buildArgs` takes ONLY the task prompt and
|
|
4253
1189
|
* emits the prompt-only invocation (no model, no system prompt) — the safe default shape
|
|
@@ -4266,16 +1202,7 @@ const HARNESS_INVOCATIONS = {
|
|
|
4266
1202
|
command: "claude",
|
|
4267
1203
|
buildArgs: (taskPrompt) => ["-p", taskPrompt],
|
|
4268
1204
|
modelArgs: (model) => ["--model", model],
|
|
4269
|
-
|
|
4270
|
-
levels: {
|
|
4271
|
-
low: "low",
|
|
4272
|
-
medium: "medium",
|
|
4273
|
-
high: "high",
|
|
4274
|
-
xhigh: "xhigh",
|
|
4275
|
-
ultracode: "max"
|
|
4276
|
-
},
|
|
4277
|
-
args: (level) => ["--effort", level]
|
|
4278
|
-
},
|
|
1205
|
+
reasoningArgs: (level) => ["--effort", level],
|
|
4279
1206
|
permissionBypassArgs: () => ["--dangerously-skip-permissions"],
|
|
4280
1207
|
profileMcp: true
|
|
4281
1208
|
},
|
|
@@ -4283,18 +1210,7 @@ const HARNESS_INVOCATIONS = {
|
|
|
4283
1210
|
command: "codex",
|
|
4284
1211
|
buildArgs: (taskPrompt) => ["exec", taskPrompt],
|
|
4285
1212
|
modelArgs: (model) => ["-m", model],
|
|
4286
|
-
|
|
4287
|
-
levels: {
|
|
4288
|
-
none: "none",
|
|
4289
|
-
minimal: "minimal",
|
|
4290
|
-
low: "low",
|
|
4291
|
-
medium: "medium",
|
|
4292
|
-
high: "high",
|
|
4293
|
-
xhigh: "xhigh",
|
|
4294
|
-
ultracode: "xhigh"
|
|
4295
|
-
},
|
|
4296
|
-
args: (level) => ["-c", `model_reasoning_effort="${level}"`]
|
|
4297
|
-
},
|
|
1213
|
+
reasoningArgs: (level) => ["-c", `model_reasoning_effort="${level}"`],
|
|
4298
1214
|
permissionBypassArgs: () => [
|
|
4299
1215
|
"--sandbox",
|
|
4300
1216
|
"workspace-write",
|
|
@@ -4307,18 +1223,8 @@ const HARNESS_INVOCATIONS = {
|
|
|
4307
1223
|
opencode: {
|
|
4308
1224
|
command: "opencode",
|
|
4309
1225
|
buildArgs: (taskPrompt) => ["run", taskPrompt],
|
|
4310
|
-
modelArgs: (model) => ["-m", model],
|
|
4311
|
-
|
|
4312
|
-
levels: {
|
|
4313
|
-
minimal: "minimal",
|
|
4314
|
-
low: "low",
|
|
4315
|
-
medium: "medium",
|
|
4316
|
-
high: "high",
|
|
4317
|
-
xhigh: "xhigh",
|
|
4318
|
-
ultracode: "max"
|
|
4319
|
-
},
|
|
4320
|
-
args: (level) => ["--variant", level]
|
|
4321
|
-
},
|
|
1226
|
+
modelArgs: (model) => ["-m", model],
|
|
1227
|
+
reasoningArgs: (level) => ["--variant", level],
|
|
4322
1228
|
permissionBypassArgs: () => ["--auto"],
|
|
4323
1229
|
profileMcp: true
|
|
4324
1230
|
},
|
|
@@ -4326,17 +1232,7 @@ const HARNESS_INVOCATIONS = {
|
|
|
4326
1232
|
command: "pi",
|
|
4327
1233
|
buildArgs: (taskPrompt) => ["--print", taskPrompt],
|
|
4328
1234
|
modelArgs: (model) => ["--model", model],
|
|
4329
|
-
|
|
4330
|
-
levels: {
|
|
4331
|
-
minimal: "minimal",
|
|
4332
|
-
low: "low",
|
|
4333
|
-
medium: "medium",
|
|
4334
|
-
high: "high",
|
|
4335
|
-
xhigh: "xhigh",
|
|
4336
|
-
ultracode: "max"
|
|
4337
|
-
},
|
|
4338
|
-
args: (level) => ["--thinking", level]
|
|
4339
|
-
},
|
|
1235
|
+
reasoningArgs: (level) => ["--thinking", level],
|
|
4340
1236
|
permissionBypassArgs: () => ["--approve"],
|
|
4341
1237
|
profileMcp: true
|
|
4342
1238
|
},
|
|
@@ -4353,10 +1249,11 @@ const LOCAL_HARNESSES = Object.keys(HARNESS_INVOCATIONS);
|
|
|
4353
1249
|
/**
|
|
4354
1250
|
* Whether the harness's native control can express this reasoning effort. Admission checks read
|
|
4355
1251
|
* this so a profile the invocation would later refuse is rejected BEFORE any workspace state is
|
|
4356
|
-
* created, against the same table that emits the argv.
|
|
1252
|
+
* created, against the same table that emits the argv. Every harness with a native control
|
|
1253
|
+
* expresses every rung through agent-interface's `nativeReasoningControl`.
|
|
4357
1254
|
*/
|
|
4358
|
-
function harnessSupportsReasoningEffort(harness,
|
|
4359
|
-
return HARNESS_INVOCATIONS[harness].
|
|
1255
|
+
function harnessSupportsReasoningEffort(harness, _reasoningEffort) {
|
|
1256
|
+
return HARNESS_INVOCATIONS[harness].reasoningArgs !== void 0;
|
|
4360
1257
|
}
|
|
4361
1258
|
/**
|
|
4362
1259
|
* Whether a profile's MCP servers reach this harness on the local-harness path — the row's
|
|
@@ -4367,11 +1264,13 @@ function harnessSupportsReasoningEffort(harness, reasoningEffort) {
|
|
|
4367
1264
|
function harnessMaterializesProfileMcp(harness) {
|
|
4368
1265
|
return HARNESS_INVOCATIONS[harness].profileMcp === true;
|
|
4369
1266
|
}
|
|
4370
|
-
|
|
4371
|
-
|
|
4372
|
-
|
|
4373
|
-
|
|
4374
|
-
|
|
1267
|
+
/** The argv for a profile effort; `null` from agent-interface means the CLI takes no flag
|
|
1268
|
+
* for that rung (opencode `none`). */
|
|
1269
|
+
function harnessReasoningArgs(harness, reasoningEffort, model) {
|
|
1270
|
+
const reasoningArgs = HARNESS_INVOCATIONS[harness].reasoningArgs;
|
|
1271
|
+
if (reasoningArgs === void 0) throw new Error(`harnessInvocation: ${harness} cannot express reasoning effort ${String(reasoningEffort)}`);
|
|
1272
|
+
const level = nativeReasoningControl(harness, reasoningEffort, model);
|
|
1273
|
+
return level === null ? [] : reasoningArgs(level);
|
|
4375
1274
|
}
|
|
4376
1275
|
function buildHarnessArgs(harness, taskPrompt, options = {}) {
|
|
4377
1276
|
const invocation = HARNESS_INVOCATIONS[harness];
|
|
@@ -4401,7 +1300,7 @@ function harnessInvocation(harness, profile, taskPrompt, options = {}) {
|
|
|
4401
1300
|
const model = harnessProfileModel(harness, profile);
|
|
4402
1301
|
if (model) args.push(...invocation.modelArgs(model));
|
|
4403
1302
|
const reasoningEffort = profile.model?.reasoningEffort;
|
|
4404
|
-
if (reasoningEffort !== void 0) args.push(...harnessReasoningArgs(harness, reasoningEffort));
|
|
1303
|
+
if (reasoningEffort !== void 0) args.push(...harnessReasoningArgs(harness, reasoningEffort, model));
|
|
4405
1304
|
return {
|
|
4406
1305
|
command: invocation.command,
|
|
4407
1306
|
args,
|
|
@@ -4645,7 +1544,7 @@ function assertCodexReproducibleInvocation(command, args) {
|
|
|
4645
1544
|
const hasExactPrefix = expectedPrefix.every((arg, index) => args[index] === arg);
|
|
4646
1545
|
const tail = args.slice(expectedPrefix.length);
|
|
4647
1546
|
const validModel = tail[0] === "-m" && typeof tail[1] === "string" && tail[1].length > 0 && !tail[1].startsWith("-");
|
|
4648
|
-
const validReasoning = tail[2] === "-c" && typeof tail[3] === "string" && /^model_reasoning_effort="(?:none|minimal|low|medium|high|xhigh)"$/.test(tail[3]);
|
|
1547
|
+
const validReasoning = tail[2] === "-c" && typeof tail[3] === "string" && /^model_reasoning_effort="(?:none|minimal|low|medium|high|xhigh|ultra)"$/.test(tail[3]);
|
|
4649
1548
|
const nativeKeys = /* @__PURE__ */ new Set();
|
|
4650
1549
|
const nativeArgs = tail.slice(4);
|
|
4651
1550
|
let validNative = nativeArgs.length % 2 === 0;
|
|
@@ -6502,16 +3401,37 @@ function upstreamStatusFromMessage(message) {
|
|
|
6502
3401
|
const status = Number(match[1]);
|
|
6503
3402
|
return status >= 100 && status <= 599 ? status : void 0;
|
|
6504
3403
|
}
|
|
6505
|
-
|
|
6506
|
-
|
|
6507
|
-
|
|
6508
|
-
|
|
6509
|
-
|
|
6510
|
-
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
3404
|
+
const subscriptionErrorSchema = runtimeFailureRemediationSchema.extend({
|
|
3405
|
+
type: z.literal("subscription_required"),
|
|
3406
|
+
status: z.literal(403).optional(),
|
|
3407
|
+
message: z.string().min(1).max(512),
|
|
3408
|
+
provider_dispatch: z.literal("not_started")
|
|
3409
|
+
});
|
|
3410
|
+
/** Decode only structured Bridge failures. HTTP status wins over any body claim. */
|
|
3411
|
+
function bridgeUpstreamError(error, prefix, httpStatus) {
|
|
3412
|
+
const body = typeof error === "string" ? error : void 0;
|
|
3413
|
+
if (body !== void 0) try {
|
|
3414
|
+
error = recordValue(recordValue(JSON.parse(body))?.error) ?? { message: body };
|
|
3415
|
+
} catch {
|
|
3416
|
+
error = { message: body };
|
|
3417
|
+
}
|
|
3418
|
+
const raw = recordValue(error) ?? {};
|
|
3419
|
+
const redacted = recordValue(defaultRedactor({
|
|
3420
|
+
error: raw,
|
|
3421
|
+
body
|
|
3422
|
+
})) ?? {};
|
|
3423
|
+
const safe = recordValue(redacted.error) ?? {};
|
|
3424
|
+
const status = httpStatus ?? (Number.isInteger(raw.status) && Number(raw.status) >= 400 && Number(raw.status) <= 599 ? Number(raw.status) : upstreamStatusFromMessage(typeof raw.message === "string" ? raw.message : void 0));
|
|
3425
|
+
const upstreamCode = runtimeProviderCode(safe.type);
|
|
3426
|
+
const remedy = (status === 403 && (httpStatus === 403 || raw.status === 403) ? subscriptionErrorSchema.safeParse(raw) : void 0)?.success ? runtimeFailureRemediationSchema.readonly().safeParse(safe) : void 0;
|
|
3427
|
+
const message = typeof safe.message === "string" ? safe.message : upstreamCode ?? "unknown";
|
|
3428
|
+
return new BackendTransportError("bridge", `${prefix}: ${message}`.slice(0, 512), {
|
|
3429
|
+
status,
|
|
3430
|
+
upstreamCode,
|
|
3431
|
+
...raw.provider_dispatch === "not_started" ? { providerDispatch: "not_started" } : {},
|
|
3432
|
+
...remedy?.success ? { remediation: remedy.data } : {},
|
|
3433
|
+
...typeof redacted.body === "string" ? { body: redacted.body.slice(0, 2048) } : {}
|
|
3434
|
+
});
|
|
6515
3435
|
}
|
|
6516
3436
|
function assertBridgeProfileMaterialization(value, profile, wireModel) {
|
|
6517
3437
|
if (!value || typeof value !== "object" || Array.isArray(value)) throw new ValidationError$1("bridgeExecutor: profile materialization receipt must be an object");
|
|
@@ -6654,7 +3574,7 @@ function parseBridgeInferenceObservation(value) {
|
|
|
6654
3574
|
"usage",
|
|
6655
3575
|
"usageReceipts"
|
|
6656
3576
|
], [], "profile materialization inference observation");
|
|
6657
|
-
|
|
3577
|
+
const observation = {
|
|
6658
3578
|
requests: bridgeInferenceCount(raw.requests, "requests"),
|
|
6659
3579
|
generationRequests: bridgeInferenceCount(raw.generationRequests, "generationRequests"),
|
|
6660
3580
|
auxiliaryRequests: bridgeInferenceCount(raw.auxiliaryRequests, "auxiliaryRequests"),
|
|
@@ -6664,7 +3584,8 @@ function parseBridgeInferenceObservation(value) {
|
|
|
6664
3584
|
inFlightRequests: bridgeInferenceCount(raw.inFlightRequests, "inFlightRequests"),
|
|
6665
3585
|
accountingMatched: bridgeInferenceBoolean(raw.accountingMatched, "accountingMatched"),
|
|
6666
3586
|
usage: parseBridgeInferenceUsage(raw.usage)
|
|
6667
|
-
}
|
|
3587
|
+
};
|
|
3588
|
+
return detachedSnapshot(observation, "bridgeExecutor: profile materialization inference observation");
|
|
6668
3589
|
}
|
|
6669
3590
|
function parseBridgeInferenceUsage(value) {
|
|
6670
3591
|
if (typeof value !== "object" || value === null || Array.isArray(value)) throw new ValidationError$1("bridgeExecutor: profile materialization inference usage must be an object");
|
|
@@ -6678,7 +3599,7 @@ function parseBridgeInferenceUsage(value) {
|
|
|
6678
3599
|
"outputTokens"
|
|
6679
3600
|
], "profile materialization inference usage");
|
|
6680
3601
|
if (raw.costKnown !== false) throw new ValidationError$1("bridgeExecutor: profile materialization inference usage must report costKnown=false");
|
|
6681
|
-
|
|
3602
|
+
const usage = {
|
|
6682
3603
|
...raw.inputTokens === void 0 ? {} : { inputTokens: bridgeInferenceCount(raw.inputTokens, "usage.inputTokens") },
|
|
6683
3604
|
...raw.freshInputTokens === void 0 ? {} : { freshInputTokens: bridgeInferenceCount(raw.freshInputTokens, "usage.freshInputTokens") },
|
|
6684
3605
|
...raw.cacheReadInputTokens === void 0 ? {} : { cacheReadInputTokens: bridgeInferenceCount(raw.cacheReadInputTokens, "usage.cacheReadInputTokens") },
|
|
@@ -6686,7 +3607,8 @@ function parseBridgeInferenceUsage(value) {
|
|
|
6686
3607
|
...raw.outputTokens === void 0 ? {} : { outputTokens: bridgeInferenceCount(raw.outputTokens, "usage.outputTokens") },
|
|
6687
3608
|
costKnown: false,
|
|
6688
3609
|
...raw.estimatedCost === void 0 ? {} : { estimatedCost: bridgeInferenceMoney(raw.estimatedCost, "usage.estimatedCost") }
|
|
6689
|
-
}
|
|
3610
|
+
};
|
|
3611
|
+
return detachedSnapshot(usage, "bridgeExecutor: profile materialization inference usage");
|
|
6690
3612
|
}
|
|
6691
3613
|
function assertBridgeReceiptKeys(value, required, optional, context) {
|
|
6692
3614
|
const requiredSet = new Set(required);
|
|
@@ -8203,6 +5125,7 @@ var BridgeSessionLedger = class {
|
|
|
8203
5125
|
const bridgeModelCredentialHeader = "x-cli-bridge-model-credential";
|
|
8204
5126
|
const bridgeModelBaseUrlHeader = "x-cli-bridge-model-base-url";
|
|
8205
5127
|
const bridgeUsageCostSchema = "cli-bridge.usage-cost.v1";
|
|
5128
|
+
const MAX_BRIDGE_ERROR_BYTES = 8192;
|
|
8206
5129
|
/** Ceiling on one run-state read. A bridge that hangs must not pin `refreshing` forever and
|
|
8207
5130
|
* silently stop every later refresh. */
|
|
8208
5131
|
const BRIDGE_RUN_STATE_TIMEOUT_MS = 2e3;
|
|
@@ -8221,6 +5144,28 @@ const BRIDGE_ROUTE_PROBE_TIMEOUT_MS = 3e4;
|
|
|
8221
5144
|
* restarted. A positive that goes stale fails at dispatch with the bridge's own retained error,
|
|
8222
5145
|
* which is what a child saw before the preflight existed. */
|
|
8223
5146
|
const routedModels = /* @__PURE__ */ new Map();
|
|
5147
|
+
/** Buffered reads share one bound for failures; successful API reads keep their existing size. */
|
|
5148
|
+
async function readBridgeResponse(res) {
|
|
5149
|
+
const status = res.statusCode ?? 0;
|
|
5150
|
+
const chunks = [];
|
|
5151
|
+
let bytes = 0;
|
|
5152
|
+
let refused;
|
|
5153
|
+
for await (const chunk of res) {
|
|
5154
|
+
const buffer = Buffer.from(chunk);
|
|
5155
|
+
bytes += buffer.byteLength;
|
|
5156
|
+
if ((status < 200 || status >= 300) && bytes > MAX_BRIDGE_ERROR_BYTES) {
|
|
5157
|
+
refused = "Bridge error response exceeded the byte limit";
|
|
5158
|
+
res.destroy();
|
|
5159
|
+
break;
|
|
5160
|
+
}
|
|
5161
|
+
chunks.push(buffer);
|
|
5162
|
+
}
|
|
5163
|
+
return {
|
|
5164
|
+
status,
|
|
5165
|
+
headers: res.headers,
|
|
5166
|
+
text: refused ?? Buffer.concat(chunks).toString("utf8")
|
|
5167
|
+
};
|
|
5168
|
+
}
|
|
8224
5169
|
/** One bridge GET over the `node:http(s)` core client — the same transport every other bridge
|
|
8225
5170
|
* read uses, so a computed provider endpoint never reaches global `fetch`. Resolves the status
|
|
8226
5171
|
* and the body text; a transport failure rejects. */
|
|
@@ -8236,16 +5181,10 @@ function bridgeGet(seam, path, timeoutMs, signal) {
|
|
|
8236
5181
|
method: "GET",
|
|
8237
5182
|
headers: { authorization: `Bearer ${seam.bridgeBearer}` },
|
|
8238
5183
|
timeout: timeoutMs
|
|
8239
|
-
}, (res) => {
|
|
8240
|
-
|
|
8241
|
-
|
|
8242
|
-
|
|
8243
|
-
resolve({
|
|
8244
|
-
status: res.statusCode ?? 0,
|
|
8245
|
-
body: Buffer.concat(chunks).toString("utf8")
|
|
8246
|
-
});
|
|
8247
|
-
})().catch(reject);
|
|
8248
|
-
});
|
|
5184
|
+
}, (res) => void readBridgeResponse(res).then(({ status, text }) => resolve({
|
|
5185
|
+
status,
|
|
5186
|
+
body: text
|
|
5187
|
+
}), reject));
|
|
8249
5188
|
const abort = () => req.destroy(new DOMException(`bridge GET ${path} aborted`, "AbortError"));
|
|
8250
5189
|
signal?.addEventListener("abort", abort, { once: true });
|
|
8251
5190
|
req.on("timeout", () => req.destroy(/* @__PURE__ */ new Error(`bridge GET ${path} timed out`)));
|
|
@@ -8352,33 +5291,10 @@ async function bridgeAdmissionRefusal(seam, signal) {
|
|
|
8352
5291
|
}
|
|
8353
5292
|
/** `GET /v1/runs/:id` — the bridge's durable-run registry read. Resolves `undefined` for any
|
|
8354
5293
|
* non-200 or non-conforming body rather than throwing: this is only ever an observability read. */
|
|
8355
|
-
function bridgeRunStateGet(seam, runId) {
|
|
8356
|
-
const
|
|
8357
|
-
|
|
8358
|
-
const requestFn = target.protocol === "https:" ? request$1 : request;
|
|
8359
|
-
return new Promise((resolve, reject) => {
|
|
8360
|
-
const req = requestFn(target, {
|
|
8361
|
-
method: "GET",
|
|
8362
|
-
headers: { authorization: `Bearer ${seam.bridgeBearer}` },
|
|
8363
|
-
timeout: BRIDGE_RUN_STATE_TIMEOUT_MS
|
|
8364
|
-
}, (res) => {
|
|
8365
|
-
(async () => {
|
|
8366
|
-
const chunks = [];
|
|
8367
|
-
for await (const chunk of res) chunks.push(Buffer.from(chunk));
|
|
8368
|
-
if (res.statusCode !== 200) {
|
|
8369
|
-
resolve(void 0);
|
|
8370
|
-
return;
|
|
8371
|
-
}
|
|
8372
|
-
resolve(readBridgeRunState(runId, Buffer.concat(chunks).toString("utf8")));
|
|
8373
|
-
})().catch(reject);
|
|
8374
|
-
});
|
|
8375
|
-
req.on("timeout", () => req.destroy(/* @__PURE__ */ new Error("bridgeExecutor: run state read timed out")));
|
|
8376
|
-
req.on("error", reject);
|
|
8377
|
-
req.end();
|
|
8378
|
-
});
|
|
5294
|
+
async function bridgeRunStateGet(seam, runId) {
|
|
5295
|
+
const response = await bridgeGet(seam, `/v1/runs/${encodeURIComponent(runId)}?wait_ms=0`, BRIDGE_RUN_STATE_TIMEOUT_MS);
|
|
5296
|
+
return response.status === 200 ? readBridgeRunState(runId, response.body) : void 0;
|
|
8379
5297
|
}
|
|
8380
|
-
/** Decode one run snapshot. A body that does not carry the fields we report is `undefined`, never
|
|
8381
|
-
* a partially-invented snapshot. */
|
|
8382
5298
|
function readBridgeRunState(runId, body) {
|
|
8383
5299
|
let parsed;
|
|
8384
5300
|
try {
|
|
@@ -8443,15 +5359,7 @@ async function* streamDurableBridgeRun(args) {
|
|
|
8443
5359
|
await reconnectBackoff(reconnects, args.signal);
|
|
8444
5360
|
continue;
|
|
8445
5361
|
}
|
|
8446
|
-
if (!res.ok) {
|
|
8447
|
-
const body = (await res.text()).slice(0, 300);
|
|
8448
|
-
const providerDispatch = providerDispatchFromErrorBody(body);
|
|
8449
|
-
throw new BackendTransportError("bridge", `bridgeExecutor: bridge ${res.status}: ${body}`, {
|
|
8450
|
-
status: res.status,
|
|
8451
|
-
body,
|
|
8452
|
-
...providerDispatch === "not_started" ? { providerDispatch } : {}
|
|
8453
|
-
});
|
|
8454
|
-
}
|
|
5362
|
+
if (!res.ok) throw bridgeUpstreamError(await res.text(), `bridgeExecutor: bridge ${res.status}`, res.status);
|
|
8455
5363
|
if (!res.body) throw new ValidationError$1("bridgeExecutor: bridge response had no body to stream");
|
|
8456
5364
|
assertBridgeResponseIdentity(res, args.run);
|
|
8457
5365
|
let sawDone = false;
|
|
@@ -8515,41 +5423,11 @@ function turnAbortedError(signal) {
|
|
|
8515
5423
|
* is not evidence about the process that will receive the next POST. The terminal receipt remains
|
|
8516
5424
|
* mandatory because the bridge can still restart between this GET and the run request. */
|
|
8517
5425
|
async function assertBridgeExecutionCapabilities(seam, signal, requiresRuntimeAttachments) {
|
|
8518
|
-
const
|
|
8519
|
-
|
|
8520
|
-
const response = await new Promise((resolve, reject) => {
|
|
8521
|
-
if (signal.aborted) {
|
|
8522
|
-
reject(new DOMException("bridgeExecutor: aborted before capability preflight", "AbortError"));
|
|
8523
|
-
return;
|
|
8524
|
-
}
|
|
8525
|
-
const req = requestFn(target, {
|
|
8526
|
-
method: "GET",
|
|
8527
|
-
headers: { authorization: `Bearer ${seam.bridgeBearer}` },
|
|
8528
|
-
timeout: 0
|
|
8529
|
-
}, (res) => {
|
|
8530
|
-
(async () => {
|
|
8531
|
-
const chunks = [];
|
|
8532
|
-
for await (const chunk of res) chunks.push(Buffer.from(chunk));
|
|
8533
|
-
resolve({
|
|
8534
|
-
status: res.statusCode ?? 0,
|
|
8535
|
-
headers: res.headers,
|
|
8536
|
-
text: Buffer.concat(chunks).toString("utf8")
|
|
8537
|
-
});
|
|
8538
|
-
})().catch(reject);
|
|
8539
|
-
});
|
|
8540
|
-
const abort = () => req.destroy(new DOMException("bridgeExecutor: preflight aborted", "AbortError"));
|
|
8541
|
-
signal.addEventListener("abort", abort, { once: true });
|
|
8542
|
-
req.on("error", reject);
|
|
8543
|
-
req.on("close", () => signal.removeEventListener("abort", abort));
|
|
8544
|
-
req.end();
|
|
8545
|
-
});
|
|
8546
|
-
if (response.status < 200 || response.status >= 300) throw new BackendTransportError("bridge", `bridgeExecutor: capability preflight returned ${response.status}: ${response.text.slice(0, 200)}`, {
|
|
8547
|
-
status: response.status,
|
|
8548
|
-
body: response.text.slice(0, 200)
|
|
8549
|
-
});
|
|
5426
|
+
const response = await bridgeGet(seam, "/", 0, signal);
|
|
5427
|
+
if (response.status < 200 || response.status >= 300) throw bridgeUpstreamError(response.body, "bridgeExecutor: capability preflight", response.status);
|
|
8550
5428
|
let body;
|
|
8551
5429
|
try {
|
|
8552
|
-
body = JSON.parse(response.
|
|
5430
|
+
body = JSON.parse(response.body);
|
|
8553
5431
|
} catch {
|
|
8554
5432
|
throw new ValidationError$1("bridgeExecutor: capability preflight returned invalid JSON");
|
|
8555
5433
|
}
|
|
@@ -8612,17 +5490,13 @@ async function bridgeStreamPost(url, args) {
|
|
|
8612
5490
|
res.once("close", () => args.signal.removeEventListener("abort", onAbort));
|
|
8613
5491
|
const status = res.statusCode ?? 0;
|
|
8614
5492
|
const ok = status >= 200 && status < 300;
|
|
8615
|
-
const body = Readable.toWeb(res);
|
|
5493
|
+
const body = ok ? Readable.toWeb(res) : null;
|
|
8616
5494
|
resolve({
|
|
8617
5495
|
ok,
|
|
8618
5496
|
status,
|
|
8619
5497
|
headers: res.headers,
|
|
8620
5498
|
body,
|
|
8621
|
-
text: async () =>
|
|
8622
|
-
const chunks = [];
|
|
8623
|
-
for await (const c of res) chunks.push(c);
|
|
8624
|
-
return Buffer.concat(chunks).toString("utf8");
|
|
8625
|
-
}
|
|
5499
|
+
text: async () => (await readBridgeResponse(res)).text
|
|
8626
5500
|
});
|
|
8627
5501
|
});
|
|
8628
5502
|
let response;
|
|
@@ -8674,17 +5548,7 @@ function bridgeCancelPost(seam, run, waitMs) {
|
|
|
8674
5548
|
"content-length": "0"
|
|
8675
5549
|
},
|
|
8676
5550
|
timeout: 0
|
|
8677
|
-
}, (res) =>
|
|
8678
|
-
(async () => {
|
|
8679
|
-
const chunks = [];
|
|
8680
|
-
for await (const chunk of res) chunks.push(Buffer.from(chunk));
|
|
8681
|
-
resolve({
|
|
8682
|
-
status: res.statusCode ?? 0,
|
|
8683
|
-
headers: res.headers,
|
|
8684
|
-
text: Buffer.concat(chunks).toString("utf8")
|
|
8685
|
-
});
|
|
8686
|
-
})().catch(reject);
|
|
8687
|
-
});
|
|
5551
|
+
}, (res) => void readBridgeResponse(res).then(resolve, reject));
|
|
8688
5552
|
req.on("error", reject);
|
|
8689
5553
|
req.end();
|
|
8690
5554
|
});
|
|
@@ -8732,19 +5596,6 @@ async function cancelBridgeRunToTerminal(seam, run, grace, stopSignal) {
|
|
|
8732
5596
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
8733
5597
|
}
|
|
8734
5598
|
}
|
|
8735
|
-
/** Read only the Router-owned one-sided field from an error body. */
|
|
8736
|
-
function providerDispatchFromErrorBody(body) {
|
|
8737
|
-
let parsed;
|
|
8738
|
-
try {
|
|
8739
|
-
parsed = JSON.parse(body);
|
|
8740
|
-
} catch {
|
|
8741
|
-
return;
|
|
8742
|
-
}
|
|
8743
|
-
if (typeof parsed !== "object" || parsed === null) return void 0;
|
|
8744
|
-
const error = parsed.error;
|
|
8745
|
-
if (typeof error !== "object" || error === null) return void 0;
|
|
8746
|
-
return error.provider_dispatch === "not_started" ? "not_started" : void 0;
|
|
8747
|
-
}
|
|
8748
5599
|
// src/runtime/supervise/bridge-turn-signals.ts
|
|
8749
5600
|
/**
|
|
8750
5601
|
* Why one cli-bridge turn's request is aborted: external teardown or abort, a forceful interrupt
|
|
@@ -11553,28 +8404,11 @@ function createRuntimeStreamEventCollector(options = {}) {
|
|
|
11553
8404
|
* The exporter accepts both raw OtelSpan objects and LoopTraceEvents
|
|
11554
8405
|
* (which get converted to OTLP spans automatically).
|
|
11555
8406
|
*/
|
|
11556
|
-
/** OTel GenAI: the tool definitions offered to the model. Matches the constant agent-eval and
|
|
11557
|
-
* traces each define locally for the same external convention (there is no shared semconv
|
|
11558
|
-
* package to import it from). */
|
|
11559
|
-
const TOOL_DEFINITIONS_ATTR = "gen_ai.tool.definitions";
|
|
11560
8407
|
const SCOPE = {
|
|
11561
8408
|
name: "@tangle-network/agent-runtime",
|
|
11562
8409
|
version: "0.83.0"
|
|
11563
8410
|
};
|
|
11564
8411
|
/**
|
|
11565
|
-
* Current (non-deprecated) OpenTelemetry GenAI semantic-convention keys.
|
|
11566
|
-
* Registry: https://opentelemetry.io/docs/specs/semconv/registry/attributes/gen-ai/
|
|
11567
|
-
* NB: `gen_ai.system` / `gen_ai.usage.prompt_tokens` / `completion_tokens` are
|
|
11568
|
-
* DEPRECATED — do not emit them. We use `provider.name` + `input/output_tokens`.
|
|
11569
|
-
*/
|
|
11570
|
-
const GEN_AI = {
|
|
11571
|
-
operation: "gen_ai.operation.name",
|
|
11572
|
-
agentName: "gen_ai.agent.name",
|
|
11573
|
-
conversationId: "gen_ai.conversation.id",
|
|
11574
|
-
inputTokens: "gen_ai.usage.input_tokens",
|
|
11575
|
-
outputTokens: "gen_ai.usage.output_tokens"
|
|
11576
|
-
};
|
|
11577
|
-
/**
|
|
11578
8412
|
* Create an exporter that APPENDS spans to a local OpenInference-JSONL file, one complete span per
|
|
11579
8413
|
* line, instead of posting them to a collector.
|
|
11580
8414
|
*
|
|
@@ -11632,18 +8466,9 @@ function nanosToIso(nanos) {
|
|
|
11632
8466
|
const ms = Number(BigInt(nanos) / 1000000n);
|
|
11633
8467
|
return new Date(ms).toISOString();
|
|
11634
8468
|
}
|
|
11635
|
-
/** OTLP's key/value attribute array -> the plain object OpenInference carries. */
|
|
11636
|
-
function attributesToObject(attributes) {
|
|
11637
|
-
const out = {};
|
|
11638
|
-
for (const a of attributes ?? []) {
|
|
11639
|
-
const v = a.value;
|
|
11640
|
-
out[a.key] = v.stringValue ?? (v.intValue !== void 0 ? Number(v.intValue) : void 0) ?? v.doubleValue ?? v.boolValue;
|
|
11641
|
-
}
|
|
11642
|
-
return out;
|
|
11643
|
-
}
|
|
11644
8469
|
/** One `OtelSpan` as the OpenInference JSONL line `traces` and other OpenInference tools read. */
|
|
11645
8470
|
function toOpenInferenceLine(span) {
|
|
11646
|
-
const attributes =
|
|
8471
|
+
const { attributes } = decodeOtlpAttributes(span.attributes);
|
|
11647
8472
|
const kind = attributes["openinference.span.kind"];
|
|
11648
8473
|
const resource = {};
|
|
11649
8474
|
for (const key of ["service.name", "agent.name"]) if (attributes[key] !== void 0) resource[key] = attributes[key];
|
|
@@ -11685,10 +8510,10 @@ function createOtelExporter(config) {
|
|
|
11685
8510
|
const flushIntervalMs = config?.flushIntervalMs ?? 5e3;
|
|
11686
8511
|
const maxQueueSize = Math.max(positiveInteger$1(config?.maxQueueSize, 2048), batchSize);
|
|
11687
8512
|
const timeoutMs = positiveInteger$1(config?.timeoutMs, 1e4);
|
|
11688
|
-
const resource =
|
|
8513
|
+
const resource = toOtlpAttributes({
|
|
11689
8514
|
"service.name": config?.serviceName ?? "agent-runtime",
|
|
11690
8515
|
...config?.resourceAttributes ?? {}
|
|
11691
|
-
})
|
|
8516
|
+
});
|
|
11692
8517
|
const queue = [];
|
|
11693
8518
|
let inFlight = 0;
|
|
11694
8519
|
let written = 0;
|
|
@@ -11702,13 +8527,7 @@ function createOtelExporter(config) {
|
|
|
11702
8527
|
lastError = reason;
|
|
11703
8528
|
};
|
|
11704
8529
|
async function send(batch) {
|
|
11705
|
-
const body =
|
|
11706
|
-
resource,
|
|
11707
|
-
scopeSpans: [{
|
|
11708
|
-
scope: SCOPE,
|
|
11709
|
-
spans: batch
|
|
11710
|
-
}]
|
|
11711
|
-
}] };
|
|
8530
|
+
const body = otlpTraceExport(resource, SCOPE, batch);
|
|
11712
8531
|
try {
|
|
11713
8532
|
const response = await fetch(url, {
|
|
11714
8533
|
method: "POST",
|
|
@@ -11725,9 +8544,10 @@ function createOtelExporter(config) {
|
|
|
11725
8544
|
drop(batch.length, `HTTP ${response.status} from ${url}${detail}`);
|
|
11726
8545
|
return;
|
|
11727
8546
|
}
|
|
11728
|
-
const
|
|
11729
|
-
|
|
11730
|
-
|
|
8547
|
+
const partial = readOtlpPartialSuccess(text);
|
|
8548
|
+
const rejected = Math.min(partial.rejectedSpans, batch.length);
|
|
8549
|
+
if (rejected > 0) drop(rejected, `collector rejected ${rejected} spans: ${partial.errorMessage ?? "no message"}`);
|
|
8550
|
+
written += batch.length - rejected;
|
|
11731
8551
|
} catch (error) {
|
|
11732
8552
|
drop(batch.length, `POST ${url} failed: ${describeFetchError(error)}`);
|
|
11733
8553
|
}
|
|
@@ -11787,45 +8607,6 @@ function createOtelExporter(config) {
|
|
|
11787
8607
|
}
|
|
11788
8608
|
};
|
|
11789
8609
|
}
|
|
11790
|
-
/**
|
|
11791
|
-
* Spans a 2xx OTLP/HTTP response still refused. The spec lets a collector accept a request and
|
|
11792
|
-
* reject part of it through `partialSuccess.rejectedSpans`; reading only the status would count
|
|
11793
|
-
* those spans as delivered. An empty or non-JSON body is a full success.
|
|
11794
|
-
*/
|
|
11795
|
-
function rejectedSpans(text, batchLength) {
|
|
11796
|
-
if (!text.trim()) return {
|
|
11797
|
-
count: 0,
|
|
11798
|
-
message: ""
|
|
11799
|
-
};
|
|
11800
|
-
let parsed;
|
|
11801
|
-
try {
|
|
11802
|
-
parsed = JSON.parse(text);
|
|
11803
|
-
} catch {
|
|
11804
|
-
return {
|
|
11805
|
-
count: 0,
|
|
11806
|
-
message: ""
|
|
11807
|
-
};
|
|
11808
|
-
}
|
|
11809
|
-
if (parsed === null || typeof parsed !== "object") return {
|
|
11810
|
-
count: 0,
|
|
11811
|
-
message: ""
|
|
11812
|
-
};
|
|
11813
|
-
const partial = parsed.partialSuccess;
|
|
11814
|
-
if (partial === null || typeof partial !== "object") return {
|
|
11815
|
-
count: 0,
|
|
11816
|
-
message: ""
|
|
11817
|
-
};
|
|
11818
|
-
const { rejectedSpans: raw, errorMessage } = partial;
|
|
11819
|
-
const count = typeof raw === "string" ? Number(raw) : typeof raw === "number" ? raw : 0;
|
|
11820
|
-
if (!Number.isSafeInteger(count) || count <= 0) return {
|
|
11821
|
-
count: 0,
|
|
11822
|
-
message: ""
|
|
11823
|
-
};
|
|
11824
|
-
return {
|
|
11825
|
-
count: Math.min(count, batchLength),
|
|
11826
|
-
message: typeof errorMessage === "string" && errorMessage ? errorMessage : "no message"
|
|
11827
|
-
};
|
|
11828
|
-
}
|
|
11829
8610
|
/** Node's fetch reports every socket failure as "fetch failed"; the cause names which one. */
|
|
11830
8611
|
function describeFetchError(error) {
|
|
11831
8612
|
if (!(error instanceof Error)) return String(error);
|
|
@@ -11848,14 +8629,14 @@ function loopEventToOtelSpan(event, traceId, parentSpanId) {
|
|
|
11848
8629
|
for (const [k, v] of Object.entries(event.payload)) if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") attrs[`loop.${k}`] = v;
|
|
11849
8630
|
const ts = msToNs(event.timestamp);
|
|
11850
8631
|
return {
|
|
11851
|
-
traceId:
|
|
8632
|
+
traceId: toW3CTraceId(traceId),
|
|
11852
8633
|
spanId,
|
|
11853
|
-
parentSpanId: parentSpanId ?
|
|
8634
|
+
parentSpanId: parentSpanId ? toW3CSpanId(parentSpanId) : void 0,
|
|
11854
8635
|
name: event.kind,
|
|
11855
8636
|
kind: 1,
|
|
11856
8637
|
startTimeUnixNano: ts,
|
|
11857
8638
|
endTimeUnixNano: ts,
|
|
11858
|
-
attributes:
|
|
8639
|
+
attributes: toOtlpAttributes(attrs),
|
|
11859
8640
|
status: { code: 1 }
|
|
11860
8641
|
};
|
|
11861
8642
|
}
|
|
@@ -11871,14 +8652,14 @@ function flatOtelSpan(name, attributes, traceId, timestampMs, parentSpanId, endT
|
|
|
11871
8652
|
const start = msToNs(timestampMs);
|
|
11872
8653
|
const end = msToNs(Math.max(timestampMs, endTimestampMs));
|
|
11873
8654
|
return {
|
|
11874
|
-
traceId:
|
|
8655
|
+
traceId: toW3CTraceId(traceId),
|
|
11875
8656
|
spanId: generateSpanId(),
|
|
11876
|
-
parentSpanId: parentSpanId ?
|
|
8657
|
+
parentSpanId: parentSpanId ? toW3CSpanId(parentSpanId) : void 0,
|
|
11877
8658
|
name,
|
|
11878
8659
|
kind: 1,
|
|
11879
8660
|
startTimeUnixNano: start,
|
|
11880
8661
|
endTimeUnixNano: end,
|
|
11881
|
-
attributes:
|
|
8662
|
+
attributes: toOtlpAttributes(attributes),
|
|
11882
8663
|
status: { code: 1 }
|
|
11883
8664
|
};
|
|
11884
8665
|
}
|
|
@@ -11935,7 +8716,7 @@ function buildRuntimeEventOtelSpans(events, traceId, parentSpanId, options = {})
|
|
|
11935
8716
|
if (event.costUsd !== void 0) attrs["tangle.cost.usd"] = event.costUsd;
|
|
11936
8717
|
if (event.latencyMs !== void 0) attrs["tangle.latency_ms"] = event.latencyMs;
|
|
11937
8718
|
if (event.finishReason !== void 0) attrs["gen_ai.response.finish_reasons"] = event.finishReason;
|
|
11938
|
-
if (event.tools !== void 0 && event.tools.length > 0) attrs[
|
|
8719
|
+
if (event.tools !== void 0 && event.tools.length > 0) attrs[ATTR.toolDefinitions] = JSON.stringify(event.tools.map((toolName) => ({
|
|
11939
8720
|
type: "function",
|
|
11940
8721
|
name: toolName
|
|
11941
8722
|
})));
|
|
@@ -11977,16 +8758,16 @@ function buildRuntimeEventOtelSpans(events, traceId, parentSpanId, options = {})
|
|
|
11977
8758
|
* per-runId event array (e.g. flushed on `loop.ended`) and export the result.
|
|
11978
8759
|
*/
|
|
11979
8760
|
function buildLoopOtelSpans(events, traceId, rootParentSpanId, redact) {
|
|
11980
|
-
const tid =
|
|
8761
|
+
const tid = toW3CTraceId(traceId);
|
|
11981
8762
|
return buildLoopSpanNodes(events, redact).map((node) => ({
|
|
11982
8763
|
traceId: tid,
|
|
11983
8764
|
spanId: node.spanId,
|
|
11984
|
-
parentSpanId: node.parentSpanId ?
|
|
8765
|
+
parentSpanId: node.parentSpanId ? toW3CSpanId(node.parentSpanId) : rootParentSpanId ? toW3CSpanId(rootParentSpanId) : void 0,
|
|
11985
8766
|
name: node.name,
|
|
11986
8767
|
kind: 1,
|
|
11987
8768
|
startTimeUnixNano: msToNs(node.startMs),
|
|
11988
8769
|
endTimeUnixNano: msToNs(node.endMs),
|
|
11989
|
-
attributes:
|
|
8770
|
+
attributes: toOtlpAttributes({
|
|
11990
8771
|
...node.attrs,
|
|
11991
8772
|
[ATTR.spanKind]: LOOP_SPAN_KIND[node.kind],
|
|
11992
8773
|
...node.parentSpanId === void 0 && rootParentSpanId ? { [ATTR.parentConfidence]: "explicit" } : {}
|
|
@@ -12043,8 +8824,8 @@ function buildLoopSpanNodes(events, redact) {
|
|
|
12043
8824
|
});
|
|
12044
8825
|
const sp = rec(started?.payload);
|
|
12045
8826
|
const rootAttrs = {
|
|
12046
|
-
[
|
|
12047
|
-
[
|
|
8827
|
+
[OPERATION_NAME_ATTR]: "invoke_workflow",
|
|
8828
|
+
[ATTR.conversationId]: runId,
|
|
12048
8829
|
"tangle.run.id": runId,
|
|
12049
8830
|
"tangle.loop.driver": str(sp.driver) ?? "driver"
|
|
12050
8831
|
};
|
|
@@ -12086,7 +8867,7 @@ function buildLoopSpanNodes(events, redact) {
|
|
|
12086
8867
|
const id = generateSpanId();
|
|
12087
8868
|
const roundIdx = num(p.roundIndex) ?? 0;
|
|
12088
8869
|
const attrs = {
|
|
12089
|
-
[
|
|
8870
|
+
[OPERATION_NAME_ATTR]: "invoke_workflow",
|
|
12090
8871
|
"tangle.loop.round.index": roundIdx,
|
|
12091
8872
|
"tangle.loop.move.kind": str(p.moveKind) ?? "unknown",
|
|
12092
8873
|
"tangle.loop.move.round": roundIdx,
|
|
@@ -12131,16 +8912,16 @@ function buildLoopSpanNodes(events, redact) {
|
|
|
12131
8912
|
const start = iterStartTs.get(idx) ?? e.timestamp;
|
|
12132
8913
|
const err = strRedacted(p.error);
|
|
12133
8914
|
const attrs = {
|
|
12134
|
-
[
|
|
8915
|
+
[OPERATION_NAME_ATTR]: "invoke_agent",
|
|
12135
8916
|
"tangle.loop.iteration.index": idx
|
|
12136
8917
|
};
|
|
12137
8918
|
const agent = str(p.agentRunName);
|
|
12138
|
-
if (agent) attrs[
|
|
8919
|
+
if (agent) attrs[ATTR.agentName] = agent;
|
|
12139
8920
|
const tu = rec(p.tokenUsage);
|
|
12140
8921
|
const inTok = num(tu.input);
|
|
12141
|
-
if (inTok !== void 0) attrs[
|
|
8922
|
+
if (inTok !== void 0) attrs[ATTR.inputTokens] = inTok;
|
|
12142
8923
|
const outTok = num(tu.output);
|
|
12143
|
-
if (outTok !== void 0) attrs[
|
|
8924
|
+
if (outTok !== void 0) attrs[ATTR.outputTokens] = outTok;
|
|
12144
8925
|
const cost = num(p.costUsd);
|
|
12145
8926
|
if (cost !== void 0) attrs["tangle.cost.usd"] = cost;
|
|
12146
8927
|
const verdict = rec(p.verdict);
|
|
@@ -12188,48 +8969,8 @@ function parseHeadersFromEnv() {
|
|
|
12188
8969
|
}
|
|
12189
8970
|
return out;
|
|
12190
8971
|
}
|
|
12191
|
-
/**
|
|
12192
|
-
* Convert a flat record into the OTLP attribute list. Non-finite numbers are DROPPED (an OTLP
|
|
12193
|
-
* `doubleValue` of `NaN`/`Infinity` is not representable), integers ride as `intValue`. Exported so
|
|
12194
|
-
* a producer that mints its own `OtelSpan` (the supervisor span recorder) builds attributes exactly
|
|
12195
|
-
* the way every span in this file does, rather than re-deriving the encoding.
|
|
12196
|
-
*/
|
|
12197
|
-
function toOtelAttributes(record) {
|
|
12198
|
-
return Object.entries(record).flatMap(([key, value]) => {
|
|
12199
|
-
if (typeof value === "number" && !Number.isFinite(value)) return [];
|
|
12200
|
-
return [{
|
|
12201
|
-
key,
|
|
12202
|
-
value: typeof value === "number" ? Number.isInteger(value) ? { intValue: value.toString() } : { doubleValue: value } : typeof value === "boolean" ? { boolValue: value } : { stringValue: value }
|
|
12203
|
-
}];
|
|
12204
|
-
});
|
|
12205
|
-
}
|
|
12206
8972
|
function msToNs(ms) {
|
|
12207
|
-
return (
|
|
12208
|
-
}
|
|
12209
|
-
/**
|
|
12210
|
-
* Map a caller-supplied span id onto the 16-hex OTLP encoding. An id that is already a valid W3C
|
|
12211
|
-
* span id passes through UNCHANGED — that is what lets an inherited `PARENT_SPAN_ID`/`TRACEPARENT`
|
|
12212
|
-
* id keep parenting the same trace. A DASHED hex id (a UUID-form id) passes through dash-stripped:
|
|
12213
|
-
* that is the exact wire id every earlier release exported for it, so cross-version joins survive
|
|
12214
|
-
* the strict-W3C upgrade. Anything else (a human run id) is DERIVED via the zero-dep contract's
|
|
12215
|
-
* `deriveHexId`, the one legal derivation: the old slice-and-pad produced ids that embedded the
|
|
12216
|
-
* raw input in the wire id and were not even valid hex (the contract's own `non-hex-id` validator
|
|
12217
|
-
* rejected what this module exported). Exported as the ONE wire-id normalization every writer
|
|
12218
|
-
* shares — `traceContextToEnv` builds the child's `TRACEPARENT` through these same functions, so
|
|
12219
|
-
* a parent's exported spans and the context it hands its children always name the same trace.
|
|
12220
|
-
*/
|
|
12221
|
-
function padSpanId(id) {
|
|
12222
|
-
if (isW3CSpanId(id)) return id;
|
|
12223
|
-
const dashless = id.replace(/-/g, "");
|
|
12224
|
-
return isW3CSpanId(dashless) ? dashless : deriveHexId(id, 8);
|
|
12225
|
-
}
|
|
12226
|
-
/** Trace-id counterpart of {@link padSpanId}: valid W3C trace ids pass through (dash-stripped when
|
|
12227
|
-
* UUID-form, preserving the pre-strict-W3C wire id), everything else is derived with
|
|
12228
|
-
* `deriveHexId(id, 16)` so every process derives the SAME wire id for the same run. */
|
|
12229
|
-
function padTraceId(id) {
|
|
12230
|
-
if (isW3CTraceId(id)) return id;
|
|
12231
|
-
const dashless = id.replace(/-/g, "");
|
|
12232
|
-
return isW3CTraceId(dashless) ? dashless : deriveHexId(id, 16);
|
|
8973
|
+
return msToUnixNano(Number.isFinite(ms) ? ms : Date.now());
|
|
12233
8974
|
}
|
|
12234
8975
|
/** Mint a fresh 16-hex-character OTLP span id. Exported so a producer that must know a span's id
|
|
12235
8976
|
* BEFORE the span closes (a node opened at spawn and parented by its children) uses this one
|
|
@@ -12273,7 +9014,7 @@ function parseTraceparent(value) {
|
|
|
12273
9014
|
if (!match) return void 0;
|
|
12274
9015
|
const traceId = match[1].toLowerCase();
|
|
12275
9016
|
const parentSpanId = match[2].toLowerCase();
|
|
12276
|
-
if (!isW3CTraceId(traceId) || !isW3CSpanId(parentSpanId)) return void 0;
|
|
9017
|
+
if (!traceContract.isW3CTraceId(traceId) || !traceContract.isW3CSpanId(parentSpanId)) return void 0;
|
|
12277
9018
|
return {
|
|
12278
9019
|
traceId,
|
|
12279
9020
|
parentSpanId
|
|
@@ -12333,7 +9074,7 @@ function createPropagatingTraceEmitter(ctx) {
|
|
|
12333
9074
|
* Build env vars to pass to a child subprocess so it inherits the current trace context.
|
|
12334
9075
|
*
|
|
12335
9076
|
* Writes BOTH conventions (see the module doc): `TRACEPARENT` carries the W3C-hex ids — mapped
|
|
12336
|
-
* through
|
|
9077
|
+
* through agent-trace-contract's `toW3CTraceId`/`toW3CSpanId` (dashed hex passes through dash-stripped, a
|
|
12337
9078
|
* human id is derived through the contract's `deriveHexId`), the SAME normalization every emitted
|
|
12338
9079
|
* span uses, so both spellings and the parent's own spans all name ONE trace — and the legacy
|
|
12339
9080
|
* pair carries the caller's ids verbatim. `TRACEPARENT` needs a parent span id by grammar; when
|
|
@@ -12342,7 +9083,7 @@ function createPropagatingTraceEmitter(ctx) {
|
|
|
12342
9083
|
function traceContextToEnv(ctx) {
|
|
12343
9084
|
const env = { TRACE_ID: ctx.traceId };
|
|
12344
9085
|
if (ctx.parentSpanId) env.PARENT_SPAN_ID = ctx.parentSpanId;
|
|
12345
|
-
if (ctx.parentSpanId) env.TRACEPARENT = `00-${
|
|
9086
|
+
if (ctx.parentSpanId) env.TRACEPARENT = `00-${traceContract.toW3CTraceId(ctx.traceId)}-${traceContract.toW3CSpanId(ctx.parentSpanId)}-01`;
|
|
12346
9087
|
return env;
|
|
12347
9088
|
}
|
|
12348
9089
|
/**
|
|
@@ -17056,7 +13797,7 @@ var RetainedObservation = class {
|
|
|
17056
13797
|
let clearQuiet;
|
|
17057
13798
|
try {
|
|
17058
13799
|
const exactWake = new Promise((resolve) => {
|
|
17059
|
-
wake = setImmediate
|
|
13800
|
+
wake = setImmediate(() => {
|
|
17060
13801
|
const close = () => {
|
|
17061
13802
|
const flowing = this.sawTerminal || this.lastFrameAt === void 0 ? 0 : RETAINED_TAIL_FLOW_MS - (Date.now() - this.lastFrameAt);
|
|
17062
13803
|
if (flowing <= 0) {
|
|
@@ -17142,7 +13883,7 @@ var RetainedObservation = class {
|
|
|
17142
13883
|
this.budgetExceeded = true;
|
|
17143
13884
|
break;
|
|
17144
13885
|
}
|
|
17145
|
-
if (this.observedFrames > 0 && this.observedFrames % RETAINED_OBSERVATION_BATCH_EVENTS === 0) await awaitAbortable$1(new Promise((resolve) => setImmediate
|
|
13886
|
+
if (this.observedFrames > 0 && this.observedFrames % RETAINED_OBSERVATION_BATCH_EVENTS === 0) await awaitAbortable$1(new Promise((resolve) => setImmediate(resolve)), this.signal);
|
|
17146
13887
|
let observed;
|
|
17147
13888
|
try {
|
|
17148
13889
|
observed = await this.raceNextRead(iterator);
|
|
@@ -23625,418 +20366,6 @@ function createExecutorRegistry() {
|
|
|
23625
20366
|
}
|
|
23626
20367
|
};
|
|
23627
20368
|
}
|
|
23628
|
-
// src/durable/spawn-journal/event-index.ts
|
|
23629
|
-
/**
|
|
23630
|
-
* A trace root is valid only beside the output it commits to, with a well-formed root, artifact
|
|
23631
|
-
* reference and leaf count (at least the summary leaf). Records written before trace roots carry
|
|
23632
|
-
* none and stay valid.
|
|
23633
|
-
*/
|
|
23634
|
-
function assertTraceRoot(event, root) {
|
|
23635
|
-
if (!("traceRoot" in event) || event.traceRoot === void 0) return;
|
|
23636
|
-
const receipt = event.traceRoot;
|
|
23637
|
-
const fields = receipt !== null && typeof receipt === "object" ? receipt : {};
|
|
23638
|
-
if (typeof event.outRef !== "string" || typeof fields.root !== "string" || !/^sha256:[0-9a-f]{64}$/.test(fields.root) || typeof fields.ref !== "string" || !/^sha256:[0-9a-f]{64}$/.test(fields.ref) || typeof fields.leafCount !== "number" || !Number.isSafeInteger(fields.leafCount) || fields.leafCount < 1) throw new Error(`spawn journal corrupted: ${event.kind} for node '${event.id}' carries a malformed trace root in tree '${root}'`);
|
|
23639
|
-
}
|
|
23640
|
-
/** One validation implementation for memory, durable append, and cold replay. */
|
|
23641
|
-
/** @internal shared with the SQL-backed store. */
|
|
23642
|
-
var SpawnEventIndex = class {
|
|
23643
|
-
root;
|
|
23644
|
-
nodes = /* @__PURE__ */ new Map();
|
|
23645
|
-
cursors = /* @__PURE__ */ new Set();
|
|
23646
|
-
constructor(root) {
|
|
23647
|
-
this.root = root;
|
|
23648
|
-
}
|
|
23649
|
-
assert(event) {
|
|
23650
|
-
const node = this.nodes.get(event.id);
|
|
23651
|
-
this.assertRetained(node, event);
|
|
23652
|
-
assertTraceRoot(event, this.root);
|
|
23653
|
-
if (event.kind === "execution-evidence") {
|
|
23654
|
-
if (!node?.spawned || !/^sha256:[0-9a-f]{64}$/.test(event.outRef)) throw new Error("spawn journal corrupted: executor evidence has no spawned node or valid content reference");
|
|
23655
|
-
}
|
|
23656
|
-
if (event.kind === "materialized") {
|
|
23657
|
-
if (node?.materialized) throw new Error(`spawn journal corrupted: duplicate materialization receipt for node '${event.id}' in tree '${this.root}'`);
|
|
23658
|
-
if (!node?.spawned) throw new Error(`spawn journal corrupted: materialization for node '${event.id}' precedes its spawn in tree '${this.root}'`);
|
|
23659
|
-
}
|
|
23660
|
-
if (event.kind === "seat-materialized") {
|
|
23661
|
-
if (!node?.materialized || node.authoredProfileDigest === void 0 || event.receipt.status !== "known" || event.receipt.authoredProfileDigest !== node.authoredProfileDigest || !Number.isSafeInteger(event.segmentIndex) || event.segmentIndex <= node.latestSeatMaterialization) throw new Error(`spawn journal corrupted: invalid seat materialization for node '${event.id}' in tree '${this.root}'`);
|
|
23662
|
-
}
|
|
23663
|
-
if (event.kind === "execution-bound") {
|
|
23664
|
-
if (node?.bindings.has(event.binding.attemptId)) throw new Error(`spawn journal corrupted: duplicate execution binding for node '${event.id}' attempt '${event.binding.attemptId}' in tree '${this.root}'`);
|
|
23665
|
-
if (!node?.materialized) throw new Error(`spawn journal corrupted: execution binding for node '${event.id}' precedes materialization in tree '${this.root}'`);
|
|
23666
|
-
}
|
|
23667
|
-
if (!outsideCursorNamespace(event) && this.cursors.has(event.seq)) throw new Error(`spawn journal corrupted: duplicate cursor seq ${event.seq} in tree '${this.root}'; the cursor order replay relies on is not unique`);
|
|
23668
|
-
}
|
|
23669
|
-
/** Called after validation; a file writer discards staged state if grouped publication fails. */
|
|
23670
|
-
add(event) {
|
|
23671
|
-
if (!outsideCursorNamespace(event)) this.cursors.add(event.seq);
|
|
23672
|
-
if (event.kind !== "spawned" && event.kind !== "materialized" && event.kind !== "seat-materialized" && event.kind !== "execution-bound" && event.kind !== "execution-input" && event.kind !== "execution-admitted" && event.kind !== "execution-result" && event.kind !== "execution-abandoned" && event.kind !== "environment-teardown" && event.kind !== "environment-terminal" && !closesCursorSlot(event)) return;
|
|
23673
|
-
let node = this.nodes.get(event.id);
|
|
23674
|
-
if (!node) {
|
|
23675
|
-
node = {
|
|
23676
|
-
spawned: false,
|
|
23677
|
-
closed: false,
|
|
23678
|
-
materialized: false,
|
|
23679
|
-
latestSeatMaterialization: 0,
|
|
23680
|
-
bindings: /* @__PURE__ */ new Set(),
|
|
23681
|
-
inputs: /* @__PURE__ */ new Set(),
|
|
23682
|
-
input: false,
|
|
23683
|
-
result: false,
|
|
23684
|
-
abandoned: false,
|
|
23685
|
-
admissions: /* @__PURE__ */ new Map(),
|
|
23686
|
-
unusableEnvironments: /* @__PURE__ */ new Set()
|
|
23687
|
-
};
|
|
23688
|
-
this.nodes.set(event.id, node);
|
|
23689
|
-
}
|
|
23690
|
-
if (event.kind === "environment-teardown") {
|
|
23691
|
-
if (event.destroyed) node.unusableEnvironments.add(event.environmentId);
|
|
23692
|
-
return;
|
|
23693
|
-
}
|
|
23694
|
-
if (event.kind === "environment-terminal") {
|
|
23695
|
-
node.unusableEnvironments.add(event.environmentId);
|
|
23696
|
-
return;
|
|
23697
|
-
}
|
|
23698
|
-
if (event.kind === "execution-admitted" && event.admission.phase === "environment") node.lastEnvironmentId = event.admission.environmentId;
|
|
23699
|
-
if (event.kind === "spawned") node.spawned = true;
|
|
23700
|
-
else if (event.kind === "materialized") {
|
|
23701
|
-
node.materialized = true;
|
|
23702
|
-
node.authoredProfileDigest = event.receipt.authoredProfileDigest;
|
|
23703
|
-
} else if (event.kind === "seat-materialized") node.latestSeatMaterialization = event.segmentIndex;
|
|
23704
|
-
else if (event.kind === "execution-bound") node.bindings.add(event.binding.attemptId);
|
|
23705
|
-
else if (event.kind === "execution-input") {
|
|
23706
|
-
node.inputs.add(event.seq);
|
|
23707
|
-
node.input = true;
|
|
23708
|
-
node.latestInput = event.seq;
|
|
23709
|
-
node.result = false;
|
|
23710
|
-
node.abandoned = false;
|
|
23711
|
-
node.admissions.clear();
|
|
23712
|
-
} else if (event.kind === "execution-admitted") node.admissions.set(event.admission.phase, event.admission);
|
|
23713
|
-
else if (event.kind === "execution-result") node.result = true;
|
|
23714
|
-
else if (event.kind === "execution-abandoned") node.abandoned = true;
|
|
23715
|
-
if (closesCursorSlot(event)) node.closed = true;
|
|
23716
|
-
}
|
|
23717
|
-
assertRetained(node, event) {
|
|
23718
|
-
if (event.kind !== "execution-input" && event.kind !== "execution-admitted" && event.kind !== "execution-result" && event.kind !== "execution-abandoned") return;
|
|
23719
|
-
function fail(reason) {
|
|
23720
|
-
throw new Error(`spawn journal corrupted: retained execution '${event.id}' ${reason}`);
|
|
23721
|
-
}
|
|
23722
|
-
if (!node?.spawned) fail("precedes its spawn");
|
|
23723
|
-
if (node.closed) fail("follows its terminal settlement");
|
|
23724
|
-
if (event.kind === "execution-input") {
|
|
23725
|
-
if (!/^sha256:[0-9a-f]{64}$/.test(event.taskRef)) fail("has an invalid task reference");
|
|
23726
|
-
if (node.inputs.has(event.seq)) fail("has duplicate input sequence");
|
|
23727
|
-
const environment = node.admissions.get("environment");
|
|
23728
|
-
const ranIn = environment?.phase === "environment" ? environment.environmentId : node.lastEnvironmentId;
|
|
23729
|
-
const closed = node.abandoned || ranIn !== void 0 && node.unusableEnvironments.has(ranIn);
|
|
23730
|
-
if (node.input && !node.result && !closed) fail("input replaces an unfinished invocation");
|
|
23731
|
-
return;
|
|
23732
|
-
}
|
|
23733
|
-
if (event.kind === "execution-abandoned") {
|
|
23734
|
-
if (!node.input || node.latestInput !== event.inputSeq) fail("abandons an invocation that is not its latest");
|
|
23735
|
-
if (node.result) fail("abandons an invocation that has a result");
|
|
23736
|
-
if (node.abandoned) fail("abandons an invocation twice");
|
|
23737
|
-
if (!Number.isSafeInteger(event.failures) || event.failures < 1) fail("abandons without a failure count");
|
|
23738
|
-
return;
|
|
23739
|
-
}
|
|
23740
|
-
if (node.abandoned) fail(`${event.kind} follows its abandonment`);
|
|
23741
|
-
if (event.kind === "execution-result") {
|
|
23742
|
-
if (node.result) fail("has duplicate result");
|
|
23743
|
-
if (!node.admissions.has("dispatched")) fail("result precedes dispatch");
|
|
23744
|
-
if (!/^sha256:[0-9a-f]{64}$/.test(event.outRef)) fail("has an invalid result reference");
|
|
23745
|
-
assertValidSpend(event.spent, "retained execution result");
|
|
23746
|
-
executorFailureReason(event);
|
|
23747
|
-
return;
|
|
23748
|
-
}
|
|
23749
|
-
const admission = event.admission;
|
|
23750
|
-
if (!admission || ![
|
|
23751
|
-
"intent",
|
|
23752
|
-
"environment",
|
|
23753
|
-
"dispatched"
|
|
23754
|
-
].includes(admission.phase)) fail("has an invalid admission phase");
|
|
23755
|
-
if (node.admissions.has(admission.phase)) fail("has duplicate admission phase");
|
|
23756
|
-
if (node.result) fail("admission follows result");
|
|
23757
|
-
if (!node.input) fail("admission precedes input");
|
|
23758
|
-
if (admission.phase === "intent") {
|
|
23759
|
-
if (node.admissions.size !== 0) fail("intent follows another admission");
|
|
23760
|
-
return;
|
|
23761
|
-
}
|
|
23762
|
-
const intent = node.admissions.get("intent");
|
|
23763
|
-
if (intent?.phase !== "intent") fail("admission precedes intent");
|
|
23764
|
-
if (admission.idempotencyKey !== intent.idempotencyKey || admission.turnId !== intent.turnId) fail("changes its admitted request identity");
|
|
23765
|
-
if (admission.phase === "environment") {
|
|
23766
|
-
if (admission.provider !== intent.provider || admission.sessionId !== intent.sessionId || admission.executionId !== intent.executionId) fail("changes its admitted execution identity");
|
|
23767
|
-
return;
|
|
23768
|
-
}
|
|
23769
|
-
const environment = node.admissions.get("environment");
|
|
23770
|
-
if (environment?.phase !== "environment") fail("dispatch precedes environment");
|
|
23771
|
-
const control = admission.controlRef;
|
|
23772
|
-
if (!control || control.provider !== intent.provider || control.environmentId !== environment.environmentId || control.sessionId !== intent.sessionId || control.executionId !== intent.executionId) fail("dispatch changes its admitted execution identity");
|
|
23773
|
-
}
|
|
23774
|
-
};
|
|
23775
|
-
/**
|
|
23776
|
-
* The records that CLOSE a node's cursor slot: a settlement or a cancellation. The complement question to {@link outsideCursorNamespace}, and the one a resume needs —
|
|
23777
|
-
* a kind missing here leaves a finished node looking live, so its budget is never reclaimed and
|
|
23778
|
-
* its reservation is replayed as still in doubt.
|
|
23779
|
-
*/
|
|
23780
|
-
function closesCursorSlot(ev) {
|
|
23781
|
-
return ev.kind === "settled" || ev.kind === "cancelled";
|
|
23782
|
-
}
|
|
23783
|
-
const outsideKinds = /* @__PURE__ */ new Set([
|
|
23784
|
-
"spawned",
|
|
23785
|
-
"metered",
|
|
23786
|
-
"materialized",
|
|
23787
|
-
"execution-bound",
|
|
23788
|
-
"execution-input",
|
|
23789
|
-
"execution-admitted",
|
|
23790
|
-
"execution-result",
|
|
23791
|
-
"execution-abandoned",
|
|
23792
|
-
"execution-evidence",
|
|
23793
|
-
"evidence-pending",
|
|
23794
|
-
"progress",
|
|
23795
|
-
"reconciled",
|
|
23796
|
-
"edge",
|
|
23797
|
-
"teardown-unconfirmed",
|
|
23798
|
-
"teardown-pending",
|
|
23799
|
-
"teardown-confirmed",
|
|
23800
|
-
"environment-teardown",
|
|
23801
|
-
"environment-terminal",
|
|
23802
|
-
"workspace-checkpoint-requested",
|
|
23803
|
-
"workspace-checkpoint",
|
|
23804
|
-
"workspace-checkpoint-fork-requested",
|
|
23805
|
-
"workspace-checkpoint-fork-ready",
|
|
23806
|
-
"workspace-checkpoint-fork-teardown",
|
|
23807
|
-
"workspace-checkpoint-capture",
|
|
23808
|
-
"workspace-checkpoint-cleanup",
|
|
23809
|
-
"workspace-restored",
|
|
23810
|
-
"workspace-capture-started",
|
|
23811
|
-
"workspace-capture",
|
|
23812
|
-
"trace-unpropagated",
|
|
23813
|
-
"seat-segment",
|
|
23814
|
-
"seat-materialized",
|
|
23815
|
-
"seat-paused",
|
|
23816
|
-
"run-parked",
|
|
23817
|
-
"root-segment",
|
|
23818
|
-
"root-reentered",
|
|
23819
|
-
"reopened",
|
|
23820
|
-
"paused",
|
|
23821
|
-
"barren-pause",
|
|
23822
|
-
"driver-attempt",
|
|
23823
|
-
"node-inputs-resolved",
|
|
23824
|
-
"edge-verdict",
|
|
23825
|
-
"join-state"
|
|
23826
|
-
]);
|
|
23827
|
-
/** True for a node-CREATION or informational record. Narrows, so a caller keeps the union's arms. */
|
|
23828
|
-
function outsideCursorNamespace(ev) {
|
|
23829
|
-
return outsideKinds.has(ev.kind);
|
|
23830
|
-
}
|
|
23831
|
-
/** True for a record inside the cursor namespace. Narrows to {@link CursorNamespaceEvent}. */
|
|
23832
|
-
function insideCursorNamespace(ev) {
|
|
23833
|
-
return !outsideKinds.has(ev.kind);
|
|
23834
|
-
}
|
|
23835
|
-
// src/durable/jsonl-append.ts
|
|
23836
|
-
/** Commit JSONL bytes and validate their descriptor under one generation fence. @internal */
|
|
23837
|
-
function commitJsonLine(path, record, acknowledge) {
|
|
23838
|
-
const encoded = JSON.stringify(record);
|
|
23839
|
-
if (encoded === void 0) throw new TypeError("JSONL records must be JSON serializable");
|
|
23840
|
-
return fenceRunWrite(() => {
|
|
23841
|
-
const separator = prepareJsonlTail(path);
|
|
23842
|
-
const fd = openSync(path, "a");
|
|
23843
|
-
try {
|
|
23844
|
-
const bytes = Buffer.from(`${separator ? "\n" : ""}${encoded}\n`);
|
|
23845
|
-
let offset = 0;
|
|
23846
|
-
while (offset < bytes.length) {
|
|
23847
|
-
const written = writeSync(fd, bytes, offset, bytes.length - offset);
|
|
23848
|
-
if (written <= 0) throw new Error(`append-only file write made no progress at byte ${offset}`);
|
|
23849
|
-
offset += written;
|
|
23850
|
-
}
|
|
23851
|
-
fsyncSync(fd);
|
|
23852
|
-
return acknowledge?.(fd);
|
|
23853
|
-
} finally {
|
|
23854
|
-
closeSync(fd);
|
|
23855
|
-
}
|
|
23856
|
-
});
|
|
23857
|
-
}
|
|
23858
|
-
/** Recover only an uncommitted tail. Caller holds the generation fence. @internal */
|
|
23859
|
-
function prepareJsonlTail(path) {
|
|
23860
|
-
let fd;
|
|
23861
|
-
try {
|
|
23862
|
-
fd = openSync(path, "r+");
|
|
23863
|
-
} catch (error) {
|
|
23864
|
-
if (error.code === "ENOENT") return false;
|
|
23865
|
-
throw error;
|
|
23866
|
-
}
|
|
23867
|
-
try {
|
|
23868
|
-
const size = fstatSync(fd).size;
|
|
23869
|
-
if (size === 0) return false;
|
|
23870
|
-
const last = Buffer.allocUnsafe(1);
|
|
23871
|
-
readAt(fd, last, size - 1);
|
|
23872
|
-
if (last[0] === 10) return false;
|
|
23873
|
-
const chunks = [];
|
|
23874
|
-
let end = size;
|
|
23875
|
-
let tailStart = 0;
|
|
23876
|
-
while (end > 0) {
|
|
23877
|
-
const start = Math.max(0, end - 65536);
|
|
23878
|
-
const chunk = Buffer.allocUnsafe(end - start);
|
|
23879
|
-
readAt(fd, chunk, start);
|
|
23880
|
-
const newline = chunk.lastIndexOf(10);
|
|
23881
|
-
chunks.push(chunk.subarray(newline + 1));
|
|
23882
|
-
if (newline >= 0) {
|
|
23883
|
-
tailStart = start + newline + 1;
|
|
23884
|
-
break;
|
|
23885
|
-
}
|
|
23886
|
-
end = start;
|
|
23887
|
-
}
|
|
23888
|
-
try {
|
|
23889
|
-
JSON.parse(Buffer.concat(chunks.reverse()).toString("utf8"));
|
|
23890
|
-
return true;
|
|
23891
|
-
} catch (error) {
|
|
23892
|
-
if (!(error instanceof SyntaxError)) throw error;
|
|
23893
|
-
ftruncateSync(fd, tailStart);
|
|
23894
|
-
fsyncSync(fd);
|
|
23895
|
-
return false;
|
|
23896
|
-
}
|
|
23897
|
-
} finally {
|
|
23898
|
-
closeSync(fd);
|
|
23899
|
-
}
|
|
23900
|
-
}
|
|
23901
|
-
function readAt(fd, buffer, position) {
|
|
23902
|
-
let offset = 0;
|
|
23903
|
-
while (offset < buffer.length) {
|
|
23904
|
-
const read = readSync(fd, buffer, offset, buffer.length - offset, position + offset);
|
|
23905
|
-
if (read === 0) throw new Error("journal changed while preparing its append tail");
|
|
23906
|
-
offset += read;
|
|
23907
|
-
}
|
|
23908
|
-
}
|
|
23909
|
-
// src/durable/jsonl-file.ts
|
|
23910
|
-
/** Read a fixed file prefix one record at a time, never allocating the whole journal as a string.
|
|
23911
|
-
* A reader cannot chase concurrent appends forever. Missing files are empty only when requested;
|
|
23912
|
-
* corruption, short reads, and other I/O errors remain failures. Closing the iterator closes the fd. */
|
|
23913
|
-
async function* readCommittedJsonLines(path, options = {}) {
|
|
23914
|
-
const fs = await import("node:fs/promises");
|
|
23915
|
-
let handle;
|
|
23916
|
-
try {
|
|
23917
|
-
handle = await fs.open(path, "r");
|
|
23918
|
-
} catch (error) {
|
|
23919
|
-
if (options.allowMissing && isNoEntError(error)) return;
|
|
23920
|
-
throw error;
|
|
23921
|
-
}
|
|
23922
|
-
try {
|
|
23923
|
-
const { size } = await handle.stat();
|
|
23924
|
-
if (size === 0) return;
|
|
23925
|
-
const { StringDecoder } = await import("node:string_decoder");
|
|
23926
|
-
const decoder = new StringDecoder("utf8");
|
|
23927
|
-
const stream = handle.createReadStream({
|
|
23928
|
-
autoClose: false,
|
|
23929
|
-
end: size - 1
|
|
23930
|
-
});
|
|
23931
|
-
const fragments = [];
|
|
23932
|
-
let lineNumber = 1;
|
|
23933
|
-
try {
|
|
23934
|
-
for await (const chunk of stream) {
|
|
23935
|
-
options.onBytes?.(chunk);
|
|
23936
|
-
const text = decoder.write(chunk);
|
|
23937
|
-
let start = 0;
|
|
23938
|
-
let end = text.indexOf("\n", start);
|
|
23939
|
-
while (end !== -1) {
|
|
23940
|
-
fragments.push(text.slice(start, end));
|
|
23941
|
-
const record = parseRecord(fragments.join(""), path, lineNumber++, true, options.allowWhitespaceLines);
|
|
23942
|
-
fragments.length = 0;
|
|
23943
|
-
if (record !== void 0) yield record.value;
|
|
23944
|
-
start = end + 1;
|
|
23945
|
-
end = text.indexOf("\n", start);
|
|
23946
|
-
}
|
|
23947
|
-
if (start < text.length) fragments.push(text.slice(start));
|
|
23948
|
-
}
|
|
23949
|
-
if (stream.bytesRead !== size) throw new Error(`${path}: journal changed while reading its committed prefix`);
|
|
23950
|
-
fragments.push(decoder.end());
|
|
23951
|
-
const record = parseRecord(fragments.join(""), path, lineNumber, false, options.allowWhitespaceLines);
|
|
23952
|
-
if (record !== void 0) yield record.value;
|
|
23953
|
-
} finally {
|
|
23954
|
-
stream.destroy();
|
|
23955
|
-
}
|
|
23956
|
-
} finally {
|
|
23957
|
-
await handle.close();
|
|
23958
|
-
}
|
|
23959
|
-
}
|
|
23960
|
-
/** An invalid unterminated tail may be a torn write; other parse failures are not evidence of one. */
|
|
23961
|
-
function parseRecord(line, source, lineNumber, terminated, allowWhitespaceLines = false) {
|
|
23962
|
-
if (line.length === 0 || allowWhitespaceLines && line.trim().length === 0) return void 0;
|
|
23963
|
-
try {
|
|
23964
|
-
return { value: JSON.parse(line) };
|
|
23965
|
-
} catch (cause) {
|
|
23966
|
-
if (!(cause instanceof SyntaxError)) throw cause;
|
|
23967
|
-
if (!terminated) return void 0;
|
|
23968
|
-
throw new Error(`${source}: malformed JSONL record at line ${lineNumber}`, { cause });
|
|
23969
|
-
}
|
|
23970
|
-
}
|
|
23971
|
-
async function appendCommittedJsonLine(path, record, acknowledge) {
|
|
23972
|
-
return commitJsonLine(path, record, acknowledge);
|
|
23973
|
-
}
|
|
23974
|
-
/** Serialize JSONL file updates across processes and aliases using Eval's existing file lock.
|
|
23975
|
-
* The caller owns validation and the record format. The lock path names the canonical file,
|
|
23976
|
-
* including its first write, so two directory aliases cannot acquire different write heads.
|
|
23977
|
-
* @internal
|
|
23978
|
-
*/
|
|
23979
|
-
async function withJsonlFileLock(path, operation) {
|
|
23980
|
-
const fs = await import("node:fs/promises");
|
|
23981
|
-
const { dirname } = await import("node:path");
|
|
23982
|
-
const { tryAcquireAtomicFileLock } = await import("@tangle-network/agent-eval/ledger-core");
|
|
23983
|
-
await fs.mkdir(dirname(path), { recursive: true });
|
|
23984
|
-
await (await fs.open(path, "a")).close();
|
|
23985
|
-
const canonicalPath = await fs.realpath(path);
|
|
23986
|
-
const deadline = Date.now() + 5e3;
|
|
23987
|
-
for (;;) {
|
|
23988
|
-
const acquisition = tryAcquireAtomicFileLock({ lockPath: `${canonicalPath}.lock` });
|
|
23989
|
-
if (!acquisition.acquired) {
|
|
23990
|
-
if (Date.now() >= deadline) throw new Error(`journal file lock unavailable after 5000ms: ${canonicalPath}`);
|
|
23991
|
-
await new Promise((resolve) => setTimeout(resolve, 25));
|
|
23992
|
-
continue;
|
|
23993
|
-
}
|
|
23994
|
-
try {
|
|
23995
|
-
return await operation(canonicalPath);
|
|
23996
|
-
} finally {
|
|
23997
|
-
acquisition.lock.release();
|
|
23998
|
-
}
|
|
23999
|
-
}
|
|
24000
|
-
}
|
|
24001
|
-
/** Read one keyed registry snapshot without an extra whole-file text allocation.
|
|
24002
|
-
* Historical registry files allow whitespace-only separators; append journals remain strict.
|
|
24003
|
-
* @internal
|
|
24004
|
-
*/
|
|
24005
|
-
async function readCommittedJsonFile(path) {
|
|
24006
|
-
const records = [];
|
|
24007
|
-
for await (const record of readCommittedJsonLines(path, {
|
|
24008
|
-
allowMissing: true,
|
|
24009
|
-
allowWhitespaceLines: true
|
|
24010
|
-
})) records.push(record);
|
|
24011
|
-
return records;
|
|
24012
|
-
}
|
|
24013
|
-
/** Replace one keyed row and publish one durable snapshot. The caller holds the file lock.
|
|
24014
|
-
* Keep unrelated rows in order and put the replacement last. No revision archive is retained.
|
|
24015
|
-
* @internal
|
|
24016
|
-
*/
|
|
24017
|
-
async function upsertCommittedJsonLine(path, record, key) {
|
|
24018
|
-
const encoded = JSON.stringify(record);
|
|
24019
|
-
if (encoded === void 0) throw new Error("JSONL records must be JSON serializable");
|
|
24020
|
-
const snapshot = JSON.parse(encoded);
|
|
24021
|
-
const id = key(snapshot);
|
|
24022
|
-
const updated = [];
|
|
24023
|
-
for await (const prior of readCommittedJsonLines(path, {
|
|
24024
|
-
allowMissing: true,
|
|
24025
|
-
allowWhitespaceLines: true
|
|
24026
|
-
})) if (key(prior) !== id) updated.push(prior);
|
|
24027
|
-
updated.push(snapshot);
|
|
24028
|
-
const { writeAtomicDurableFile } = await import("./record-events-B2GUgYdz.js").then((n) => n.i);
|
|
24029
|
-
writeAtomicDurableFile(path, `${updated.map((row) => JSON.stringify(row)).join("\n")}\n`, { preserveMode: true });
|
|
24030
|
-
}
|
|
24031
|
-
/** Recover a torn tail under the same generation fence as durable publications. @internal */
|
|
24032
|
-
async function prepareJsonlAppend(path) {
|
|
24033
|
-
return fenceRunWrite(() => prepareJsonlTail(path));
|
|
24034
|
-
}
|
|
24035
|
-
/** True for a filesystem error meaning "the file is not there yet" — the one condition an
|
|
24036
|
-
* append-only reader treats as an empty log rather than a fault. */
|
|
24037
|
-
function isNoEntError(error) {
|
|
24038
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
24039
|
-
}
|
|
24040
20369
|
// src/durable/spawn-journal/result-blob-store.ts
|
|
24041
20370
|
/**
|
|
24042
20371
|
* FS `ResultBlobStore`. One JSON file per artifact under `dir`, named by a
|
|
@@ -24360,7 +20689,8 @@ async function loadSpawnForest(journal, root) {
|
|
|
24360
20689
|
if (visited.has(current.root)) throw new Error(`loadSpawnForest: recursive journal tree '${current.root}' was reached twice`);
|
|
24361
20690
|
visited.add(current.root);
|
|
24362
20691
|
const stableEvents = detachedSnapshot(current.events, `loadSpawnForest tree ${JSON.stringify(current.root)}`);
|
|
24363
|
-
const
|
|
20692
|
+
const viewEvents = current.ownerSpawn === void 0 || stableEvents.some((event) => event.kind === "spawned" && event.id === current.ownerSpawn?.id) ? [...stableEvents] : [ownedTreeRootSpawn(current.ownerSpawn), ...stableEvents];
|
|
20693
|
+
const view = detachedSnapshot(materializeTreeView(viewEvents), `loadSpawnForest view ${JSON.stringify(current.root)}`);
|
|
24364
20694
|
trees.push({
|
|
24365
20695
|
root: current.root,
|
|
24366
20696
|
...current.ownerNodeId === void 0 ? {} : { ownerNodeId: current.ownerNodeId },
|
|
@@ -28205,166 +24535,6 @@ function createChildRetention({ args, controller, id, ordinal, identity, recover
|
|
|
28205
24535
|
function overBudget(budget, spent) {
|
|
28206
24536
|
return chargedTokens(spent.tokens) > budget.maxTokens || spent.iterations > budget.maxIterations || budget.maxUsd !== void 0 && spent.usdKnown !== false && spent.usd > budget.maxUsd;
|
|
28207
24537
|
}
|
|
28208
|
-
// src/durable/spawn-journal/journal-store.ts
|
|
28209
|
-
/**
|
|
28210
|
-
* Append every event as one grouped publication where the journal supports it (`appendEvents`),
|
|
28211
|
-
* preserving legacy per-event appends otherwise — the SQL contexts' fenced head advances once.
|
|
28212
|
-
* @internal
|
|
28213
|
-
*/
|
|
28214
|
-
async function appendSpawnEvents(journal, root, events) {
|
|
28215
|
-
if (journal.appendEvents !== void 0) return journal.appendEvents(root, events);
|
|
28216
|
-
for (const event of events) await journal.appendEvent(root, event);
|
|
28217
|
-
}
|
|
28218
|
-
/**
|
|
28219
|
-
* JSONL on disk. One line per record: the first record is `begin`, subsequent records
|
|
28220
|
-
* are `event` or atomic `events` envelopes. `loadTree` replays the whole file,
|
|
28221
|
-
* filtering by `root`, and applies the same begin-precedes-events + unique-seq
|
|
28222
|
-
* corruption guards as the in-memory impl. Each append fsyncs so a crash between
|
|
28223
|
-
* writes never loses an acknowledged event.
|
|
28224
|
-
*
|
|
28225
|
-
* @stable
|
|
28226
|
-
*/
|
|
28227
|
-
var FileSpawnJournal = class {
|
|
28228
|
-
path;
|
|
28229
|
-
writeContext = captureRunWriteContext();
|
|
28230
|
-
appendTail = Promise.resolve();
|
|
28231
|
-
appendIndex;
|
|
28232
|
-
constructor(path) {
|
|
28233
|
-
this.path = path;
|
|
28234
|
-
}
|
|
28235
|
-
async loadTree(root) {
|
|
28236
|
-
let begun = false;
|
|
28237
|
-
const events = [];
|
|
28238
|
-
const index = new SpawnEventIndex(root);
|
|
28239
|
-
for await (const record of readCommittedJsonLines(this.path, { allowMissing: true })) {
|
|
28240
|
-
if (record.root !== root) continue;
|
|
28241
|
-
if (record.kind === "begin") begun = true;
|
|
28242
|
-
else {
|
|
28243
|
-
if (!begun) throw new Error(`spawn journal corrupted: event for tree '${root}' precedes its begin record`);
|
|
28244
|
-
for (const event of spawnJournalRecordEvents(record)) {
|
|
28245
|
-
index.assert(event);
|
|
28246
|
-
index.add(event);
|
|
28247
|
-
events.push(event);
|
|
28248
|
-
}
|
|
28249
|
-
}
|
|
28250
|
-
}
|
|
28251
|
-
return begun ? events : void 0;
|
|
28252
|
-
}
|
|
28253
|
-
async beginTree(root, at) {
|
|
28254
|
-
return this.serializeAppend(async () => {
|
|
28255
|
-
const state = await this.validationIndex();
|
|
28256
|
-
const existing = state.trees.get(root);
|
|
28257
|
-
if (existing) {
|
|
28258
|
-
if (existing.begunAt !== at) throw new Error(`spawn tree '${root}' already begun in ${this.path} at ${existing.begunAt}; refusing to overwrite with ${at}`);
|
|
28259
|
-
return;
|
|
28260
|
-
}
|
|
28261
|
-
state.stamp = await this.writeRecord({
|
|
28262
|
-
kind: "begin",
|
|
28263
|
-
root,
|
|
28264
|
-
at
|
|
28265
|
-
});
|
|
28266
|
-
state.trees.set(root, {
|
|
28267
|
-
begunAt: at,
|
|
28268
|
-
index: new SpawnEventIndex(root)
|
|
28269
|
-
});
|
|
28270
|
-
});
|
|
28271
|
-
}
|
|
28272
|
-
async appendEvent(root, ev) {
|
|
28273
|
-
const event = detachedSnapshot(ev, "spawn event");
|
|
28274
|
-
return this.serializeAppend(async () => {
|
|
28275
|
-
const state = await this.validationIndex();
|
|
28276
|
-
const tree = state.trees.get(root);
|
|
28277
|
-
if (tree === void 0) throw new Error(`appendEvent called for unknown spawn tree '${root}'; call beginTree first`);
|
|
28278
|
-
tree.index.assert(event);
|
|
28279
|
-
state.stamp = await this.writeRecord({
|
|
28280
|
-
kind: "event",
|
|
28281
|
-
root,
|
|
28282
|
-
event
|
|
28283
|
-
});
|
|
28284
|
-
tree.index.add(event);
|
|
28285
|
-
});
|
|
28286
|
-
}
|
|
28287
|
-
async appendEvents(root, events) {
|
|
28288
|
-
const captured = detachedSnapshot(events, "spawn events");
|
|
28289
|
-
return this.serializeAppend(async () => {
|
|
28290
|
-
const state = await this.validationIndex();
|
|
28291
|
-
const tree = state.trees.get(root);
|
|
28292
|
-
if (tree === void 0) throw new Error(`appendEvents called for unknown spawn tree '${root}'; call beginTree first`);
|
|
28293
|
-
try {
|
|
28294
|
-
for (const event of captured) {
|
|
28295
|
-
tree.index.assert(event);
|
|
28296
|
-
tree.index.add(event);
|
|
28297
|
-
}
|
|
28298
|
-
state.stamp = await this.writeRecord({
|
|
28299
|
-
kind: "events",
|
|
28300
|
-
root,
|
|
28301
|
-
events: captured
|
|
28302
|
-
});
|
|
28303
|
-
} catch (error) {
|
|
28304
|
-
this.appendIndex = void 0;
|
|
28305
|
-
throw error;
|
|
28306
|
-
}
|
|
28307
|
-
});
|
|
28308
|
-
}
|
|
28309
|
-
async validationIndex() {
|
|
28310
|
-
const stamp = await this.fileStamp();
|
|
28311
|
-
if (this.appendIndex && this.appendIndex.stamp === stamp) return this.appendIndex;
|
|
28312
|
-
this.appendIndex = void 0;
|
|
28313
|
-
const trees = /* @__PURE__ */ new Map();
|
|
28314
|
-
if (stamp !== void 0) for await (const record of readCommittedJsonLines(this.path)) if (record.kind === "begin") {
|
|
28315
|
-
if (!trees.has(record.root)) trees.set(record.root, {
|
|
28316
|
-
begunAt: record.at,
|
|
28317
|
-
index: new SpawnEventIndex(record.root)
|
|
28318
|
-
});
|
|
28319
|
-
} else {
|
|
28320
|
-
const tree = trees.get(record.root);
|
|
28321
|
-
if (!tree) throw new Error(`spawn journal corrupted: event for tree '${record.root}' precedes its begin record`);
|
|
28322
|
-
for (const event of spawnJournalRecordEvents(record)) {
|
|
28323
|
-
tree.index.assert(event);
|
|
28324
|
-
tree.index.add(event);
|
|
28325
|
-
}
|
|
28326
|
-
}
|
|
28327
|
-
if (await this.fileStamp() !== stamp) throw new Error("spawn journal changed while rebuilding its append index");
|
|
28328
|
-
this.appendIndex = {
|
|
28329
|
-
stamp,
|
|
28330
|
-
trees
|
|
28331
|
-
};
|
|
28332
|
-
return this.appendIndex;
|
|
28333
|
-
}
|
|
28334
|
-
async fileStamp() {
|
|
28335
|
-
const fs = await import("node:fs/promises");
|
|
28336
|
-
try {
|
|
28337
|
-
return journalFileStamp(await fs.stat(this.path, { bigint: true }));
|
|
28338
|
-
} catch (error) {
|
|
28339
|
-
if (isNoEntError(error)) return void 0;
|
|
28340
|
-
throw error;
|
|
28341
|
-
}
|
|
28342
|
-
}
|
|
28343
|
-
async serializeAppend(operation) {
|
|
28344
|
-
const append = this.appendTail.then(() => this.writeContext(operation));
|
|
28345
|
-
this.appendTail = append.catch(() => void 0);
|
|
28346
|
-
return append;
|
|
28347
|
-
}
|
|
28348
|
-
async writeRecord(record) {
|
|
28349
|
-
const fs = await import("node:fs/promises");
|
|
28350
|
-
const { dirname } = await import("node:path");
|
|
28351
|
-
try {
|
|
28352
|
-
await fs.mkdir(dirname(this.path), { recursive: true });
|
|
28353
|
-
return await appendCommittedJsonLine(this.path, record, (fd) => {
|
|
28354
|
-
const stamp = journalFileStamp(fstatSync(fd, { bigint: true }));
|
|
28355
|
-
if (journalFileStamp(statSync(this.path, { bigint: true })) !== stamp) throw new Error("spawn journal changed while acknowledging its append");
|
|
28356
|
-
return stamp;
|
|
28357
|
-
});
|
|
28358
|
-
} catch (error) {
|
|
28359
|
-
this.appendIndex = void 0;
|
|
28360
|
-
throw error;
|
|
28361
|
-
}
|
|
28362
|
-
}
|
|
28363
|
-
};
|
|
28364
|
-
/** Detect replacement, truncation, and same-size edits; this is not a cross-process write lock. */
|
|
28365
|
-
function journalFileStamp(stat) {
|
|
28366
|
-
return `${stat.dev}:${stat.ino}:${stat.size}:${stat.mtimeNs}:${stat.ctimeNs}`;
|
|
28367
|
-
}
|
|
28368
24538
|
// src/runtime/retained-interactive-handle.ts
|
|
28369
24539
|
function createRetainedInteractiveRunHandle(environment, inputRef, capabilities, requestedStart) {
|
|
28370
24540
|
const ref = freezeInteractiveRef(inputRef);
|
|
@@ -32683,24 +28853,25 @@ function wrapJournalForBreaker(journal, breaker) {
|
|
|
32683
28853
|
}
|
|
32684
28854
|
// src/runtime/supervise/supervisor-result.ts
|
|
32685
28855
|
/**
|
|
32686
|
-
* Normalize an arbitrary rejection into the serializable
|
|
28856
|
+
* Normalize an arbitrary rejection into the serializable evidence the result carries. A driver may
|
|
32687
28857
|
* reject with anything (a string, a plain object, a null-prototype value), and the whole point of
|
|
32688
28858
|
* this field is that the failure survives, so every branch produces a message instead of throwing:
|
|
32689
28859
|
* a stringification that itself throws (circular, hostile `toString`) falls back to the tag.
|
|
32690
28860
|
*/
|
|
32691
28861
|
function describeRejection(error) {
|
|
32692
|
-
|
|
32693
|
-
const stack = errorProperty(error, "stack");
|
|
32694
|
-
return {
|
|
32695
|
-
name: errorProperty(error, "name") ?? "Error",
|
|
32696
|
-
message: error instanceof DriverAttemptsExhaustedError ? errorProperty(error, "message") ?? "" : errMessage(error),
|
|
32697
|
-
...stack !== void 0 ? { stack } : {}
|
|
32698
|
-
};
|
|
32699
|
-
}
|
|
32700
|
-
return {
|
|
28862
|
+
const result = {
|
|
32701
28863
|
name: "NonError",
|
|
32702
|
-
message: errMessage(error)
|
|
28864
|
+
message: errMessage(error),
|
|
28865
|
+
failure: runtimeFailureCause(error)
|
|
32703
28866
|
};
|
|
28867
|
+
try {
|
|
28868
|
+
if (!(error instanceof Error)) return result;
|
|
28869
|
+
result.name = errorProperty(error, "name") ?? "Error";
|
|
28870
|
+
const stack = errorProperty(error, "stack");
|
|
28871
|
+
if (stack !== void 0) result.stack = stack;
|
|
28872
|
+
if (error instanceof DriverAttemptsExhaustedError) result.message = errorProperty(error, "message") ?? "";
|
|
28873
|
+
} catch {}
|
|
28874
|
+
return result;
|
|
32704
28875
|
}
|
|
32705
28876
|
/**
|
|
32706
28877
|
* The lifecycle cause of a no-winner, or `undefined` when the supervisor's own state proves
|
|
@@ -32753,7 +28924,8 @@ function assertResumeLimits(events, opts, reopenBudget) {
|
|
|
32753
28924
|
const roots = events.filter((event) => event.kind === "spawned" && event.parent === void 0);
|
|
32754
28925
|
if (roots.length !== 1 || roots[0]?.id !== opts.runId) throw new RuntimeRunStateError(`supervisor: resumed run '${opts.runId}' must contain exactly one root identity event; found ${roots.length}`);
|
|
32755
28926
|
const recorded = roots[0];
|
|
32756
|
-
|
|
28927
|
+
const reopenedBudget = reopenBudget ?? events.filter((event) => event.kind === "reopened" && event.id === opts.runId && event.budget !== void 0).at(-1)?.budget;
|
|
28928
|
+
if (contentAddress(reopenedBudget ?? recorded.budget) !== contentAddress(opts.budget)) throw new RuntimeRunStateError(`supervisor: resume budget mismatch for run '${opts.runId}'; use a new runId to change limits`);
|
|
32757
28929
|
const expectedAdmission = opts.reservationPolicy ? { policy: opts.reservationPolicy } : void 0;
|
|
32758
28930
|
if (contentAddress(recorded.recursiveAdmission ?? null) !== contentAddress(expectedAdmission ?? null)) throw new RuntimeRunStateError(`supervisor: resume reservation policy mismatch for run '${opts.runId}'; use a new runId`);
|
|
32759
28931
|
return recorded;
|
|
@@ -33470,6 +29642,6 @@ async function noWinner(settled, rejection) {
|
|
|
33470
29642
|
reason: "no-result-selected"
|
|
33471
29643
|
};
|
|
33472
29644
|
}
|
|
33473
|
-
export {
|
|
29645
|
+
export { abandonScopeRetainedOwnerInvocation as $, CheckUnavailableError as $n, newExecutionAttemptId as $r, harnessTranscriptArtifact as $t, noteScopeOwnerPlannedExecution as A, createPeerMailbox as An, harnessInvocation as Ar, acquireSandbox as At, deriveNodeExecutionIdentity as B, DriverAttemptsExhaustedError as Bn, renderUnsupported as Br, createAgentEnvironmentProviderRegistry as Bt, interactiveAdmissionSeamKey as C, INBOX_RECEIPT_KEY as Cn, signalAbortError as Cr, DEFAULT_SANDBOX_STEERING_MAX_TURNS as Ct, recoverRetainedInteractiveRun as D, DEFAULT_PEER_MAIL_LIMITS as Dn, runSettledCommand as Dr, readPromptOptions as Dt, reconnectRetainedInteractiveRun as E, AUTHORITY_MARKERS as En, defaultRunCommand as Er, assertBoxlessPromptOptions as Et, coordinationProfileToolPrefix as F, DEFAULT_STALL_AFTER_MS as Fn, defineProfileMaterializationContract as Fr, createCodeModeTools as Ft, meterRuntimeOwnedProviderAttempt as G, summarizeDriverAttempts as Gn, attestRuntimeOwnedPendingExecutor as Gr, reconnectRetainedRun as Gt, queueOf as H, abandonablePending as Hn, sandboxProfileMaterialization as Hr, providerAsExecutor as Ht, providerVisibleProfile as I, createActivityLog as In, fullProfileMaterialization as Ir, runCodeMode as It, recordScopeOwnerPause as J, SeatChain as Jn, authoredProfileDigest as Jr, startRetainedRunInEnvironment as Jt, recordScopeOwnerBarrenPause as K, undispatchedCredentialExit as Kn, attestRuntimeOwnedScopeOwner as Kr, recoverRetainedRun as Kt, runtimeGraphMetadataKey as L, readWorkerProgress as Ln, profileMaterializationAxes$1 as Lr, toolResultText as Lt, restoreScopeOwnerAcceptedExecution as M, peerMailTools as Mn, assertProfileMaterialization as Mr, defaultSelectWinner as Mt, scopeOwnerExecutorNodeContext as N, peerMailVerbNames as Nn, assertProfileMaterializes as Nr, codeModeArguments as Nt, startRetainedInteractiveRun as O, PEER_MAIL_WIRE_KEY as On, runWorktreeHarness as Or, createSandboxForSpec as Ot, coordinationMcpAlias as P, taskToPrompt$1 as Pn, controlProfileMaterialization as Pr, codeModeModelOutput as Pt, resolveSpawnResourcePaths as Q, subscriptionUsageLimitSignal as Qn, knownMaterializationReceipt as Qr, captureHarnessTranscript as Qt, settledToIteration as R, isLiveNodeStatus as Rn, promptControlProfileMaterialization as Rr, createRouterTranscript as Rt, workerTraceAnalysisStore as S, sanitizeRuntimeStreamEvent as Sn, runAbortable as Sr, snapshotExecutorConfig as St, readWorkerInteractiveBinding as T, failPendingAfter as Tn, bridgeStopSignalKey as Tr, runAgentRounds as Tt, rollingDispatch as U, causedByPartialFailure as Un, unsupportedProfileDimensions as Ur, LEAF_CONTINUATION_TASK as Ut, freeSlots as V, HarnessTurnFailedError as Vn, sandboxActProfileMaterialization as Vr, resolveAgentEnvironmentProvider as Vt, meterRuntimeOwnedAccounting as W, classifyDriverFailure as Wn, worktreeCliProfileMaterialization as Wr, providerAsSandboxClient as Wt, scopeRetainedOwnerResourceReader as X, authoredSeatStages as Xn, inheritRuntimeOwnedExecutorAttestation as Xr, destroyInteractiveEnvironment as Xt, prepareScopeRetainedOwnerSeatSwitch as Y, SeatChainExhaustedError as Yn, finalizeRuntimeOwnedPendingExecutor as Yr, retainedCreateMaterial as Yt, environmentReader as Z, seatExecutionProfile as Zn, knownExecutionBindingReceipt as Zr, environmentGone as Zt, withDriverExecutor as _, providerMessageText as _i, generateSpanId as _n, harnessUsageIsEmpty as _r, FileResultBlobStore as _t, assertRunParking as a, runtimeOwnedExecutorProviderEvidence as ai, openSlotGroup as an, admitContinuationPolicy as ar, scopeRetainedOwnerTurnEnded as at, isTraceAnalysisStore as b, createRuntimeStreamEventCollector as bn, RunCancellationReason as br, captureReusableExecutorConfig as bt, sleepScopeWait as c, unknownExecutionBindingReceipt as ci, WORKER_TRACE_PROPAGATION as cn, createContinuationKeeper as cr, noteScopeRetainedOwnerCoordination as ct, runFinalizer as d, executableAgentSpecSnapshot as di, buildLoopOtelSpans as dn, bridgeAdmissionRefusal as dr, scopeRetainedOwnerContext as dt, providerAttemptEvidence as ei, persistHarnessTranscript as en, gateFactoryOnDeliverable as er, consumeScopeRetainedOwnerResult as et, runTree as f, projectSandboxOutcome as fi, buildLoopSpanNodes as fn, bridgeModelRouteRefusal as fr, scopeRetainedOwnerJournalRoot as ft, isDriverSpec as g, promptOptionsFromAgentTurnInput as gi, flatOtelSpan as gn, createCodexRolloutStoreReader as gr, InMemorySpawnJournal as gt, driverExecutorFactory as h, promptFromAgentTurnInput as hi, createOtelExporter as hn, addHarnessUsage as hr, InMemoryResultBlobStore as ht, RunParkedError as i, runtimeOwnedExecutorMaterialization as ii, createWorkerSlots as in, CONTINUATIONS_DIR as ir, beginScopeRetainedOwnerDrive as it, recordScopeOwnerMaterialization as j, isPeerMailEnvelope as jn, terminateProcessTreeAndConfirm as jr, TERMINAL_DECISIONS as jt, beginScopeOwnerAttempt as k, claimsAuthority as kn, LOCAL_HARNESSES as kr, createSandboxLineage as kt, withRunParking as l, unknownMaterializationReceipt as li, createPropagatingTraceEmitter as ln, failedItems as lr, bindScopeRetainedOwnerEnvironmentId as lt, driverChild as m, awaitAbortable$1 as mi, createOpenInferenceFileExporter as mn, ROUTER_PRICE_SOURCE as mr, materializeTreeView as mt, createSupervisor as n, runtimeOwnedDriveHarnessProviderEvidence as ni, resolveHarnessTranscriptCapture as nn, mapExecutorResult as nr, scopeRetainedOwnerPriorSpend as nt, bindScopeParkingDrain as o, runtimeOwnedPendingExecutorMaterialization as oi, createWorktreeCliExecutor as on, bestComposite as or, reconcileScopeRetainedOwnerEnvironment as ot, createRootHandle as p, readSandboxOutcome as pi, buildRuntimeEventOtelSpans as pn, priceTokens as pr, loadSpawnForest as pt, recordScopeOwnerDriverAttempt as q, upstreamUnavailableSignal as qn, authoredAgentIdentity as qr, startRetainedRun as qt, assertResumeLimits as r, runtimeOwnedExecutorExecutionBinding as ri, retainHarnessTranscript as rn, verdictFromJudgeScore as rr, scopeRetainedOwnerResult as rt, readRunParking as s, runtimeOwnedScopeOwnerRuntime as si, createInPlaceCliExecutor as sn, checkVerdictOf as sr, scopeRetainedOwnerRestorePoint as st, DEFAULT_MAX_DEPTH as t, recordRuntimeOwnedDriveHarnessProviderEvidence as ti, readHarnessTranscript as tn, gateOnDeliverable as tr, prepareScopeRetainedOwnerTask as tt, bestDelivered as u, executableAgentProfileSnapshot as ui, readTraceContextFromEnv as un, isFiniteNumber as ur, bindScopeRetainedOwnerProvider as ut, createScope as v, loopEventToOtelSpan as vn, readCodexRolloutSession as vr, createExecutorRegistry as vt, attachWorker as w, createInbox as wn, bridgeRuntimeAttachmentsKey as wr, createSteerableSandboxSession as wt, parseWorkerToolTraceArtifact as x, sanitizeKnowledgeReadinessReport as xn, linkAbort as xr, createExecutor as xt, replaySpawnTree as y, createRuntimeEventCollector as yn, createPushTraceSource as yr, bindReusableExecutorExecutionId as yt, assertRecursiveReservationPolicy as z, isTerminalNodeStatus as zn, promptModelProfileMaterialization as zr, routerBrain as zt };
|
|
33474
29646
|
|
|
33475
|
-
//# sourceMappingURL=supervisor-
|
|
29647
|
+
//# sourceMappingURL=supervisor-C_sDmX5H.js.map
|