@obversa/runtime 0.2.0 → 0.2.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/api.d.ts +1 -0
- package/dist/api.js +260 -71
- package/dist/api.js.map +1 -1
- package/dist/{chunk-ZXVFWRW5.js → chunk-G2OIKOUI.js} +273 -64
- package/dist/chunk-G2OIKOUI.js.map +1 -0
- package/dist/core/interaction.d.ts +26 -0
- package/dist/core/job.d.ts +3 -0
- package/dist/core/judge.d.ts +36 -0
- package/dist/core/types.d.ts +40 -1
- package/dist/runtime/persist.d.ts +4 -17
- package/dist/workflow-support.js +1 -1
- package/dist/workflow.d.ts +15 -1
- package/package.json +4 -4
- package/dist/chunk-ZXVFWRW5.js.map +0 -1
package/dist/api.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { compileGraph } from './chunk-DEW5R23M.js';
|
|
2
2
|
export { compileGraph } from './chunk-DEW5R23M.js';
|
|
3
|
-
import { LoopError, needDecisionsOf, DEFAULT_FANOUT_CONCURRENCY, setMeta, jobMeta, describeConditions, Budget, currentBranch, assertDistinctSeats, seatIdentity, copyJobMeta, predicate, setLabel, resolveEnv, scrubCapture, withConditionPreparation, toCondition, prepareCondition, validateCallbackRequest, reportedUsage, validateIncompleteResultEvidence, engineSelection, validateAgentResult, requireFinalResultText, isRepo, addWorktree, childContext, stageAll, commit, mergeBranch, removeWorktree, deleteBranch, headSha, hasStagedChanges, reviewPanel, createCallbackGate, defineAgent, isInfrastructureError, validateCallbackResponse, gitRoot, captureGitWorkspaceSnapshot, workspaceFingerprint, revisionFromOutcome, agentJob, commandJob, approval, mergeNoCommit, mergeAbort, normalizeFeedbackSeverity, nodeJobContext, outcomeFromAgentText, requireNonEmptyFiles, requireNoFiles, panelReviewers, delegateNodeJob, branchCommits, RECORDED_ENGINE_USAGE, compareGitWorkspaceSnapshots } from './chunk-
|
|
4
|
-
export { LoopError, RECORDED_ENGINE_USAGE, agentCheck, agentJob, all, always, any, approval, bodyPassed, callbackRequestDigest, commandJob, commandSucceeds, copyJobMeta, createCallbackGate, defineAgent, defineSkill, describeConditions, failed, finalResultPart, finalResultText, fnJob, fromFile, gateJob, jobMeta, kickback, minConfidence, never, not, passed, predicate, prove, quorum, renderPlan, reviewContext, reviewPanel, revisionRequest, toCondition, validateAgentResult, validateCallbackRequest, validateCallbackResponse, withEnv } from './chunk-
|
|
3
|
+
import { LoopError, needDecisionsOf, interactionIdentity, DEFAULT_FANOUT_CONCURRENCY, interactionDeclaration, setMeta, jobMeta, describeConditions, Budget, currentBranch, assertDistinctSeats, seatIdentity, copyJobMeta, predicate, setLabel, resolveEnv, scrubCapture, withConditionPreparation, toCondition, prepareCondition, validateCallbackRequest, reportedUsage, validateIncompleteResultEvidence, engineSelection, validateAgentResult, requireFinalResultText, isRepo, addWorktree, childContext, stageAll, commit, mergeBranch, removeWorktree, deleteBranch, headSha, hasStagedChanges, reviewPanel, createCallbackGate, defineAgent, isInfrastructureError, validateCallbackResponse, gitRoot, captureGitWorkspaceSnapshot, workspaceFingerprint, savedInteraction, revisionFromOutcome, checkpointInteraction, jsonSnapshot, outcomeSnapshot, agentJob, humanReview, commandJob, approval, requestInteraction, mergeNoCommit, mergeAbort, normalizeFeedbackSeverity, DEFAULT_INTERACTION, nodeJobContext, outcomeFromAgentText, requireNonEmptyFiles, requireNoFiles, panelReviewers, hasSavedInteraction, fnJob, kickback, delegateNodeJob, branchCommits, RECORDED_ENGINE_USAGE, compareGitWorkspaceSnapshots } from './chunk-G2OIKOUI.js';
|
|
4
|
+
export { LoopError, RECORDED_ENGINE_USAGE, agentCheck, agentJob, all, always, any, approval, bodyPassed, callbackRequestDigest, commandJob, commandSucceeds, copyJobMeta, createCallbackGate, defineAgent, defineSkill, describeConditions, failed, finalResultPart, finalResultText, fnJob, fromFile, gateJob, humanReview, jobMeta, kickback, minConfidence, never, not, passed, predicate, prove, quorum, renderPlan, reviewContext, reviewPanel, revisionRequest, toCondition, validateAgentResult, validateCallbackRequest, validateCallbackResponse, withEnv } from './chunk-G2OIKOUI.js';
|
|
5
5
|
import { runRuntimeProcess, processText } from './chunk-NIBHM5I5.js';
|
|
6
6
|
import { loadRunDefinition, StorageError, validateDomainEventBatch, validateNewDomainEvent, validateDomainEventEnvelope, validateDomainEventId, validateArtifactReference, validateStorageId } from './chunk-RZLMX3IA.js';
|
|
7
7
|
export { StorageError, loadRunDefinition, persistRunDefinition, validateArtifactReference, validateArtifactScope, validateDomainEventBatch, validateDomainEventEnvelope, validateDomainEventId, validateEventStreamRef, validateNewArtifact, validateNewDomainEvent, validateRunDefinition, validateRunStartRecord, validateRunStoragePolicy, validateRunStorageRecord } from './chunk-RZLMX3IA.js';
|
|
@@ -14,7 +14,7 @@ import { randomUUID, createHash } from 'crypto';
|
|
|
14
14
|
import { SUBAGENT_TOOLS, snapshotApprovalSubject, assertApprovalPermissionsAdmitted, validateAcceptedResultRecord, EngineIncompleteResultError, EngineError, classifyEngineFailure, LANE_DEAD_FAILURES, validateCallbackEvent, ApprovalSubjectError, isEngine, cloneFrozenJson as cloneFrozenJson$1, validateApprovalRecord, digestJson as digestJson$1, GraphExecutionError, validateApprovalSubject, modelIdentity, validateActionDecision } from '@obversa/api';
|
|
15
15
|
export { ApprovalSubjectError, EngineError, EngineIncompleteResultError, GraphExecutionError, LANE_DEAD_FAILURES, acceptedResultMatches, approvalSubjectDigest, classifyEngineFailure, createApprovalCallbackGate, validateAcceptedResultRecord, validateApprovalRecord, validateCallbackEvent } from '@obversa/api';
|
|
16
16
|
import { readFileSync, mkdirSync, existsSync, writeFileSync, appendFileSync, lstatSync, realpathSync } from 'fs';
|
|
17
|
-
import { mkdir, stat, open, rename, rm, realpath,
|
|
17
|
+
import { mkdir, readFile, stat, open, rename, rm, realpath, lstat } from 'fs/promises';
|
|
18
18
|
import { join, resolve, basename, extname, dirname, isAbsolute, relative, sep } from 'path';
|
|
19
19
|
import pLimit from 'p-limit';
|
|
20
20
|
import toposort from 'toposort';
|
|
@@ -150,12 +150,13 @@ function stopQuestions(what = "the draft") {
|
|
|
150
150
|
},
|
|
151
151
|
stop_reason: {
|
|
152
152
|
type: "choice",
|
|
153
|
-
instructions: "
|
|
153
|
+
instructions: "If a person must settle a product decision, choose product_decision. Otherwise, are we at the point of diminishing returns? If so, which kind; if not, continue.",
|
|
154
154
|
criteria: {
|
|
155
155
|
holds: `${what} holds for this use case.`,
|
|
156
156
|
over_polishing: "The remaining findings are taste, nits or edge cases past the bar.",
|
|
157
157
|
not_converging: "The same class of finding keeps returning, so another round will not fix it.",
|
|
158
|
-
continue: "Another round is worth it."
|
|
158
|
+
continue: "Another round is worth it.",
|
|
159
|
+
product_decision: "A person must settle a product decision before this review can continue."
|
|
159
160
|
}
|
|
160
161
|
}
|
|
161
162
|
};
|
|
@@ -164,7 +165,7 @@ function judge(seat, opts) {
|
|
|
164
165
|
if (!Number.isSafeInteger(opts.cap) || opts.cap < 1) {
|
|
165
166
|
throw new TypeError("judge cap must be a positive integer");
|
|
166
167
|
}
|
|
167
|
-
return { kind: "judge", seat, cap: opts.cap, questions: opts.questions ?? stopQuestions() };
|
|
168
|
+
return { kind: "judge", seat, cap: opts.cap, questions: opts.questions ?? stopQuestions(), ...opts.interaction ? { interaction: opts.interaction } : {} };
|
|
168
169
|
}
|
|
169
170
|
function isJudge(value) {
|
|
170
171
|
return typeof value === "object" && value !== null && value.kind === "judge";
|
|
@@ -183,17 +184,21 @@ function judgeDecision(answers) {
|
|
|
183
184
|
const doing = answers.worth_doing?.noul ?? answers.worth_doing?.probability;
|
|
184
185
|
const holds = answers.holds?.noul ?? answers.holds?.probability;
|
|
185
186
|
const reason = answers.stop_reason?.choice ?? "unknown";
|
|
187
|
+
if (reason === "product_decision") return { again: false, stop: "product_decision", reason: "the judge requested a product decision" };
|
|
188
|
+
if (reason === "holds" || reason === "over_polishing") {
|
|
189
|
+
return { again: false, stop: "ship", reason: `the judge chose ${reason}` };
|
|
190
|
+
}
|
|
186
191
|
if (reason !== "unknown" && reason !== "continue") {
|
|
187
|
-
return { again: false, reason: `the judge chose ${reason}` };
|
|
192
|
+
return { again: false, stop: "fail", reason: `the judge chose ${reason}` };
|
|
188
193
|
}
|
|
189
194
|
if (typeof holds === "number" && holds >= 0.5) {
|
|
190
|
-
return { again: false, reason: `the judge says it holds (${holds.toFixed(2)})` };
|
|
195
|
+
return { again: false, stop: "ship", reason: `the judge says it holds (${holds.toFixed(2)})` };
|
|
191
196
|
}
|
|
192
197
|
if (typeof doing === "number" && doing < 0.5) {
|
|
193
|
-
return { again: false, reason: `the judge says the findings are not worth doing (${doing.toFixed(2)})` };
|
|
198
|
+
return { again: false, stop: "ship", reason: `the judge says the findings are not worth doing (${doing.toFixed(2)})` };
|
|
194
199
|
}
|
|
195
200
|
if (typeof worth === "number" && worth < 0.5) {
|
|
196
|
-
return { again: false, reason: `the judge says another round is not worth it (${worth.toFixed(2)})` };
|
|
201
|
+
return { again: false, stop: "fail", reason: `the judge says another round is not worth it (${worth.toFixed(2)})` };
|
|
197
202
|
}
|
|
198
203
|
return {
|
|
199
204
|
again: true,
|
|
@@ -225,6 +230,29 @@ async function askJudge(cfg, state, ctx, path) {
|
|
|
225
230
|
ctx.emit({ kind: "refine:judge", ts: Date.now(), path: [...path], answers, reason: decision.reason });
|
|
226
231
|
return { answers, decision };
|
|
227
232
|
}
|
|
233
|
+
async function consultJudge(cfg, initialState, ctx, path, options) {
|
|
234
|
+
let state = initialState;
|
|
235
|
+
let pending = options.pending;
|
|
236
|
+
for (; ; ) {
|
|
237
|
+
if (pending) {
|
|
238
|
+
options.save(state);
|
|
239
|
+
const answer2 = await requestInteraction(
|
|
240
|
+
cfg.interaction ?? DEFAULT_INTERACTION,
|
|
241
|
+
"What product decision should guide this review?",
|
|
242
|
+
jsonSnapshot({
|
|
243
|
+
requester: { path, identity: options.identity, engine: cfg.seat.identity, round: state.round },
|
|
244
|
+
material: state
|
|
245
|
+
}),
|
|
246
|
+
ctx
|
|
247
|
+
);
|
|
248
|
+
if ("paused" in answer2) return { state, paused: answer2.paused };
|
|
249
|
+
state = { ...state, productFeedback: [...state.productFeedback ?? [], answer2.response] };
|
|
250
|
+
}
|
|
251
|
+
const { decision } = await askJudge(cfg, state, ctx, path);
|
|
252
|
+
if (decision.stop !== "product_decision") return { state, decision };
|
|
253
|
+
pending = true;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
228
256
|
|
|
229
257
|
// src/core/dag.ts
|
|
230
258
|
function slug(s) {
|
|
@@ -339,7 +367,7 @@ function dag(config) {
|
|
|
339
367
|
return isJudge(budget) ? budget.cap : budget;
|
|
340
368
|
};
|
|
341
369
|
const routeKickbacks = config.maxKickbacks !== void 0 && (perTargetBudget || maxKickbacks > 0);
|
|
342
|
-
const
|
|
370
|
+
const identity = interactionIdentity(config);
|
|
343
371
|
const dependents = new Map(names.map((n) => [n, []]));
|
|
344
372
|
for (const [dep, name] of edges) dependents.get(dep).push(name);
|
|
345
373
|
const ancestorsOf = (name) => {
|
|
@@ -371,12 +399,17 @@ function dag(config) {
|
|
|
371
399
|
const path = [...parent.path, config.name];
|
|
372
400
|
const depth = parent.depth + 1;
|
|
373
401
|
const ts = () => Date.now();
|
|
374
|
-
const
|
|
402
|
+
const checkpointPath = [...path, "@judge-kickback"];
|
|
403
|
+
const saved = savedInteraction(parent, checkpointPath, identity);
|
|
404
|
+
let pending = saved?.pending;
|
|
405
|
+
const targetCounts = new Map(Object.entries(saved?.targetCounts ?? {}));
|
|
406
|
+
const judgeHistory = new Map(Object.entries(saved?.history ?? {}));
|
|
407
|
+
const productFeedback = new Map(Object.entries(saved?.productFeedback ?? {}));
|
|
375
408
|
parent.emit({ kind: "dag:start", ts: ts(), path, depth, nodes: names });
|
|
376
409
|
const limit2 = pLimit(limitN);
|
|
377
|
-
const results =
|
|
378
|
-
const memo =
|
|
379
|
-
const attempts =
|
|
410
|
+
const results = new Map(Object.entries(saved?.results ?? {}));
|
|
411
|
+
const memo = new Map([...results].map(([name, outcome2]) => [name, Promise.resolve(outcome2)]));
|
|
412
|
+
const attempts = new Map(Object.entries(saved?.attempts ?? {}));
|
|
380
413
|
let stopped = false;
|
|
381
414
|
const pendingKickback = /* @__PURE__ */ new Map();
|
|
382
415
|
const nodeContext = (name) => {
|
|
@@ -615,8 +648,8 @@ function dag(config) {
|
|
|
615
648
|
};
|
|
616
649
|
await Promise.all(names.map(run2));
|
|
617
650
|
if (routeKickbacks) {
|
|
618
|
-
let used = 0;
|
|
619
|
-
const rejected =
|
|
651
|
+
let used = Number(saved?.used ?? 0);
|
|
652
|
+
const rejected = new Set(saved?.rejected ?? []);
|
|
620
653
|
const emitKickback = (from, to, reason, accepted, count, limit3, note) => parent.emit({
|
|
621
654
|
kind: "dag:kickback",
|
|
622
655
|
ts: ts(),
|
|
@@ -641,7 +674,7 @@ function dag(config) {
|
|
|
641
674
|
const request = revisionFromOutcome(results.get(from));
|
|
642
675
|
const to = request.target;
|
|
643
676
|
const { reason } = request;
|
|
644
|
-
const count = (targetCounts.get(to) ?? 0) + 1;
|
|
677
|
+
const count = pending?.from === from ? pending.count : (targetCounts.get(to) ?? 0) + 1;
|
|
645
678
|
targetCounts.set(to, count);
|
|
646
679
|
const limit3 = targetLimit(to);
|
|
647
680
|
const allow = nodes.get(from).acceptsKickbackTo;
|
|
@@ -657,20 +690,61 @@ function dag(config) {
|
|
|
657
690
|
let effectiveReason = reason;
|
|
658
691
|
if (cfgJudge !== void 0 && withinBudget && !hasBlockFinding(requestFindings)) {
|
|
659
692
|
const history = judgeHistory.get(to) ?? [];
|
|
660
|
-
const state = {
|
|
693
|
+
const state = pending?.from === from ? pending.state : {
|
|
694
|
+
...productFeedback.has(to) ? { productFeedback: productFeedback.get(to) } : {},
|
|
661
695
|
latestFindings: requestFindings,
|
|
662
696
|
rounds: history,
|
|
663
697
|
round: count,
|
|
664
698
|
cap: cfgJudge.cap
|
|
665
699
|
};
|
|
666
|
-
const
|
|
700
|
+
const result = await consultJudge(cfgJudge, state, parent, path, {
|
|
701
|
+
identity: interactionIdentity({ identity, from, to }),
|
|
702
|
+
pending: pending?.from === from,
|
|
703
|
+
save: (questionState) => checkpointInteraction(parent, checkpointPath, identity, jsonSnapshot({
|
|
704
|
+
pending: { from, count, state: questionState },
|
|
705
|
+
results: Object.fromEntries([...results].map(([name, outcome2]) => [name, outcomeSnapshot(outcome2)])),
|
|
706
|
+
attempts: Object.fromEntries(attempts),
|
|
707
|
+
targetCounts: Object.fromEntries(targetCounts),
|
|
708
|
+
history: Object.fromEntries(judgeHistory),
|
|
709
|
+
productFeedback: Object.fromEntries(productFeedback),
|
|
710
|
+
used,
|
|
711
|
+
rejected: [...rejected]
|
|
712
|
+
}))
|
|
713
|
+
});
|
|
714
|
+
if ("paused" in result) {
|
|
715
|
+
record4(from, result.paused, "done");
|
|
716
|
+
break;
|
|
717
|
+
}
|
|
718
|
+
checkpointInteraction(parent, checkpointPath, identity, null);
|
|
719
|
+
pending = void 0;
|
|
720
|
+
productFeedback.set(to, result.state.productFeedback ?? []);
|
|
721
|
+
const { decision } = result;
|
|
667
722
|
judgeHistory.set(to, [
|
|
668
723
|
...history,
|
|
669
724
|
{ round: count, findings: requestFindings, counts: countBySeverity(requestFindings) }
|
|
670
725
|
]);
|
|
671
726
|
if (!decision.again) {
|
|
672
|
-
rejected.add(from);
|
|
673
727
|
emitKickback(from, to, `${reason} (${decision.reason})`, false, count, limit3, decision.reason);
|
|
728
|
+
if (decision.stop === "ship") {
|
|
729
|
+
const shipped = record4(from, {
|
|
730
|
+
status: "pass",
|
|
731
|
+
confidence: results.get(from).confidence,
|
|
732
|
+
summary: decision.reason,
|
|
733
|
+
data: results.get(from).data
|
|
734
|
+
}, "done");
|
|
735
|
+
memo.set(from, Promise.resolve(shipped));
|
|
736
|
+
const unblocked = dirtyFrom(from);
|
|
737
|
+
unblocked.delete(from);
|
|
738
|
+
for (const d of unblocked) {
|
|
739
|
+
memo.delete(d);
|
|
740
|
+
results.delete(d);
|
|
741
|
+
rejected.delete(d);
|
|
742
|
+
}
|
|
743
|
+
stopped = false;
|
|
744
|
+
await Promise.all(names.map(run2));
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
rejected.add(from);
|
|
674
748
|
continue;
|
|
675
749
|
}
|
|
676
750
|
effectiveReason = `${reason} (${decision.reason})`;
|
|
@@ -750,7 +824,7 @@ function dag(config) {
|
|
|
750
824
|
parent.emit({ kind: "dag:end", ts: ts(), path, outcome });
|
|
751
825
|
return outcome;
|
|
752
826
|
};
|
|
753
|
-
return setMeta(job, {
|
|
827
|
+
return interactionDeclaration(setMeta(job, {
|
|
754
828
|
kind: "dag",
|
|
755
829
|
name: config.name,
|
|
756
830
|
...config.maxKickbacks !== void 0 ? { maxKickbacks: config.maxKickbacks } : {},
|
|
@@ -771,7 +845,7 @@ function dag(config) {
|
|
|
771
845
|
job: jobMeta(nodeJob)
|
|
772
846
|
};
|
|
773
847
|
})
|
|
774
|
-
});
|
|
848
|
+
}), config);
|
|
775
849
|
}
|
|
776
850
|
function sequence(name, ...jobs) {
|
|
777
851
|
const nodes = {};
|
|
@@ -944,9 +1018,15 @@ function loop(config) {
|
|
|
944
1018
|
const path = [...parent.path, config.name];
|
|
945
1019
|
const depth = parent.depth + 1;
|
|
946
1020
|
const ts = () => Date.now();
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
1021
|
+
const checkpointPath = [...path, "@interaction-loop"];
|
|
1022
|
+
const identity = interactionIdentity({ config, params: parent.params });
|
|
1023
|
+
const saved = savedInteraction(parent, checkpointPath, identity);
|
|
1024
|
+
let checkpointed = saved !== void 0;
|
|
1025
|
+
let resumePhase = saved?.phase;
|
|
1026
|
+
let lastReview = saved?.lastReview ?? parent.lastReview;
|
|
1027
|
+
let lastGate = saved?.lastGate;
|
|
1028
|
+
let iteration = typeof saved?.iteration === "number" ? saved.iteration : 0;
|
|
1029
|
+
if (resumePhase === "body") iteration -= 1;
|
|
950
1030
|
const ctxAt = (iter, lastOutcome) => childContext(parent, {
|
|
951
1031
|
depth,
|
|
952
1032
|
path,
|
|
@@ -957,6 +1037,7 @@ function loop(config) {
|
|
|
957
1037
|
});
|
|
958
1038
|
parent.emit({ kind: "loop:start", ts: ts(), path, depth, max: config.max });
|
|
959
1039
|
const finish = async (outcome, iterations) => {
|
|
1040
|
+
if (checkpointed && outcome.status !== "paused" && outcome.status !== "aborted") checkpointInteraction(parent, checkpointPath, identity, null);
|
|
960
1041
|
parent.emit({ kind: "loop:end", ts: ts(), path, outcome, iterations });
|
|
961
1042
|
if (config.onComplete) {
|
|
962
1043
|
try {
|
|
@@ -1003,7 +1084,7 @@ function loop(config) {
|
|
|
1003
1084
|
config.until ? prepareCondition(config.until, entryContext) : void 0,
|
|
1004
1085
|
config.stopOn ? prepareCondition(config.stopOn, entryContext) : void 0
|
|
1005
1086
|
]);
|
|
1006
|
-
if (start) {
|
|
1087
|
+
if (start && !saved) {
|
|
1007
1088
|
const r = await gate(start, "start", ctxAt(0), void 0);
|
|
1008
1089
|
parent.emit({
|
|
1009
1090
|
kind: "loop:condition",
|
|
@@ -1019,11 +1100,30 @@ function loop(config) {
|
|
|
1019
1100
|
0
|
|
1020
1101
|
);
|
|
1021
1102
|
}
|
|
1022
|
-
let last;
|
|
1023
|
-
let consecutiveErrors = 0;
|
|
1024
|
-
let consecutiveReviewFails = 0;
|
|
1103
|
+
let last = saved?.last;
|
|
1104
|
+
let consecutiveErrors = Number(saved?.consecutiveErrors ?? 0);
|
|
1105
|
+
let consecutiveReviewFails = Number(saved?.consecutiveReviewFails ?? 0);
|
|
1025
1106
|
const tracker = noProgress ? new ProgressTracker(noProgress) : void 0;
|
|
1107
|
+
const samples = [...saved?.samples ?? []];
|
|
1108
|
+
for (const sample of samples) tracker?.record(sample);
|
|
1026
1109
|
let warnedInert = false;
|
|
1110
|
+
const checkpointContext = (ctx, phase) => ({
|
|
1111
|
+
...ctx,
|
|
1112
|
+
interactionCheckpoint() {
|
|
1113
|
+
checkpointed = true;
|
|
1114
|
+
parent.interactionCheckpoint?.();
|
|
1115
|
+
checkpointInteraction(parent, checkpointPath, identity, jsonSnapshot({
|
|
1116
|
+
phase,
|
|
1117
|
+
iteration,
|
|
1118
|
+
consecutiveErrors,
|
|
1119
|
+
consecutiveReviewFails,
|
|
1120
|
+
samples,
|
|
1121
|
+
...last ? { last: outcomeSnapshot(last) } : {},
|
|
1122
|
+
...lastReview ? { lastReview: outcomeSnapshot(lastReview) } : {},
|
|
1123
|
+
...lastGate ? { lastGate: Object.fromEntries(Object.entries(lastGate).filter(([, value]) => value !== void 0)) } : {}
|
|
1124
|
+
}));
|
|
1125
|
+
}
|
|
1126
|
+
});
|
|
1027
1127
|
const evaluateConvergence = async (at, gateContext, bodyOutcome) => {
|
|
1028
1128
|
const conv = until ? await gate(until, "until", gateContext, bodyOutcome) : {
|
|
1029
1129
|
met: bodyOutcome.status === "pass",
|
|
@@ -1068,7 +1168,7 @@ function loop(config) {
|
|
|
1068
1168
|
}
|
|
1069
1169
|
let reviewOutcome;
|
|
1070
1170
|
try {
|
|
1071
|
-
reviewOutcome = await config.review(ctxAt(at, bodyOutcome));
|
|
1171
|
+
reviewOutcome = await config.review(checkpointContext(ctxAt(at, bodyOutcome), "review"));
|
|
1072
1172
|
} catch (e) {
|
|
1073
1173
|
throw LoopError.from(e, {
|
|
1074
1174
|
code: "VALIDATION",
|
|
@@ -1135,7 +1235,7 @@ function loop(config) {
|
|
|
1135
1235
|
}
|
|
1136
1236
|
return { conv, turnReview: reviewOutcome };
|
|
1137
1237
|
};
|
|
1138
|
-
if (config.checkFirst) {
|
|
1238
|
+
if (config.checkFirst && !saved) {
|
|
1139
1239
|
await yieldToLoop();
|
|
1140
1240
|
if (parent.signal.aborted)
|
|
1141
1241
|
return finish(
|
|
@@ -1152,7 +1252,7 @@ function loop(config) {
|
|
|
1152
1252
|
{ status: "aborted", summary: "aborted by signal" },
|
|
1153
1253
|
iteration
|
|
1154
1254
|
);
|
|
1155
|
-
if (config.max != null && iteration >= config.max) {
|
|
1255
|
+
if (resumePhase !== "review" && config.max != null && iteration >= config.max) {
|
|
1156
1256
|
return finish(
|
|
1157
1257
|
{
|
|
1158
1258
|
status: "exhausted",
|
|
@@ -1164,13 +1264,15 @@ function loop(config) {
|
|
|
1164
1264
|
iteration
|
|
1165
1265
|
);
|
|
1166
1266
|
}
|
|
1167
|
-
|
|
1267
|
+
const resumingReview = resumePhase === "review";
|
|
1268
|
+
resumePhase = void 0;
|
|
1269
|
+
if (!resumingReview) iteration += 1;
|
|
1168
1270
|
const ctx = ctxAt(iteration, last);
|
|
1169
1271
|
let turnReview;
|
|
1170
|
-
parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
|
|
1272
|
+
if (!resumingReview) parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
|
|
1171
1273
|
let bodyThrew = false;
|
|
1172
1274
|
try {
|
|
1173
|
-
last = await config.body(ctx);
|
|
1275
|
+
if (!resumingReview) last = await config.body(checkpointContext(ctx, "body"));
|
|
1174
1276
|
consecutiveErrors = 0;
|
|
1175
1277
|
} catch (e) {
|
|
1176
1278
|
bodyThrew = true;
|
|
@@ -1261,7 +1363,7 @@ function loop(config) {
|
|
|
1261
1363
|
iteration
|
|
1262
1364
|
);
|
|
1263
1365
|
}
|
|
1264
|
-
if (config.onIteration) {
|
|
1366
|
+
if (config.onIteration && !resumingReview) {
|
|
1265
1367
|
try {
|
|
1266
1368
|
await config.onIteration(last, ctx);
|
|
1267
1369
|
} catch (e) {
|
|
@@ -1273,7 +1375,7 @@ function loop(config) {
|
|
|
1273
1375
|
});
|
|
1274
1376
|
}
|
|
1275
1377
|
}
|
|
1276
|
-
if (stopOn) {
|
|
1378
|
+
if (stopOn && !resumingReview) {
|
|
1277
1379
|
const r = await gate(stopOn, "stopOn", ctx, last);
|
|
1278
1380
|
parent.emit({
|
|
1279
1381
|
kind: "loop:condition",
|
|
@@ -1321,7 +1423,7 @@ function loop(config) {
|
|
|
1321
1423
|
});
|
|
1322
1424
|
}
|
|
1323
1425
|
}
|
|
1324
|
-
const
|
|
1426
|
+
const sample = {
|
|
1325
1427
|
iteration,
|
|
1326
1428
|
fingerprint,
|
|
1327
1429
|
signal: signalValue,
|
|
@@ -1330,7 +1432,9 @@ function loop(config) {
|
|
|
1330
1432
|
gate: noProgress.gate && until && !conv.met ? conv.output : void 0,
|
|
1331
1433
|
confidence: turnReview?.confidence ?? conv.confidence ?? last.confidence,
|
|
1332
1434
|
reason: turnReview ? turnReview.summary ?? "review rejected" : conv.reason
|
|
1333
|
-
}
|
|
1435
|
+
};
|
|
1436
|
+
samples.push(Object.fromEntries(Object.entries(sample).filter(([, value]) => value !== void 0)));
|
|
1437
|
+
const report = tracker.record(sample);
|
|
1334
1438
|
if (!warnedInert && tracker.isInert()) {
|
|
1335
1439
|
warnedInert = true;
|
|
1336
1440
|
parent.log(
|
|
@@ -1386,7 +1490,7 @@ function loop(config) {
|
|
|
1386
1490
|
);
|
|
1387
1491
|
}
|
|
1388
1492
|
};
|
|
1389
|
-
return setMeta(job, {
|
|
1493
|
+
return interactionDeclaration(setMeta(job, {
|
|
1390
1494
|
kind: "loop",
|
|
1391
1495
|
name: config.name,
|
|
1392
1496
|
max: config.max,
|
|
@@ -1398,7 +1502,7 @@ function loop(config) {
|
|
|
1398
1502
|
review: !!config.review,
|
|
1399
1503
|
maxReviewRestarts: config.maxReviewRestarts,
|
|
1400
1504
|
body: jobMeta(config.body)
|
|
1401
|
-
});
|
|
1505
|
+
}), config);
|
|
1402
1506
|
}
|
|
1403
1507
|
function delay(ms, signal) {
|
|
1404
1508
|
if (signal.aborted) return Promise.resolve();
|
|
@@ -1617,11 +1721,13 @@ var NOISE = /* @__PURE__ */ new Set([
|
|
|
1617
1721
|
"engine:thinking"
|
|
1618
1722
|
]);
|
|
1619
1723
|
function readResumeRecord(path) {
|
|
1724
|
+
const receipts = [];
|
|
1725
|
+
const interactions = /* @__PURE__ */ new Map();
|
|
1620
1726
|
const anchors = /* @__PURE__ */ new Map();
|
|
1621
1727
|
const stages = /* @__PURE__ */ new Map();
|
|
1622
1728
|
const usage2 = /* @__PURE__ */ new Map();
|
|
1623
1729
|
const started = /* @__PURE__ */ new Set();
|
|
1624
|
-
if (!existsSync(path)) return { outcomes: { anchors, stages }, usage: usage2 };
|
|
1730
|
+
if (!existsSync(path)) return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
|
|
1625
1731
|
const lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
1626
1732
|
for (const [lineNumber, line] of lines.entries()) {
|
|
1627
1733
|
if (!line) continue;
|
|
@@ -1631,11 +1737,19 @@ function readResumeRecord(path) {
|
|
|
1631
1737
|
} catch {
|
|
1632
1738
|
continue;
|
|
1633
1739
|
}
|
|
1634
|
-
if (event.kind === "
|
|
1740
|
+
if (event.kind === "engine:usage") receipts.push(event.usage);
|
|
1741
|
+
if (event.kind === "interaction:checkpoint") {
|
|
1742
|
+
const key = event.path.join("/");
|
|
1743
|
+
if (event.data === null) interactions.delete(key);
|
|
1744
|
+
else interactions.set(key, { identity: event.identity, workspace: event.workspace, data: cloneFrozenJson(event.data) });
|
|
1745
|
+
} else if (event.kind === "workflow:start") {
|
|
1635
1746
|
const key = event.path.join("/");
|
|
1636
1747
|
const prior = anchors.get(key);
|
|
1637
1748
|
if (prior?.identity !== event.identity || prior.workspace !== event.workspace) {
|
|
1638
1749
|
const prefix = key ? `${key}/` : "";
|
|
1750
|
+
for (const stage2 of interactions.keys()) {
|
|
1751
|
+
if (stage2.startsWith(prefix)) interactions.delete(stage2);
|
|
1752
|
+
}
|
|
1639
1753
|
for (const stage2 of stages.keys()) {
|
|
1640
1754
|
if (stage2.startsWith(prefix)) stages.delete(stage2);
|
|
1641
1755
|
}
|
|
@@ -1671,7 +1785,7 @@ function readResumeRecord(path) {
|
|
|
1671
1785
|
}
|
|
1672
1786
|
}
|
|
1673
1787
|
}
|
|
1674
|
-
return { outcomes: { anchors, stages }, usage: usage2 };
|
|
1788
|
+
return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
|
|
1675
1789
|
}
|
|
1676
1790
|
function ensureDir(path) {
|
|
1677
1791
|
const dir = dirname(path);
|
|
@@ -2370,7 +2484,10 @@ async function run(job, options = {}) {
|
|
|
2370
2484
|
thin: options.recordTo === "auto",
|
|
2371
2485
|
...resumed === void 0 ? {} : { resume: true }
|
|
2372
2486
|
}));
|
|
2373
|
-
if (resumed !== void 0
|
|
2487
|
+
if (resumed !== void 0) {
|
|
2488
|
+
if (resumed.outcomes.interactions.size > 0) {
|
|
2489
|
+
for (const receipt of resumed.receipts) budget?.addUsage(receipt);
|
|
2490
|
+
}
|
|
2374
2491
|
initialState[RESUME_STAGE_OUTCOMES] = resumed.outcomes;
|
|
2375
2492
|
initialState[RESUME_RECORDED_USAGE] = resumed.usage;
|
|
2376
2493
|
}
|
|
@@ -2601,8 +2718,8 @@ function parseFrontMatter(source) {
|
|
|
2601
2718
|
function briefFromFile(path) {
|
|
2602
2719
|
return parseFrontMatter(readFileSync(path, "utf8"));
|
|
2603
2720
|
}
|
|
2604
|
-
function person(question) {
|
|
2605
|
-
return { kind: "person", question: text(question, "person question") };
|
|
2721
|
+
function person(question, options = {}) {
|
|
2722
|
+
return { kind: "person", question: text(question, "person question"), ...options };
|
|
2606
2723
|
}
|
|
2607
2724
|
function stage(name, config) {
|
|
2608
2725
|
return { name: text(name, "stage name"), config: { ...config } };
|
|
@@ -2782,6 +2899,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2782
2899
|
try {
|
|
2783
2900
|
const all2 = recordedUsage(ctx);
|
|
2784
2901
|
const beforeLength = all2.length;
|
|
2902
|
+
const resumingInteraction = hasSavedInteraction(ctx, ctx.path);
|
|
2785
2903
|
const writerSide = (records) => records.filter(
|
|
2786
2904
|
(record4) => record4.role === "writer" && record4.stage !== void 0 && writerStageNames.includes(record4.stage)
|
|
2787
2905
|
);
|
|
@@ -2793,7 +2911,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2793
2911
|
const current = recordedUsage(ctx);
|
|
2794
2912
|
const after = current.slice(beforeLength);
|
|
2795
2913
|
const afterWriters = writerSide(after);
|
|
2796
|
-
if (writerRunsInsidePanel && afterWriters.length === 0) {
|
|
2914
|
+
if (writerRunsInsidePanel && afterWriters.length === 0 && !resumingInteraction) {
|
|
2797
2915
|
throw new LoopError({
|
|
2798
2916
|
code: "BODY",
|
|
2799
2917
|
phase: "review",
|
|
@@ -2801,7 +2919,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2801
2919
|
});
|
|
2802
2920
|
}
|
|
2803
2921
|
assertRecordedFamilies(afterWriters, reviewerDeclarations);
|
|
2804
|
-
const reviewerSide = after.filter((record4) => record4.role === "reviewer");
|
|
2922
|
+
const reviewerSide = (resumingInteraction ? current : after).filter((record4) => record4.role === "reviewer" && record4.stage === panelStage);
|
|
2805
2923
|
if (reviewerSide.length === 0) {
|
|
2806
2924
|
throw new LoopError({
|
|
2807
2925
|
code: "BODY",
|
|
@@ -2906,6 +3024,26 @@ function guardedAgent(brief, named, seat, files, declaredFiles) {
|
|
|
2906
3024
|
return requireNoFiles(named.name, required, ctx.workspace.dir, forbidden, "body")(ctx);
|
|
2907
3025
|
};
|
|
2908
3026
|
}
|
|
3027
|
+
function guardedFn(fn, named, declaredFiles) {
|
|
3028
|
+
const writes = writesOf(named.config);
|
|
3029
|
+
const target = named.config.sendsBackTo;
|
|
3030
|
+
const body = fnJob(named.name, async (ctx) => {
|
|
3031
|
+
const outcome = await fn(ctx);
|
|
3032
|
+
if (outcome.status === "fail" && target !== void 0 && revisionFromOutcome(outcome)?.target === void 0) {
|
|
3033
|
+
return kickback(target, outcome.summary ?? `${named.name} failed`, {
|
|
3034
|
+
confidence: outcome.confidence,
|
|
3035
|
+
data: outcome.data,
|
|
3036
|
+
error: outcome.error
|
|
3037
|
+
});
|
|
3038
|
+
}
|
|
3039
|
+
return outcome;
|
|
3040
|
+
});
|
|
3041
|
+
const forbidden = declaredFiles.filter((file) => !writes.includes(file));
|
|
3042
|
+
return async (ctx) => {
|
|
3043
|
+
const required = writes.length ? requireNonEmptyFiles(named.name, body, ctx.workspace.dir, writes) : body;
|
|
3044
|
+
return requireNoFiles(named.name, required, ctx.workspace.dir, forbidden, "body")(ctx);
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
2909
3047
|
function unchangedNoteGuard(label, job, writes) {
|
|
2910
3048
|
const previousHashKey = `declarativePreviousHash:${label}`;
|
|
2911
3049
|
return async (ctx) => {
|
|
@@ -2949,7 +3087,11 @@ function judgedReview(brief, config, cfgJudge, panel) {
|
|
|
2949
3087
|
const file = writesOf(config)[0];
|
|
2950
3088
|
const history = [];
|
|
2951
3089
|
let previousDraft;
|
|
3090
|
+
let productFeedback = [];
|
|
3091
|
+
const identity = interactionIdentity({ brief, config, cfgJudge });
|
|
2952
3092
|
return async (ctx) => {
|
|
3093
|
+
const checkpointPath = [...ctx.path, "@judge-review"];
|
|
3094
|
+
let saved = savedInteraction(ctx, checkpointPath, identity);
|
|
2953
3095
|
let draft;
|
|
2954
3096
|
let changedLines;
|
|
2955
3097
|
if (file !== void 0) {
|
|
@@ -2960,7 +3102,13 @@ function judgedReview(brief, config, cfgJudge, panel) {
|
|
|
2960
3102
|
}
|
|
2961
3103
|
previousDraft = draft;
|
|
2962
3104
|
}
|
|
2963
|
-
|
|
3105
|
+
if (saved && saved.state.draft !== draft) saved = void 0;
|
|
3106
|
+
if (saved) {
|
|
3107
|
+
history.splice(0, history.length, ...saved.state.rounds);
|
|
3108
|
+
productFeedback = saved.state.productFeedback ?? [];
|
|
3109
|
+
previousDraft = draft;
|
|
3110
|
+
}
|
|
3111
|
+
const panelOutcome = saved ? saved.panel : await panel({
|
|
2964
3112
|
...ctx,
|
|
2965
3113
|
depth: ctx.depth + 1,
|
|
2966
3114
|
path: [...ctx.path, "review-panel"]
|
|
@@ -2969,7 +3117,8 @@ function judgedReview(brief, config, cfgJudge, panel) {
|
|
|
2969
3117
|
const findings = revisionFromOutcome(panelOutcome)?.findings ?? [];
|
|
2970
3118
|
if (hasBlockFinding(findings)) return panelOutcome;
|
|
2971
3119
|
const round2 = history.length + 1;
|
|
2972
|
-
const state = {
|
|
3120
|
+
const state = saved ? saved.state : {
|
|
3121
|
+
...productFeedback.length ? { productFeedback } : {},
|
|
2973
3122
|
...useCase !== void 0 ? { useCase } : {},
|
|
2974
3123
|
...file !== void 0 ? { file } : {},
|
|
2975
3124
|
...draft !== void 0 ? { draft } : {},
|
|
@@ -2978,7 +3127,15 @@ function judgedReview(brief, config, cfgJudge, panel) {
|
|
|
2978
3127
|
round: round2,
|
|
2979
3128
|
cap: cfgJudge.cap
|
|
2980
3129
|
};
|
|
2981
|
-
const
|
|
3130
|
+
const result = await consultJudge(cfgJudge, state, ctx, ctx.path, {
|
|
3131
|
+
identity,
|
|
3132
|
+
pending: saved !== void 0,
|
|
3133
|
+
save: (questionState) => checkpointInteraction(ctx, checkpointPath, identity, jsonSnapshot({ state: questionState, panel: outcomeSnapshot(panelOutcome) }))
|
|
3134
|
+
});
|
|
3135
|
+
if ("paused" in result) return result.paused;
|
|
3136
|
+
checkpointInteraction(ctx, checkpointPath, identity, null);
|
|
3137
|
+
productFeedback = result.state.productFeedback ?? [];
|
|
3138
|
+
const { decision } = result;
|
|
2982
3139
|
history.push({
|
|
2983
3140
|
round: round2,
|
|
2984
3141
|
findings,
|
|
@@ -2986,6 +3143,9 @@ function judgedReview(brief, config, cfgJudge, panel) {
|
|
|
2986
3143
|
...changedLines !== void 0 ? { changedLines } : {}
|
|
2987
3144
|
});
|
|
2988
3145
|
if (!decision.again) {
|
|
3146
|
+
if (decision.stop === "fail") {
|
|
3147
|
+
throw new LoopError({ code: "VALIDATION", phase: "review", path: ctx.path, message: decision.reason });
|
|
3148
|
+
}
|
|
2989
3149
|
return { status: "pass", confidence: panelOutcome.confidence, summary: decision.reason, data: panelOutcome.data };
|
|
2990
3150
|
}
|
|
2991
3151
|
return { ...panelOutcome, summary: `${panelOutcome.summary} (${decision.reason})` };
|
|
@@ -2998,6 +3158,24 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
|
|
|
2998
3158
|
const guarded = guardedAgent(brief, named, seatRole(roles, config.agent), files, declaredFiles);
|
|
2999
3159
|
const job = config.reviewedBy === void 0 ? guarded : unchangedNoteGuard(named.name, guarded, writes);
|
|
3000
3160
|
if (config.reviewedBy === void 0) return job;
|
|
3161
|
+
const reviewRole = role(roles, config.reviewedBy);
|
|
3162
|
+
if (!Array.isArray(reviewRole) && "kind" in reviewRole && reviewRole.kind === "person") {
|
|
3163
|
+
if (!reviewRole.interaction) throw new TypeError("a human reviewer needs an interaction binding");
|
|
3164
|
+
const humanLoop = loop({
|
|
3165
|
+
name: `${named.name}-review`,
|
|
3166
|
+
body: job,
|
|
3167
|
+
max: refineCap(refineOf(config)) + 1,
|
|
3168
|
+
review: humanReview(named.name, {
|
|
3169
|
+
question: reviewRole.question,
|
|
3170
|
+
interaction: reviewRole.interaction,
|
|
3171
|
+
input: async (ctx) => Object.fromEntries(await Promise.all(writes.map(async (file) => [file, await readFile(join(ctx.workspace.dir, file), "utf8")])))
|
|
3172
|
+
})
|
|
3173
|
+
});
|
|
3174
|
+
return interactionDeclaration(copyJobMeta(async (ctx) => {
|
|
3175
|
+
const outcome = await humanLoop(ctx);
|
|
3176
|
+
return outcome.status === "exhausted" ? { ...outcome, status: "fail" } : outcome;
|
|
3177
|
+
}, humanLoop), { humanLoop });
|
|
3178
|
+
}
|
|
3001
3179
|
const reviewers = panelRole(roles, config.reviewedBy);
|
|
3002
3180
|
const panel = reviewerPanel(brief, named, reviewers, files, declaredFiles, void 0);
|
|
3003
3181
|
const refine = refineOf(config);
|
|
@@ -3058,9 +3236,27 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
|
|
|
3058
3236
|
}
|
|
3059
3237
|
if ("input" in config && config.input !== void 0) {
|
|
3060
3238
|
const personRole = inputRole(roles, config.input);
|
|
3061
|
-
return approval(named.name, { question: personRole.question, target: config.sendsBackTo });
|
|
3239
|
+
if (!personRole.interaction) return approval(named.name, { question: personRole.question, target: config.sendsBackTo });
|
|
3240
|
+
return async (ctx) => {
|
|
3241
|
+
const identity = interactionIdentity({ brief, named, personRole });
|
|
3242
|
+
const checkpointPath = [...ctx.path, "@person-interaction"];
|
|
3243
|
+
const input = jsonSnapshot({ requester: { path: ctx.path, identity }, material: brief });
|
|
3244
|
+
const result = await requestInteraction(personRole.interaction, personRole.question, input, {
|
|
3245
|
+
...ctx,
|
|
3246
|
+
interactionCheckpoint() {
|
|
3247
|
+
ctx.interactionCheckpoint?.();
|
|
3248
|
+
checkpointInteraction(ctx, checkpointPath, identity, input);
|
|
3249
|
+
}
|
|
3250
|
+
});
|
|
3251
|
+
if ("paused" in result) return result.paused;
|
|
3252
|
+
checkpointInteraction(ctx, checkpointPath, identity, null);
|
|
3253
|
+
return { status: "pass", summary: result.response.prompt, data: result.response };
|
|
3254
|
+
};
|
|
3062
3255
|
}
|
|
3063
|
-
|
|
3256
|
+
if ("fn" in config && config.fn !== void 0) {
|
|
3257
|
+
return guardedFn(config.fn, named, declaredFiles);
|
|
3258
|
+
}
|
|
3259
|
+
throw new TypeError(`stage ${named.name} must declare agent, run, panel, input or fn`);
|
|
3064
3260
|
}
|
|
3065
3261
|
function workflowRecord(outcome) {
|
|
3066
3262
|
return {
|
|
@@ -3085,26 +3281,13 @@ function panelReviewFiles(stages, index) {
|
|
|
3085
3281
|
const targetWrites = target === void 0 ? [] : writesOf(target.config);
|
|
3086
3282
|
return targetWrites.length ? targetWrites : preceding === void 0 ? [] : writesOf(preceding.config);
|
|
3087
3283
|
}
|
|
3088
|
-
function canonicalForDigest(value) {
|
|
3089
|
-
if (typeof value === "function") return `fn:${value.toString()}`;
|
|
3090
|
-
if (Array.isArray(value)) return value.map(canonicalForDigest);
|
|
3091
|
-
if (value !== null && typeof value === "object") {
|
|
3092
|
-
return Object.fromEntries(
|
|
3093
|
-
Object.keys(value).sort().map((key) => [
|
|
3094
|
-
key,
|
|
3095
|
-
canonicalForDigest(value[key])
|
|
3096
|
-
])
|
|
3097
|
-
);
|
|
3098
|
-
}
|
|
3099
|
-
return value;
|
|
3100
|
-
}
|
|
3101
3284
|
function resumeIdentity(name, config) {
|
|
3102
3285
|
const declared = {
|
|
3103
3286
|
name,
|
|
3104
3287
|
brief: config.brief,
|
|
3105
3288
|
stages: config.stages.map((named) => ({
|
|
3106
3289
|
name: named.name,
|
|
3107
|
-
config:
|
|
3290
|
+
config: interactionIdentity(named.config)
|
|
3108
3291
|
})),
|
|
3109
3292
|
roles: Object.fromEntries(Object.entries(config.roles).map(([key, value]) => [
|
|
3110
3293
|
key,
|
|
@@ -3139,6 +3322,10 @@ function resumeGuard(job, identity, label, retrySafe) {
|
|
|
3139
3322
|
const parentPath = ctx.path.slice(0, -1).join("/");
|
|
3140
3323
|
const anchor = resumed?.anchors.get(parentPath);
|
|
3141
3324
|
const recorded = anchor?.identity === identity && anchor.workspace === ctx.workspace.dir ? resumed?.stages.get(ctx.path.join("/")) : void 0;
|
|
3325
|
+
if (anchor?.identity === identity && anchor.workspace === ctx.workspace.dir && hasSavedInteraction(ctx, ctx.path)) {
|
|
3326
|
+
restoreRecordedUsage(ctx);
|
|
3327
|
+
return delegateNodeJob(ctx, guarded, job, ctx);
|
|
3328
|
+
}
|
|
3142
3329
|
if (ctx.graph?.attempt === 1 && recorded !== void 0) {
|
|
3143
3330
|
if (recorded.kind === "interrupted") {
|
|
3144
3331
|
if (!retrySafe) {
|
|
@@ -3162,7 +3349,7 @@ function resumeGuard(job, identity, label, retrySafe) {
|
|
|
3162
3349
|
}
|
|
3163
3350
|
return delegateNodeJob(ctx, guarded, job, ctx);
|
|
3164
3351
|
};
|
|
3165
|
-
return guarded;
|
|
3352
|
+
return interactionDeclaration(guarded, { identity, label, job });
|
|
3166
3353
|
}
|
|
3167
3354
|
async function reconcileInterrupted(ctx, owner, job, label, identity, recordId, startLine) {
|
|
3168
3355
|
const question = approval(`reconcile ${label}`, {
|
|
@@ -3233,10 +3420,12 @@ function workflow(name, config) {
|
|
|
3233
3420
|
for (const [index, named] of config.stages.entries()) {
|
|
3234
3421
|
const stageConfig = named.config;
|
|
3235
3422
|
if ("agent" in stageConfig && stageConfig.agent !== void 0 && stageConfig.reviewedBy !== void 0) {
|
|
3236
|
-
|
|
3423
|
+
const reviewer = role(config.roles, stageConfig.reviewedBy);
|
|
3424
|
+
if (Array.isArray(reviewer)) assertDistinctSeats([
|
|
3237
3425
|
seatRole(config.roles, stageConfig.agent),
|
|
3238
3426
|
...panelRole(config.roles, stageConfig.reviewedBy)
|
|
3239
3427
|
]);
|
|
3428
|
+
else if (!("kind" in reviewer) || reviewer.kind !== "person") throw new TypeError("reviewedBy must name a reviewer panel or person");
|
|
3240
3429
|
}
|
|
3241
3430
|
if ("panel" in stageConfig && stageConfig.panel !== void 0) {
|
|
3242
3431
|
const preceding = precedingAgent(config.stages, index);
|