@smartmemory/stratum 0.3.4 → 0.4.1
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/cli/guard.js +63 -6
- package/dist/cli/guard.js.map +1 -1
- package/dist/cli/learn.js +226 -0
- package/dist/cli/learn.js.map +1 -0
- package/dist/cli/stratum.js +3 -1
- package/dist/cli/stratum.js.map +1 -1
- package/dist/connectors/background.js +53 -9
- package/dist/connectors/background.js.map +1 -1
- package/dist/connectors/base.js +16 -0
- package/dist/connectors/base.js.map +1 -1
- package/dist/connectors/cancellation.js +119 -0
- package/dist/connectors/cancellation.js.map +1 -0
- package/dist/connectors/claude-bg-worker.js +9 -1
- package/dist/connectors/claude-bg-worker.js.map +1 -1
- package/dist/connectors/claude.js +179 -86
- package/dist/connectors/claude.js.map +1 -1
- package/dist/connectors/codex.js +219 -62
- package/dist/connectors/codex.js.map +1 -1
- package/dist/connectors/runner.js +44 -3
- package/dist/connectors/runner.js.map +1 -1
- package/dist/contracts/events.json +34 -1
- package/dist/contracts/guard-signers.allowed +36 -0
- package/dist/contracts/mcp-surface.json +1085 -104
- package/dist/engine/checkpoint.js +8 -1
- package/dist/engine/checkpoint.js.map +1 -1
- package/dist/engine/engine.js +403 -42
- package/dist/engine/engine.js.map +1 -1
- package/dist/engine/ledger.js +12 -0
- package/dist/engine/ledger.js.map +1 -1
- package/dist/engine/receipts.js +91 -0
- package/dist/engine/receipts.js.map +1 -0
- package/dist/engine/state.js +51 -1
- package/dist/engine/state.js.map +1 -1
- package/dist/guard/authorization.js +71 -0
- package/dist/guard/authorization.js.map +1 -0
- package/dist/guard/descriptors.js +248 -0
- package/dist/guard/descriptors.js.map +1 -0
- package/dist/guard/errors.js +12 -0
- package/dist/guard/errors.js.map +1 -1
- package/dist/guard/evidence.js +24 -3
- package/dist/guard/evidence.js.map +1 -1
- package/dist/guard/sshsig.js +264 -0
- package/dist/guard/sshsig.js.map +1 -0
- package/dist/guard/store.js +13 -0
- package/dist/guard/store.js.map +1 -1
- package/dist/guard/transition.js +484 -33
- package/dist/guard/transition.js.map +1 -1
- package/dist/guard/trust.js +78 -0
- package/dist/guard/trust.js.map +1 -0
- package/dist/judge/judged.js +1 -1
- package/dist/judge/pricing.js +1 -0
- package/dist/judge/pricing.js.map +1 -1
- package/dist/learn/apply.js +600 -0
- package/dist/learn/apply.js.map +1 -0
- package/dist/learn/candidate.js +186 -0
- package/dist/learn/candidate.js.map +1 -0
- package/dist/learn/classify.js +181 -0
- package/dist/learn/classify.js.map +1 -0
- package/dist/learn/harvest.js +138 -0
- package/dist/learn/harvest.js.map +1 -0
- package/dist/learn/smartmemory_egress.js +330 -0
- package/dist/learn/smartmemory_egress.js.map +1 -0
- package/dist/mcp/server.js +150 -22
- package/dist/mcp/server.js.map +1 -1
- package/dist/policy/bundle.js +195 -0
- package/dist/policy/bundle.js.map +1 -0
- package/dist/policy/events.js +51 -0
- package/dist/policy/events.js.map +1 -0
- package/dist/policy/smartmemory_client.js +332 -0
- package/dist/policy/smartmemory_client.js.map +1 -0
- package/dist/policy/types.js +2 -0
- package/dist/policy/types.js.map +1 -0
- package/package.json +3 -2
package/dist/engine/engine.js
CHANGED
|
@@ -8,8 +8,13 @@ import { z } from "zod";
|
|
|
8
8
|
import { runAgent } from "../connectors/runner.js";
|
|
9
9
|
import { extractReferences } from "../ir/refs.js";
|
|
10
10
|
import { validateSpec } from "../ir/validate.js";
|
|
11
|
-
import {
|
|
11
|
+
import { LearnEgress } from "../learn/smartmemory_egress.js";
|
|
12
|
+
import { mergeBundleIntoSpec, policyRuleKey, predicateType, validateBundle } from "../policy/bundle.js";
|
|
13
|
+
import { buildFlowTerminalEvent, buildGateResolutionEvent } from "../policy/events.js";
|
|
14
|
+
import { emitPolicyEvent as postPolicyEvent } from "../policy/smartmemory_client.js";
|
|
15
|
+
import { BUDGET_KEYS, BudgetLedger, validConnectorTelemetry, validUsage } from "./ledger.js";
|
|
12
16
|
import { commitCheckpoint, revertCheckpoint } from "./checkpoint.js";
|
|
17
|
+
import { buildReceipt, findReceipt, ReceiptValidationError, spineSpent } from "./receipts.js";
|
|
13
18
|
import { StateStore } from "./state.js";
|
|
14
19
|
const execFileAsync = promisify(execFile);
|
|
15
20
|
/**
|
|
@@ -61,12 +66,17 @@ export class SpecValidationError extends Error {
|
|
|
61
66
|
this.errors = errors;
|
|
62
67
|
}
|
|
63
68
|
}
|
|
69
|
+
export class InputValidationError extends SpecValidationError {
|
|
70
|
+
constructor(errors) { super(errors); this.message = "entry input validation failed"; }
|
|
71
|
+
}
|
|
64
72
|
export class StratumEngine {
|
|
65
73
|
store;
|
|
66
74
|
evaluator;
|
|
67
75
|
judge;
|
|
68
76
|
evaluateRunner;
|
|
69
77
|
connector;
|
|
78
|
+
learnEgress;
|
|
79
|
+
learnEgressStartup;
|
|
70
80
|
// Serializes load-modify-save per run: plan may hand out several ready steps, so
|
|
71
81
|
// stepDone/resume can race in-process. The state root is owned by one engine process in v1.
|
|
72
82
|
runLocks = new Map();
|
|
@@ -87,6 +97,16 @@ export class StratumEngine {
|
|
|
87
97
|
if (options.evaluateRunner)
|
|
88
98
|
this.evaluateRunner = options.evaluateRunner;
|
|
89
99
|
this.connector = options.connector ?? defaultConnector;
|
|
100
|
+
this.learnEgress = options.learnEgress ?? new LearnEgress({
|
|
101
|
+
...options.learnEgressOptions,
|
|
102
|
+
store: this.store,
|
|
103
|
+
withReceiptUpdate: (runId, update) => this.withReceiptUpdate(runId, update),
|
|
104
|
+
});
|
|
105
|
+
this.learnEgressStartup = this.learnEgress.enabled()
|
|
106
|
+
? this.learnEgress.drainAll().catch((error) => {
|
|
107
|
+
console.warn(`SmartMemory egress startup reconciliation failed: ${message(error)}`);
|
|
108
|
+
})
|
|
109
|
+
: Promise.resolve();
|
|
90
110
|
}
|
|
91
111
|
async loadRun(runId) {
|
|
92
112
|
const active = this.activeRuns.get(runId);
|
|
@@ -120,27 +140,70 @@ export class StratumEngine {
|
|
|
120
140
|
});
|
|
121
141
|
return result;
|
|
122
142
|
}
|
|
143
|
+
async withReceiptUpdate(runId, update) {
|
|
144
|
+
return this.withRunLock(runId, async () => {
|
|
145
|
+
const run = await this.loadRun(runId);
|
|
146
|
+
const result = await update(run);
|
|
147
|
+
await this.persist(run);
|
|
148
|
+
return result;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
async closeLearnEgress() {
|
|
152
|
+
await this.learnEgressStartup;
|
|
153
|
+
await this.learnEgress.close();
|
|
154
|
+
}
|
|
123
155
|
async plan(specInput, input, options = {}) {
|
|
124
156
|
const validation = validateSpec(specInput);
|
|
125
157
|
if (!validation.ok)
|
|
126
158
|
throw new SpecValidationError(validation.errors);
|
|
127
|
-
|
|
128
|
-
|
|
159
|
+
let effectiveSpec = validation.value;
|
|
160
|
+
let policyFields = {};
|
|
161
|
+
if (options.policyBundle !== undefined) {
|
|
162
|
+
const bundle = validateBundle(options.policyBundle);
|
|
163
|
+
const merged = mergeBundleIntoSpec(effectiveSpec, bundle, options.policyStepSelector);
|
|
164
|
+
effectiveSpec = merged.spec;
|
|
165
|
+
policyFields = { bundle_id: merged.bundle_id, policy_rules: merged.policy_rules, policy_rules_version: 2, policy_verdicts: [] };
|
|
166
|
+
const policyBindings = Object.values(merged.policy_rules).flat();
|
|
167
|
+
const ruleCount = new Set(policyBindings.map((binding) => binding.rule_id)).size;
|
|
168
|
+
console.warn(`policy bundle ${merged.bundle_id}: ${ruleCount} rules bound to ${policyBindings.length} step-predicate pairs`);
|
|
169
|
+
if (bundle.rules.some((rule) => rule.bind.kind === "ensure" && rule.on_fail === "gate")) {
|
|
170
|
+
console.warn("ensure policy rule on_fail=gate is enforced as refuse in P1; gate routing is deferred to P3");
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Validate the policy-merged specification and entry input before allocating
|
|
174
|
+
// or persisting a run. A rejected request must never dispatch work.
|
|
175
|
+
const effectiveValidation = validateSpec(effectiveSpec);
|
|
176
|
+
if (!effectiveValidation.ok)
|
|
177
|
+
throw new SpecValidationError(effectiveValidation.errors);
|
|
178
|
+
effectiveSpec = effectiveValidation.value;
|
|
179
|
+
const flowName = effectiveSpec.flows.entry;
|
|
180
|
+
const parsedInput = effectiveValidation.inputs[flowName]?.safeParse(input);
|
|
181
|
+
if (!parsedInput)
|
|
182
|
+
throw new Error("entry flow input contract missing after validation");
|
|
183
|
+
if (!parsedInput.success) {
|
|
184
|
+
throw new InputValidationError(parsedInput.error.issues.map((issue) => ({
|
|
185
|
+
code: "INPUT_CONTRACT_INVALID",
|
|
186
|
+
path: ["input", ...issue.path].map((part, index) => typeof part === "number" ? `[${part}]` : index === 0 ? part : `.${part}`).join(""),
|
|
187
|
+
message: issue.message,
|
|
188
|
+
})));
|
|
189
|
+
}
|
|
190
|
+
const flow = effectiveSpec.flows[flowName];
|
|
129
191
|
if (!flow)
|
|
130
192
|
throw new Error("entry flow missing after validation");
|
|
131
193
|
const steps = Object.create(null);
|
|
132
194
|
for (const step of flow.steps)
|
|
133
195
|
steps[step.id] = { status: "pending", attempts: [], spent: {} };
|
|
134
196
|
const run = {
|
|
135
|
-
id: randomUUID(), spec:
|
|
136
|
-
input, flowName, status: "running", flowSpent: {}, steps,
|
|
197
|
+
id: randomUUID(), spec: effectiveSpec, revisionDigest: digest(effectiveSpec), generationCounter: 0,
|
|
198
|
+
input: parsedInput.data, flowName, status: "running", flowSpent: {}, steps,
|
|
137
199
|
events: [{ at: now(), type: "planned" }],
|
|
200
|
+
...policyFields,
|
|
138
201
|
// Canonicalize at plan time: a relative root must never re-resolve against a
|
|
139
202
|
// different process cwd after restart.
|
|
140
203
|
...(options.workspaceRoot !== undefined ? { workspaceRoot: resolve(options.workspaceRoot) } : {}),
|
|
141
204
|
};
|
|
142
205
|
await this.persist(run);
|
|
143
|
-
return this.withRevisionDigest(await this.advance(run,
|
|
206
|
+
return this.withRevisionDigest(await this.advance(run, effectiveValidation.value, effectiveValidation.contracts), run);
|
|
144
207
|
}
|
|
145
208
|
async flowRunBg(specInput, input, options = {}) {
|
|
146
209
|
const validation = validateSpec(specInput);
|
|
@@ -288,7 +351,9 @@ export class StratumEngine {
|
|
|
288
351
|
const usage = { ...reported };
|
|
289
352
|
delete usage.dispatches;
|
|
290
353
|
// "settle": the agent already ran, so over-limit usage is still recorded in both ledgers.
|
|
291
|
-
const budgetFailure =
|
|
354
|
+
const budgetFailure = hasBudget(usage)
|
|
355
|
+
? this.settleLegacyReceipt(run, usage, "step_done", telemetry, { scope, step, state }, result.usdSource, result.split)
|
|
356
|
+
: undefined;
|
|
292
357
|
if (budgetFailure === "flow") {
|
|
293
358
|
const failure = { attempt, reason: "flow budget exhausted" };
|
|
294
359
|
state.attempts.push({ attempt, at: now(), failure, ...telemetryFields(telemetry), ...(hasBudget(usage) ? { usage } : {}) });
|
|
@@ -364,6 +429,70 @@ export class StratumEngine {
|
|
|
364
429
|
await this.persist(run);
|
|
365
430
|
return this.advance(run, validated.value, validated.contracts, scope);
|
|
366
431
|
}
|
|
432
|
+
async usageReport(runId, input) {
|
|
433
|
+
return this.withRunLock(runId, async () => {
|
|
434
|
+
if (typeof input === "object" && input !== null && input.usdSource === "legacy") {
|
|
435
|
+
throw new ReceiptValidationError('usdSource "legacy" is reserved for engine-synthesized receipts');
|
|
436
|
+
}
|
|
437
|
+
const run = await this.loadRun(runId);
|
|
438
|
+
const candidate = input;
|
|
439
|
+
if (typeof input !== "object" || input === null || Array.isArray(input)
|
|
440
|
+
|| typeof candidate?.dispatchId !== "string" || candidate.dispatchId.length === 0) {
|
|
441
|
+
throw new ReceiptValidationError("dispatchId must be a non-empty string");
|
|
442
|
+
}
|
|
443
|
+
if (candidate.dispatchId.startsWith("legacy:")) {
|
|
444
|
+
throw new ReceiptValidationError('dispatchId prefix "legacy:" is reserved for engine-synthesized receipts');
|
|
445
|
+
}
|
|
446
|
+
if (candidate.dispatchId.startsWith("engine:")) {
|
|
447
|
+
throw new ReceiptValidationError('dispatchId prefix "engine:" is reserved for engine-synthesized receipts');
|
|
448
|
+
}
|
|
449
|
+
const duplicate = findReceipt(run, candidate.dispatchId);
|
|
450
|
+
if (duplicate !== undefined) {
|
|
451
|
+
return { status: "duplicate", runId: run.id, seq: duplicate.seq, ledger: this.ledgerInfo(run) };
|
|
452
|
+
}
|
|
453
|
+
// Validate and allocate against a staging copy so a rejected receipt cannot
|
|
454
|
+
// advance the live counter (important while an active fanout pins the run object).
|
|
455
|
+
const staged = { ...run };
|
|
456
|
+
const receipt = buildReceipt(staged, input);
|
|
457
|
+
const located = receipt.stepId === undefined
|
|
458
|
+
? undefined
|
|
459
|
+
: this.locateReceiptStep(run, this.validationFor(run).value, receipt.stepId);
|
|
460
|
+
if (receipt.stepId !== undefined && located === undefined) {
|
|
461
|
+
throw new ReceiptValidationError(`receipt step ${JSON.stringify(receipt.stepId)} does not exist`, "invalid_step");
|
|
462
|
+
}
|
|
463
|
+
run.receiptCounter = receipt.seq;
|
|
464
|
+
const wasRunning = run.status === "running";
|
|
465
|
+
const settled = this.settleReceipt(run, receipt, located);
|
|
466
|
+
if (settled.status === "duplicate") {
|
|
467
|
+
return { status: "duplicate", runId: run.id, seq: settled.receipt.seq, ledger: this.ledgerInfo(run) };
|
|
468
|
+
}
|
|
469
|
+
let budget;
|
|
470
|
+
if (settled.budget === "flow") {
|
|
471
|
+
budget = wasRunning ? "flow_exhausted" : "flow_exhausted_after_terminal";
|
|
472
|
+
if (wasRunning) {
|
|
473
|
+
const attempt = located?.item?.attempts.length ?? located?.state.attempts.length ?? 0;
|
|
474
|
+
await this.terminalBudget(run, { attempt: attempt + 1, reason: "flow budget exhausted" });
|
|
475
|
+
}
|
|
476
|
+
else {
|
|
477
|
+
await this.persist(run);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
else {
|
|
481
|
+
if (settled.budget === "subflow")
|
|
482
|
+
budget = "subflow_exhausted";
|
|
483
|
+
if (settled.budget === "task")
|
|
484
|
+
budget = "task_exhausted";
|
|
485
|
+
await this.persist(run);
|
|
486
|
+
}
|
|
487
|
+
return {
|
|
488
|
+
status: "ok",
|
|
489
|
+
runId: run.id,
|
|
490
|
+
seq: receipt.seq,
|
|
491
|
+
...(budget !== undefined ? { budget } : {}),
|
|
492
|
+
ledger: this.ledgerInfo(run),
|
|
493
|
+
};
|
|
494
|
+
});
|
|
495
|
+
}
|
|
367
496
|
async commit(runId, label) {
|
|
368
497
|
this.assertExternalMutationAllowed(runId, "commit");
|
|
369
498
|
return await this.withRunLock(runId, async () => {
|
|
@@ -392,12 +521,39 @@ export class StratumEngine {
|
|
|
392
521
|
const run = await this.loadCheckpointRun(runId);
|
|
393
522
|
this.assertNoForegroundFanout(run, "revert");
|
|
394
523
|
const normalized = label.trim();
|
|
524
|
+
// Money spent is spent: a revert restores state, never spend. Capture the live
|
|
525
|
+
// cumulative total before the snapshot overwrites it.
|
|
526
|
+
const liveSpentBeforeRevert = { ...run.flowSpent };
|
|
395
527
|
if (!revertCheckpoint(run, normalized)) {
|
|
396
528
|
// Insertion order, matching Python (list(state.checkpoints.keys())) and the commit
|
|
397
529
|
// envelope's `checkpoints` — not sorted, and robust to numeric labels (array, not object).
|
|
398
530
|
const available = (run.checkpoints ?? []).map((entry) => entry.label);
|
|
399
531
|
throw new CheckpointOperationError("checkpoint_not_found", `No checkpoint '${normalized}' on flow '${runId}'`, available);
|
|
400
532
|
}
|
|
533
|
+
const receiptsAtRevert = run.receiptCounter ?? 0;
|
|
534
|
+
const stepsRestored = Object.keys(run.steps);
|
|
535
|
+
// flowSpent is monotonic across reverts: the live pre-revert total already
|
|
536
|
+
// includes every receipt (spine) plus any pre-receipt legacy spend, so it is
|
|
537
|
+
// the correct value in both the receipt-era and the upgraded-mid-run case.
|
|
538
|
+
// The spine is kept as a floor (defense against a corrupted live total); the
|
|
539
|
+
// restored snapshot is never consulted for spend.
|
|
540
|
+
const spine = spineSpent(run);
|
|
541
|
+
const reconciled = {};
|
|
542
|
+
for (const key of BUDGET_KEYS) {
|
|
543
|
+
const value = Math.max(liveSpentBeforeRevert[key] ?? 0, spine[key] ?? 0);
|
|
544
|
+
if (value !== 0)
|
|
545
|
+
reconciled[key] = value;
|
|
546
|
+
}
|
|
547
|
+
run.flowSpent = reconciled;
|
|
548
|
+
const detail = { label: normalized, receiptsAtRevert, stepsRestored };
|
|
549
|
+
this.event(run, "checkpoint_reverted", undefined, detail);
|
|
550
|
+
const checkpointReceiptSeq = receiptsAtRevert + 1;
|
|
551
|
+
(run.receipts ??= []).push(buildReceipt(run, {
|
|
552
|
+
dispatchId: `engine:checkpoint_reverted:${checkpointReceiptSeq}`,
|
|
553
|
+
source: "engine",
|
|
554
|
+
usage: {},
|
|
555
|
+
detail,
|
|
556
|
+
}));
|
|
401
557
|
this.rotateRestoredIssuances(run);
|
|
402
558
|
await this.persist(run);
|
|
403
559
|
return { ...await this.reAdvanceLocked(runId), reverted_to: normalized };
|
|
@@ -446,7 +602,10 @@ export class StratumEngine {
|
|
|
446
602
|
async flowPoll(runId, cursor = 0) {
|
|
447
603
|
if (!Number.isInteger(cursor) || cursor < 0)
|
|
448
604
|
throw new Error("invalid event cursor");
|
|
449
|
-
|
|
605
|
+
// Active fanouts pin a mutable run and set terminal status before save()
|
|
606
|
+
// finishes. Observers must see only committed state; otherwise "completed"
|
|
607
|
+
// can race the final write/rename and disagree with a restarted engine.
|
|
608
|
+
const run = await this.store.load(runId);
|
|
450
609
|
return {
|
|
451
610
|
runId,
|
|
452
611
|
status: run.status,
|
|
@@ -458,18 +617,19 @@ export class StratumEngine {
|
|
|
458
617
|
};
|
|
459
618
|
}
|
|
460
619
|
async flowBgPoll(runId, cursor = 0) {
|
|
461
|
-
const flow = await this.flowPoll(runId, cursor);
|
|
462
620
|
const bg = this.bgFlows.get(runId);
|
|
463
621
|
if (!bg)
|
|
464
622
|
throw new Error(`background flow ${runId} not found`);
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
623
|
+
// Capture driver state before reading disk. A terminal driver status is set
|
|
624
|
+
// only after persistence, so this ordering cannot pair bg.completed with an
|
|
625
|
+
// older running snapshot when the driver finishes during the asynchronous read.
|
|
626
|
+
const driver = {
|
|
627
|
+
status: bg.status,
|
|
628
|
+
cancelRequested: bg.cancelRequested,
|
|
629
|
+
pendingGates: [...bg.pendingGates],
|
|
472
630
|
};
|
|
631
|
+
const flow = await this.flowPoll(runId, cursor);
|
|
632
|
+
return { ...flow, bg: driver };
|
|
473
633
|
}
|
|
474
634
|
async flowCancelBg(runId) {
|
|
475
635
|
const bg = this.bgFlows.get(runId);
|
|
@@ -494,8 +654,20 @@ export class StratumEngine {
|
|
|
494
654
|
}
|
|
495
655
|
return { status: bg.status };
|
|
496
656
|
}
|
|
497
|
-
async gateResolve(runId, stepId, decision, gateToken) {
|
|
657
|
+
async gateResolve(runId, stepId, decision, gateToken, userId) {
|
|
498
658
|
const response = await this.withRunLock(runId, () => this.gateResolveLocked(runId, stepId, decision, gateToken));
|
|
659
|
+
const resolvedRun = await this.loadRun(runId);
|
|
660
|
+
if (resolvedRun.bundle_id !== undefined) {
|
|
661
|
+
const round = resolvedRun.events.filter((event) => event.type === "gate_resolved" && event.stepId === stepId).length;
|
|
662
|
+
this.firePolicyEvent(buildGateResolutionEvent({
|
|
663
|
+
runId,
|
|
664
|
+
bundleId: resolvedRun.bundle_id,
|
|
665
|
+
stepId,
|
|
666
|
+
round,
|
|
667
|
+
outcome: decision,
|
|
668
|
+
...(userId !== undefined ? { resolvedByUserId: userId } : {}),
|
|
669
|
+
}));
|
|
670
|
+
}
|
|
499
671
|
const bg = this.bgFlows.get(runId);
|
|
500
672
|
if (bg?.status === "paused_gate" && response.status !== "ready" && response.status !== "running") {
|
|
501
673
|
bg.status = response.status;
|
|
@@ -567,7 +739,7 @@ export class StratumEngine {
|
|
|
567
739
|
scope.parent.state.sub.rounds = total;
|
|
568
740
|
else
|
|
569
741
|
run.rounds = total;
|
|
570
|
-
this.resetFrom(
|
|
742
|
+
this.resetFrom(run, scope, target);
|
|
571
743
|
// The target's descendants include this gate; retain its local revision counter.
|
|
572
744
|
scope.steps[step.id].iterations = gateRounds + 1;
|
|
573
745
|
await this.persist(run);
|
|
@@ -595,7 +767,7 @@ export class StratumEngine {
|
|
|
595
767
|
return this.advance(run, validated.value, validated.contracts, scope);
|
|
596
768
|
}
|
|
597
769
|
async completeTerminalGate(run, flow, contracts) {
|
|
598
|
-
const output = this.resolveFlowOutput({ input: run.input, steps: run.steps, flow });
|
|
770
|
+
const output = this.resolveFlowOutput({ input: run.input, steps: run.steps, flow, flowName: run.flowName });
|
|
599
771
|
const parsed = contracts[flow.output.contract]?.safeParse(output);
|
|
600
772
|
if (!parsed?.success)
|
|
601
773
|
return this.terminalFailure(run, { attempt: 0, reason: parsed?.error.message ?? "flow output contract missing" });
|
|
@@ -603,6 +775,7 @@ export class StratumEngine {
|
|
|
603
775
|
run.status = "completed";
|
|
604
776
|
this.event(run, "completed", undefined, { output });
|
|
605
777
|
await this.persist(run);
|
|
778
|
+
this.emitFlowTerminal(run);
|
|
606
779
|
return this.response(run);
|
|
607
780
|
}
|
|
608
781
|
/** Re-derive a run's response after async fanout/subflow progress without
|
|
@@ -772,6 +945,7 @@ export class StratumEngine {
|
|
|
772
945
|
run.status = "completed";
|
|
773
946
|
this.event(run, "completed", undefined, { output });
|
|
774
947
|
await this.persist(run);
|
|
948
|
+
this.emitFlowTerminal(run);
|
|
775
949
|
return this.response(run);
|
|
776
950
|
}
|
|
777
951
|
return this.failScope(run, spec, contracts, scope, "no runnable steps remain");
|
|
@@ -1465,7 +1639,11 @@ export class StratumEngine {
|
|
|
1465
1639
|
const usage = { ...(result.usage ?? {}) };
|
|
1466
1640
|
const reportedDispatches = usage.dispatches !== undefined;
|
|
1467
1641
|
delete usage.dispatches;
|
|
1468
|
-
const settled =
|
|
1642
|
+
const settled = hasBudget(usage)
|
|
1643
|
+
? this.settleLegacyReceipt(run, usage, "fanout", result.telemetry, {
|
|
1644
|
+
scope: this.rootScope(run, spec), step, state, item,
|
|
1645
|
+
}, result.usdSource, result.split)
|
|
1646
|
+
: undefined;
|
|
1469
1647
|
if (hasBudget(usage))
|
|
1470
1648
|
this.event(run, "fanout_ledger_debit", step.id, { itemIndex: item.index, amount: usage });
|
|
1471
1649
|
const stageStep = { ...step, do: stage.do, out: stage.out, ensure: stage.ensure, budget: step.budget };
|
|
@@ -1587,7 +1765,7 @@ export class StratumEngine {
|
|
|
1587
1765
|
}
|
|
1588
1766
|
/** Evaluates a step's ensure list in order; the first failing predicate wins. */
|
|
1589
1767
|
async runEnsures(run, step, state, output, scope = this.rootScope(run, this.validationFor(run).value), fanoutItem) {
|
|
1590
|
-
for (const predicate of step.ensure ?? []) {
|
|
1768
|
+
for (const [ensureIndex, predicate] of (step.ensure ?? []).entries()) {
|
|
1591
1769
|
if ("judged" in predicate) {
|
|
1592
1770
|
const { statement, stakes } = predicate.judged;
|
|
1593
1771
|
// The runner is an injected seam — validate its outcome; a malformed shape
|
|
@@ -1626,12 +1804,24 @@ export class StratumEngine {
|
|
|
1626
1804
|
failureReason = `judged predicate failed: ${message(error)}`;
|
|
1627
1805
|
}
|
|
1628
1806
|
}
|
|
1629
|
-
const usage = outcome?.usage ?? {};
|
|
1630
|
-
const
|
|
1807
|
+
const usage = { ...(outcome?.usage ?? {}) };
|
|
1808
|
+
const judgedUsageAsReported = { ...usage };
|
|
1809
|
+
// A judge may report `dispatches`; that key is ledger-only (never a receipt)
|
|
1810
|
+
// and keeps its pre-receipt settle semantics.
|
|
1811
|
+
const judgedDispatches = usage.dispatches !== undefined ? { dispatches: usage.dispatches } : undefined;
|
|
1812
|
+
delete usage.dispatches;
|
|
1813
|
+
const receiptItem = fanoutItem === undefined ? undefined : state.fanout?.items[fanoutItem.itemIndex];
|
|
1814
|
+
const dispatchFailure = judgedDispatches !== undefined ? this.debit(run, step, state, judgedDispatches, "settle", scope) : undefined;
|
|
1815
|
+
const costFailure = hasBudget(usage)
|
|
1816
|
+
? this.settleLegacyReceipt(run, usage, "judged", outcome?.model === undefined ? undefined : {
|
|
1817
|
+
model: outcome.model.length > 0 ? outcome.model : "unknown", durationMs: 0,
|
|
1818
|
+
}, { scope, step, state, ...(receiptItem !== undefined ? { item: receiptItem } : {}) })
|
|
1819
|
+
: undefined;
|
|
1820
|
+
const budgetFailure = worstBudget(dispatchFailure, costFailure);
|
|
1631
1821
|
// A judged debit inside a fanout item must stay visible per item — the
|
|
1632
1822
|
// observability contract forbids anonymous ledger movement.
|
|
1633
|
-
if (fanoutItem && hasBudget(
|
|
1634
|
-
this.event(run, "fanout_ledger_debit", step.id, { itemIndex: fanoutItem.itemIndex, amount:
|
|
1823
|
+
if (fanoutItem && hasBudget(judgedUsageAsReported)) {
|
|
1824
|
+
this.event(run, "fanout_ledger_debit", step.id, { itemIndex: fanoutItem.itemIndex, amount: judgedUsageAsReported, source: "judged" });
|
|
1635
1825
|
}
|
|
1636
1826
|
// Fixed audit payload — every judged evaluation events, failures included.
|
|
1637
1827
|
this.event(run, "judged", this.scopedId(scope, step.id), {
|
|
@@ -1644,6 +1834,7 @@ export class StratumEngine {
|
|
|
1644
1834
|
usage: { tokens: usage.tokens ?? 0, usd: usage.usd ?? 0 },
|
|
1645
1835
|
...(fanoutItem ? { itemIndex: fanoutItem.itemIndex, stage: fanoutItem.stage } : {}),
|
|
1646
1836
|
});
|
|
1837
|
+
this.recordPolicyVerdict(run, scope.flowName, step.id, ensureIndex, failureReason === undefined && outcome?.holds === true, "judged");
|
|
1647
1838
|
if (budgetFailure === "flow")
|
|
1648
1839
|
return { kind: "flow_budget" };
|
|
1649
1840
|
if (budgetFailure === "subflow")
|
|
@@ -1663,6 +1854,7 @@ export class StratumEngine {
|
|
|
1663
1854
|
? `file_exists(${JSON.stringify(predicate.file_exists)})`
|
|
1664
1855
|
: `file_contains(${JSON.stringify(predicate.file_contains.path)}, ${JSON.stringify(predicate.file_contains.text)})`;
|
|
1665
1856
|
const verdict = this.ensurePredicate(expression, run, output, scope, fanoutItem);
|
|
1857
|
+
this.recordPolicyVerdict(run, scope.flowName, step.id, ensureIndex, verdict.holds, predicateType(predicate));
|
|
1666
1858
|
if (!verdict.holds)
|
|
1667
1859
|
return { kind: "fail", reason: `ensure ${JSON.stringify(expression)} failed: ${verdict.reason}` };
|
|
1668
1860
|
}
|
|
@@ -1727,6 +1919,90 @@ export class StratumEngine {
|
|
|
1727
1919
|
return "task";
|
|
1728
1920
|
return undefined;
|
|
1729
1921
|
}
|
|
1922
|
+
settleReceipt(run, receipt, located, explicitAttempt) {
|
|
1923
|
+
const duplicate = findReceipt(run, receipt.dispatchId);
|
|
1924
|
+
if (duplicate !== undefined)
|
|
1925
|
+
return { status: "duplicate", receipt: duplicate };
|
|
1926
|
+
const canonicalStepId = located === undefined ? undefined : this.scopedId(located.scope, located.step.id);
|
|
1927
|
+
if (canonicalStepId !== undefined)
|
|
1928
|
+
receipt.stepId = canonicalStepId;
|
|
1929
|
+
let budget;
|
|
1930
|
+
const executable = located !== undefined && (located.step.do !== undefined || located.step.fanout !== undefined);
|
|
1931
|
+
if (executable) {
|
|
1932
|
+
budget = this.debit(run, located.step, located.state, receipt.amount, "settle", located.scope);
|
|
1933
|
+
}
|
|
1934
|
+
else {
|
|
1935
|
+
const flowLedger = new BudgetLedger(this.flowFor(run, this.validationFor(run).value).budget, run.flowSpent);
|
|
1936
|
+
const subflowLedger = located?.scope.parent
|
|
1937
|
+
? new BudgetLedger(located.scope.parent.step.budget, located.scope.parent.state.spent)
|
|
1938
|
+
: undefined;
|
|
1939
|
+
const flowOk = flowLedger.canDebit(receipt.amount);
|
|
1940
|
+
const subflowOk = subflowLedger?.canDebit(receipt.amount) ?? true;
|
|
1941
|
+
flowLedger.debit(receipt.amount);
|
|
1942
|
+
subflowLedger?.debit(receipt.amount);
|
|
1943
|
+
Object.assign(run.flowSpent, flowLedger.spent);
|
|
1944
|
+
if (subflowLedger && located?.scope.parent)
|
|
1945
|
+
Object.assign(located.scope.parent.state.spent, subflowLedger.spent);
|
|
1946
|
+
if (!flowOk)
|
|
1947
|
+
budget = "flow";
|
|
1948
|
+
else if (!subflowOk)
|
|
1949
|
+
budget = "subflow";
|
|
1950
|
+
}
|
|
1951
|
+
(run.receipts ??= []).push(receipt);
|
|
1952
|
+
// `attempt` names the attempt the call belongs to. Legacy settles pass it (the
|
|
1953
|
+
// attempt record is pushed after settlement, so length+1 is that attempt);
|
|
1954
|
+
// external receipts get it only while the step is awaiting a result. Gates
|
|
1955
|
+
// and finished steps have no attempt to name.
|
|
1956
|
+
const attempt = explicitAttempt
|
|
1957
|
+
?? (executable && (located?.item !== undefined
|
|
1958
|
+
? (located.item.status === "ready" || located.item.status === "running")
|
|
1959
|
+
: located?.state.status === "ready")
|
|
1960
|
+
? (located?.item !== undefined ? located.item.attempts.length + 1 : located.state.attempts.length + 1)
|
|
1961
|
+
: undefined);
|
|
1962
|
+
const item = located?.item;
|
|
1963
|
+
const detail = {
|
|
1964
|
+
...(located?.state.epoch !== undefined ? { epoch: located.state.epoch } : {}),
|
|
1965
|
+
...(attempt !== undefined ? { attempt } : {}),
|
|
1966
|
+
...(item?.stage !== undefined ? { item: { itemIndex: item.index, stage: item.stage, generation: item.generation } } : {}),
|
|
1967
|
+
};
|
|
1968
|
+
// One detail object serves both the audit event and the receipt row, so the
|
|
1969
|
+
// SmartMemory mirror carries exactly what the event stream carries.
|
|
1970
|
+
const eventDetail = {
|
|
1971
|
+
seq: receipt.seq,
|
|
1972
|
+
dispatchId: receipt.dispatchId,
|
|
1973
|
+
source: receipt.source,
|
|
1974
|
+
amount: { ...receipt.amount },
|
|
1975
|
+
model: receipt.telemetry.model,
|
|
1976
|
+
...(receipt.telemetry.effort !== undefined ? { effort: receipt.telemetry.effort } : {}),
|
|
1977
|
+
durationMs: receipt.telemetry.durationMs,
|
|
1978
|
+
...detail,
|
|
1979
|
+
...(receipt.split !== undefined ? { split: { ...receipt.split } } : {}),
|
|
1980
|
+
...(receipt.usdSource !== undefined ? { usdSource: receipt.usdSource } : {}),
|
|
1981
|
+
...(receipt.reportedAt !== undefined ? { reportedAt: receipt.reportedAt } : {}),
|
|
1982
|
+
};
|
|
1983
|
+
receipt.detail = { ...(receipt.detail ?? {}), ...eventDetail };
|
|
1984
|
+
this.event(run, "usage_debit", canonicalStepId, structuredClone(eventDetail));
|
|
1985
|
+
return { status: "ok", ...(budget !== undefined ? { budget } : {}) };
|
|
1986
|
+
}
|
|
1987
|
+
settleLegacyReceipt(run, usage, source, telemetry, located, usdSource, split) {
|
|
1988
|
+
const seq = (run.receiptCounter ?? 0) + 1;
|
|
1989
|
+
const receipt = buildReceipt(run, {
|
|
1990
|
+
dispatchId: `legacy:${seq}`,
|
|
1991
|
+
stepId: this.scopedId(located.scope, located.step.id),
|
|
1992
|
+
source,
|
|
1993
|
+
usage,
|
|
1994
|
+
...(telemetry !== undefined ? { telemetry } : {}),
|
|
1995
|
+
...(split !== undefined ? { split } : {}),
|
|
1996
|
+
// A connector that priced the call itself keeps "reported"; only an
|
|
1997
|
+
// unlabelled usd is engine-synthesized "legacy".
|
|
1998
|
+
...(usage.usd !== undefined ? { usdSource: usdSource ?? "legacy" } : {}),
|
|
1999
|
+
});
|
|
2000
|
+
const attempt = (located.item?.attempts.length ?? located.state.attempts.length) + 1;
|
|
2001
|
+
const settled = this.settleReceipt(run, receipt, located, attempt);
|
|
2002
|
+
if (settled.status === "duplicate")
|
|
2003
|
+
throw new Error(`legacy receipt sequence ${seq} collided`);
|
|
2004
|
+
return settled.budget;
|
|
2005
|
+
}
|
|
1730
2006
|
unreachableOnFailTarget(step, scope) {
|
|
1731
2007
|
const routers = scope.flow.steps.filter((candidate) => candidate.on_fail === step.id);
|
|
1732
2008
|
return routers.length > 0
|
|
@@ -1739,7 +2015,8 @@ export class StratumEngine {
|
|
|
1739
2015
|
return !routesHere || scope.steps[step.id].routed !== undefined;
|
|
1740
2016
|
}
|
|
1741
2017
|
/** Reset a revise target and its ordinary descendants; static validation proved target ancestry. */
|
|
1742
|
-
resetFrom(
|
|
2018
|
+
resetFrom(run, scope, target) {
|
|
2019
|
+
const { flow, steps } = scope;
|
|
1743
2020
|
const descendants = new Set([target]);
|
|
1744
2021
|
let changed = true;
|
|
1745
2022
|
while (changed) {
|
|
@@ -1760,6 +2037,20 @@ export class StratumEngine {
|
|
|
1760
2037
|
}
|
|
1761
2038
|
}
|
|
1762
2039
|
}
|
|
2040
|
+
const reset = [...descendants].map((id) => {
|
|
2041
|
+
const fromEpoch = steps[id].epoch ?? 0;
|
|
2042
|
+
return { stepId: this.scopedId(scope, id), fromEpoch, toEpoch: fromEpoch + 1 };
|
|
2043
|
+
});
|
|
2044
|
+
const subflowsDropped = [...descendants].flatMap((id) => steps[id].sub === undefined ? [] : [this.scopedId(scope, id)]);
|
|
2045
|
+
const detail = { reason: "revise", reset, subflowsDropped };
|
|
2046
|
+
this.event(run, "step_reset", this.scopedId(scope, target), detail);
|
|
2047
|
+
const resetReceiptSeq = (run.receiptCounter ?? 0) + 1;
|
|
2048
|
+
(run.receipts ??= []).push(buildReceipt(run, {
|
|
2049
|
+
dispatchId: `engine:step_reset:${resetReceiptSeq}`,
|
|
2050
|
+
source: "engine",
|
|
2051
|
+
usage: {},
|
|
2052
|
+
detail,
|
|
2053
|
+
}));
|
|
1763
2054
|
const gateIds = new Set(flow.steps.flatMap((step) => step.gate !== undefined ? [step.id] : []));
|
|
1764
2055
|
for (const id of descendants) {
|
|
1765
2056
|
const state = steps[id];
|
|
@@ -1962,7 +2253,7 @@ export class StratumEngine {
|
|
|
1962
2253
|
return parse && !parse.success ? parse.error.message : parse ? undefined : "output contract missing";
|
|
1963
2254
|
}
|
|
1964
2255
|
rootScope(run, spec) {
|
|
1965
|
-
return { input: run.input, steps: run.steps, flow: this.flowFor(run, spec) };
|
|
2256
|
+
return { input: run.input, steps: run.steps, flow: this.flowFor(run, spec), flowName: run.flowName };
|
|
1966
2257
|
}
|
|
1967
2258
|
childScope(spec, parentStep, parentState) {
|
|
1968
2259
|
if (parentStep.run === undefined || parentState.sub === undefined)
|
|
@@ -1974,6 +2265,7 @@ export class StratumEngine {
|
|
|
1974
2265
|
input: parentState.sub.input,
|
|
1975
2266
|
steps: parentState.sub.steps,
|
|
1976
2267
|
flow,
|
|
2268
|
+
flowName: parentStep.run,
|
|
1977
2269
|
prefix: parentStep.id,
|
|
1978
2270
|
parent: { step: parentStep, state: parentState },
|
|
1979
2271
|
};
|
|
@@ -2004,6 +2296,32 @@ export class StratumEngine {
|
|
|
2004
2296
|
const state = scope.steps[childId];
|
|
2005
2297
|
return step && state ? { scope, step, state } : undefined;
|
|
2006
2298
|
}
|
|
2299
|
+
/** Receipt attribution may arrive after a step or run terminalizes, when the
|
|
2300
|
+
* execution lookup intentionally hides inactive subflow/fanout state. */
|
|
2301
|
+
locateReceiptStep(run, spec, id) {
|
|
2302
|
+
const active = this.locateStep(run, spec, id);
|
|
2303
|
+
if (active !== undefined || !id.includes("/"))
|
|
2304
|
+
return active;
|
|
2305
|
+
const parts = id.split("/");
|
|
2306
|
+
if (parts.length !== 2 || !parts[0] || !parts[1])
|
|
2307
|
+
return undefined;
|
|
2308
|
+
const [parentId, childId] = parts;
|
|
2309
|
+
const root = this.rootScope(run, spec);
|
|
2310
|
+
const parentStep = root.flow.steps.find((candidate) => candidate.id === parentId);
|
|
2311
|
+
const parentState = root.steps[parentId];
|
|
2312
|
+
if (!parentStep || !parentState)
|
|
2313
|
+
return undefined;
|
|
2314
|
+
if (parentStep.fanout?.dispatch === "consumer" && parentState.fanout && /^(0|[1-9][0-9]*)$/.test(childId)) {
|
|
2315
|
+
const item = parentState.fanout.items[Number(childId)];
|
|
2316
|
+
return item && item.index === Number(childId) ? { scope: root, step: parentStep, state: parentState, item } : undefined;
|
|
2317
|
+
}
|
|
2318
|
+
if (parentStep.run === undefined || parentState.sub === undefined)
|
|
2319
|
+
return undefined;
|
|
2320
|
+
const scope = this.childScope(spec, parentStep, parentState);
|
|
2321
|
+
const step = scope.flow.steps.find((candidate) => candidate.id === childId);
|
|
2322
|
+
const state = scope.steps[childId];
|
|
2323
|
+
return step && state ? { scope, step, state } : undefined;
|
|
2324
|
+
}
|
|
2007
2325
|
collectReady(run, spec) {
|
|
2008
2326
|
const root = this.rootScope(run, spec);
|
|
2009
2327
|
const ready = root.flow.steps.flatMap((step) => {
|
|
@@ -2148,7 +2466,9 @@ export class StratumEngine {
|
|
|
2148
2466
|
try {
|
|
2149
2467
|
return await this.loadRun(runId);
|
|
2150
2468
|
}
|
|
2151
|
-
catch {
|
|
2469
|
+
catch (error) {
|
|
2470
|
+
if (!(typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT"))
|
|
2471
|
+
throw error;
|
|
2152
2472
|
throw new CheckpointOperationError("flow_not_found", `No active flow with id '${runId}'`);
|
|
2153
2473
|
}
|
|
2154
2474
|
}
|
|
@@ -2258,6 +2578,7 @@ export class StratumEngine {
|
|
|
2258
2578
|
run.failure = failure;
|
|
2259
2579
|
this.event(run, "budget_exhausted", undefined, failure);
|
|
2260
2580
|
await this.persist(run);
|
|
2581
|
+
this.emitFlowTerminal(run);
|
|
2261
2582
|
return this.response(run);
|
|
2262
2583
|
}
|
|
2263
2584
|
async terminalFailure(run, failure) {
|
|
@@ -2265,8 +2586,42 @@ export class StratumEngine {
|
|
|
2265
2586
|
run.failure = failure;
|
|
2266
2587
|
this.event(run, "failed", undefined, failure);
|
|
2267
2588
|
await this.persist(run);
|
|
2589
|
+
this.emitFlowTerminal(run);
|
|
2268
2590
|
return this.response(run);
|
|
2269
2591
|
}
|
|
2592
|
+
recordPolicyVerdict(run, flowName, stepId, ensureIndex, met, predicateTypeValue) {
|
|
2593
|
+
const binding = run.policy_rules?.[policyRuleKey(flowName, stepId)]?.find((candidate) => candidate.ensure_index === ensureIndex);
|
|
2594
|
+
if (binding === undefined)
|
|
2595
|
+
return;
|
|
2596
|
+
(run.policy_verdicts ??= []).push({
|
|
2597
|
+
rule_id: binding.rule_id,
|
|
2598
|
+
source: structuredClone(binding.source),
|
|
2599
|
+
met,
|
|
2600
|
+
predicate_type: predicateTypeValue,
|
|
2601
|
+
});
|
|
2602
|
+
}
|
|
2603
|
+
emitFlowTerminal(run) {
|
|
2604
|
+
if (run.bundle_id === undefined)
|
|
2605
|
+
return;
|
|
2606
|
+
this.firePolicyEvent(buildFlowTerminalEvent({
|
|
2607
|
+
runId: run.id,
|
|
2608
|
+
bundleId: run.bundle_id,
|
|
2609
|
+
outcome: run.status,
|
|
2610
|
+
rulesEvaluated: run.policy_verdicts ?? [],
|
|
2611
|
+
}));
|
|
2612
|
+
}
|
|
2613
|
+
firePolicyEvent(event) {
|
|
2614
|
+
void postPolicyEvent(event).catch((error) => {
|
|
2615
|
+
console.warn(`policy event ${event.event_id} delivery failed: ${message(error)}`);
|
|
2616
|
+
});
|
|
2617
|
+
}
|
|
2618
|
+
triggerLearnEgress(runId) {
|
|
2619
|
+
if (!this.learnEgress.enabled())
|
|
2620
|
+
return;
|
|
2621
|
+
void this.learnEgress.drainRun(runId).catch((error) => {
|
|
2622
|
+
console.warn(`SmartMemory egress drain failed for run ${runId}: ${message(error)}`);
|
|
2623
|
+
});
|
|
2624
|
+
}
|
|
2270
2625
|
response(run) {
|
|
2271
2626
|
const ledger = this.ledgerInfo(run);
|
|
2272
2627
|
if (run.status === "completed")
|
|
@@ -2289,7 +2644,13 @@ export class StratumEngine {
|
|
|
2289
2644
|
}
|
|
2290
2645
|
persist(run) {
|
|
2291
2646
|
const previous = this.persistLocks.get(run.id) ?? Promise.resolve();
|
|
2292
|
-
const result = previous
|
|
2647
|
+
const result = previous
|
|
2648
|
+
.then(() => this.store.save(run))
|
|
2649
|
+
.then(() => {
|
|
2650
|
+
if (this.learnEgress.enabled() && run.receipts?.some((receipt) => receipt.egress === "pending") === true) {
|
|
2651
|
+
this.triggerLearnEgress(run.id);
|
|
2652
|
+
}
|
|
2653
|
+
});
|
|
2293
2654
|
const tail = result.catch(() => undefined);
|
|
2294
2655
|
this.persistLocks.set(run.id, tail);
|
|
2295
2656
|
void tail.then(() => { if (this.persistLocks.get(run.id) === tail)
|
|
@@ -2403,16 +2764,12 @@ function message(error) {
|
|
|
2403
2764
|
}
|
|
2404
2765
|
}
|
|
2405
2766
|
function hasBudget(usage) { return Object.keys(usage).length > 0; }
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
return false;
|
|
2413
|
-
return typeof value.durationMs === "number" && Number.isFinite(value.durationMs) && value.durationMs >= 0
|
|
2414
|
-
&& typeof value.model === "string" && value.model.length > 0
|
|
2415
|
-
&& (value.effort === undefined || (typeof value.effort === "string" && value.effort.length > 0));
|
|
2767
|
+
/** Two settle results from one attempt collapse to the most severe ledger breach. */
|
|
2768
|
+
function worstBudget(...results) {
|
|
2769
|
+
for (const level of ["flow", "subflow", "task"])
|
|
2770
|
+
if (results.includes(level))
|
|
2771
|
+
return level;
|
|
2772
|
+
return undefined;
|
|
2416
2773
|
}
|
|
2417
2774
|
function telemetryFields(value) {
|
|
2418
2775
|
return value === undefined ? {} : { durationMs: value.durationMs, model: value.model, ...(value.effort !== undefined ? { effort: value.effort } : {}) };
|
|
@@ -2444,13 +2801,17 @@ export const defaultConnector = async ({ agent, prompt, cwd, previousFailure, ou
|
|
|
2444
2801
|
});
|
|
2445
2802
|
if ("status" in result)
|
|
2446
2803
|
return { failure: "background connector response is not valid for synchronous fanout" };
|
|
2804
|
+
const provenance = {
|
|
2805
|
+
...(result.usdSource !== undefined ? { usdSource: result.usdSource } : {}),
|
|
2806
|
+
...(result.split !== undefined ? { split: result.split } : {}),
|
|
2807
|
+
};
|
|
2447
2808
|
if (outSchema === undefined)
|
|
2448
|
-
return { output: result.text, usage: result.usage, telemetry: result.telemetry };
|
|
2809
|
+
return { output: result.text, usage: result.usage, telemetry: result.telemetry, ...provenance };
|
|
2449
2810
|
try {
|
|
2450
|
-
return { output: JSON.parse(stripJsonFences(result.text)), usage: result.usage, telemetry: result.telemetry };
|
|
2811
|
+
return { output: JSON.parse(stripJsonFences(result.text)), usage: result.usage, telemetry: result.telemetry, ...provenance };
|
|
2451
2812
|
}
|
|
2452
2813
|
catch {
|
|
2453
|
-
return { failure: "connector result must be JSON for a contract-enforced fanout stage", usage: result.usage, telemetry: result.telemetry };
|
|
2814
|
+
return { failure: "connector result must be JSON for a contract-enforced fanout stage", usage: result.usage, telemetry: result.telemetry, ...provenance };
|
|
2454
2815
|
}
|
|
2455
2816
|
};
|
|
2456
2817
|
function stripJsonFences(text) {
|