@obversa/runtime 0.1.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 +3 -0
- package/dist/api.js +502 -358
- package/dist/api.js.map +1 -1
- package/dist/bin/record.d.ts +2 -0
- package/dist/bin/record.js +24 -0
- package/dist/bin/record.js.map +1 -0
- package/dist/{chunk-DV5P4QLI.js → chunk-G2OIKOUI.js} +277 -66
- package/dist/chunk-G2OIKOUI.js.map +1 -0
- package/dist/chunk-XDMDT67Q.js +598 -0
- package/dist/chunk-XDMDT67Q.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 +119 -0
- package/dist/core/types.d.ts +93 -3
- package/dist/runtime/monitor.d.ts +8 -5
- package/dist/runtime/persist.d.ts +4 -17
- package/dist/runtime/render-record.d.ts +68 -0
- package/dist/workflow-support.js +1 -1
- package/dist/workflow.d.ts +23 -3
- package/package.json +7 -4
- package/dist/chunk-DV5P4QLI.js.map +0 -1
package/dist/api.js
CHANGED
|
@@ -1,25 +1,27 @@
|
|
|
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, 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';
|
|
8
8
|
export { resolveGraphPlan, validateGraphDescription } from './chunk-3L6YNPN6.js';
|
|
9
9
|
import { cloneFrozenJson, JsonValueError, digestJson, canonicalJson, GraphValidationError } from './chunk-5GLEABOU.js';
|
|
10
10
|
export { GraphValidationError, JsonValueError } from './chunk-5GLEABOU.js';
|
|
11
|
-
import {
|
|
11
|
+
import { newRunId, startSupervisor, formatEvent, runningTotal } from './chunk-XDMDT67Q.js';
|
|
12
|
+
export { formatEvent, renderRecord, summarizeRecord } from './chunk-XDMDT67Q.js';
|
|
13
|
+
import { randomUUID, createHash } from 'crypto';
|
|
12
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';
|
|
13
15
|
export { ApprovalSubjectError, EngineError, EngineIncompleteResultError, GraphExecutionError, LANE_DEAD_FAILURES, acceptedResultMatches, approvalSubjectDigest, classifyEngineFailure, createApprovalCallbackGate, validateAcceptedResultRecord, validateApprovalRecord, validateCallbackEvent } from '@obversa/api';
|
|
14
16
|
import { readFileSync, mkdirSync, existsSync, writeFileSync, appendFileSync, lstatSync, realpathSync } from 'fs';
|
|
15
|
-
import { mkdir, stat, open, rename, rm, realpath,
|
|
17
|
+
import { mkdir, readFile, stat, open, rename, rm, realpath, lstat } from 'fs/promises';
|
|
16
18
|
import { join, resolve, basename, extname, dirname, isAbsolute, relative, sep } from 'path';
|
|
17
19
|
import pLimit from 'p-limit';
|
|
18
20
|
import toposort from 'toposort';
|
|
19
21
|
import '@obversa/core/command';
|
|
20
|
-
import { homedir } from 'os';
|
|
21
22
|
import { createServer } from 'http';
|
|
22
23
|
import { isDeepStrictEqual } from 'util';
|
|
24
|
+
import { homedir } from 'os';
|
|
23
25
|
import { hasExactFields, isObject, bindingFromSubject } from '@obversa/api/approval-support';
|
|
24
26
|
import { acceptedResultSupport } from '@obversa/api/accepted-result-support';
|
|
25
27
|
import { fileURLToPath } from 'url';
|
|
@@ -119,6 +121,139 @@ Write a concise MERGE SYNTHESIS for the commit body: what this line of work acco
|
|
|
119
121
|
return requireFinalResultText(out).trim();
|
|
120
122
|
}
|
|
121
123
|
|
|
124
|
+
// src/core/judge.ts
|
|
125
|
+
function stopQuestions(what = "the draft") {
|
|
126
|
+
return {
|
|
127
|
+
holds: {
|
|
128
|
+
type: "noul",
|
|
129
|
+
instructions: `For this use case, does ${what} hold as it stands?`,
|
|
130
|
+
criteria: {
|
|
131
|
+
true: "The latest findings are nits, taste or nothing, and a reader of this kind of work would not notice what remains.",
|
|
132
|
+
false: "The latest findings include a block, or shoulds a reader of this kind of work would notice."
|
|
133
|
+
}
|
|
134
|
+
},
|
|
135
|
+
worth_doing: {
|
|
136
|
+
type: "noul",
|
|
137
|
+
instructions: "Read the latest findings themselves. For this use case, are they worth acting on?",
|
|
138
|
+
criteria: {
|
|
139
|
+
true: "They name things a reader of this kind of work would stumble on, misread or distrust.",
|
|
140
|
+
false: "They are taste, edge cases, or polish past the bar the use case sets; acting on them would not change what a reader gets."
|
|
141
|
+
}
|
|
142
|
+
},
|
|
143
|
+
worth_another_round: {
|
|
144
|
+
type: "noul",
|
|
145
|
+
instructions: "Given the whole history and whether the latest findings are worth doing, is another round worth it?",
|
|
146
|
+
criteria: {
|
|
147
|
+
true: "The findings are worth doing, and the rounds so far have fixed what was found, so one more pass would land them.",
|
|
148
|
+
false: "The findings are not worth doing for this use case; or the same class of finding keeps returning; or the change between rounds is small; or the work is being polished past the bar."
|
|
149
|
+
}
|
|
150
|
+
},
|
|
151
|
+
stop_reason: {
|
|
152
|
+
type: "choice",
|
|
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
|
+
criteria: {
|
|
155
|
+
holds: `${what} holds for this use case.`,
|
|
156
|
+
over_polishing: "The remaining findings are taste, nits or edge cases past the bar.",
|
|
157
|
+
not_converging: "The same class of finding keeps returning, so another round will not fix it.",
|
|
158
|
+
continue: "Another round is worth it.",
|
|
159
|
+
product_decision: "A person must settle a product decision before this review can continue."
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
function judge(seat, opts) {
|
|
165
|
+
if (!Number.isSafeInteger(opts.cap) || opts.cap < 1) {
|
|
166
|
+
throw new TypeError("judge cap must be a positive integer");
|
|
167
|
+
}
|
|
168
|
+
return { kind: "judge", seat, cap: opts.cap, questions: opts.questions ?? stopQuestions(), ...opts.interaction ? { interaction: opts.interaction } : {} };
|
|
169
|
+
}
|
|
170
|
+
function isJudge(value) {
|
|
171
|
+
return typeof value === "object" && value !== null && value.kind === "judge";
|
|
172
|
+
}
|
|
173
|
+
function hasBlockFinding(findings) {
|
|
174
|
+
return (findings ?? []).some((finding) => normalizeFeedbackSeverity(finding.severity) === "block");
|
|
175
|
+
}
|
|
176
|
+
var SEVERITIES = ["block", "should-fix", "nice-to-have", "approve"];
|
|
177
|
+
function countBySeverity(findings) {
|
|
178
|
+
const counts = Object.fromEntries(SEVERITIES.map((severity) => [severity, 0]));
|
|
179
|
+
for (const finding of findings) counts[normalizeFeedbackSeverity(finding.severity)] += 1;
|
|
180
|
+
return counts;
|
|
181
|
+
}
|
|
182
|
+
function judgeDecision(answers) {
|
|
183
|
+
const worth = answers.worth_another_round?.noul ?? answers.worth_another_round?.probability;
|
|
184
|
+
const doing = answers.worth_doing?.noul ?? answers.worth_doing?.probability;
|
|
185
|
+
const holds = answers.holds?.noul ?? answers.holds?.probability;
|
|
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
|
+
}
|
|
191
|
+
if (reason !== "unknown" && reason !== "continue") {
|
|
192
|
+
return { again: false, stop: "fail", reason: `the judge chose ${reason}` };
|
|
193
|
+
}
|
|
194
|
+
if (typeof holds === "number" && holds >= 0.5) {
|
|
195
|
+
return { again: false, stop: "ship", reason: `the judge says it holds (${holds.toFixed(2)})` };
|
|
196
|
+
}
|
|
197
|
+
if (typeof doing === "number" && doing < 0.5) {
|
|
198
|
+
return { again: false, stop: "ship", reason: `the judge says the findings are not worth doing (${doing.toFixed(2)})` };
|
|
199
|
+
}
|
|
200
|
+
if (typeof worth === "number" && worth < 0.5) {
|
|
201
|
+
return { again: false, stop: "fail", reason: `the judge says another round is not worth it (${worth.toFixed(2)})` };
|
|
202
|
+
}
|
|
203
|
+
return {
|
|
204
|
+
again: true,
|
|
205
|
+
reason: `the judge says another round is worth it${typeof worth === "number" ? ` (${worth.toFixed(2)})` : ""}`
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
async function askJudge(cfg, state, ctx, path) {
|
|
209
|
+
const judgeJob = agentJob({
|
|
210
|
+
label: "refine:judge",
|
|
211
|
+
engine: cfg.seat.engine,
|
|
212
|
+
model: cfg.seat.identity.model,
|
|
213
|
+
workspaceMode: "none",
|
|
214
|
+
tools: [],
|
|
215
|
+
leaf: true,
|
|
216
|
+
prompt: JSON.stringify({ state, questions: cfg.questions })
|
|
217
|
+
});
|
|
218
|
+
const outcome = await judgeJob({ ...ctx, depth: ctx.depth + 1, path: [...path, "refine-judge"] });
|
|
219
|
+
let answers = {};
|
|
220
|
+
if (outcome.status === "pass") {
|
|
221
|
+
try {
|
|
222
|
+
const parsed = JSON.parse(String(outcome.data ?? ""));
|
|
223
|
+
if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
|
|
224
|
+
answers = parsed;
|
|
225
|
+
}
|
|
226
|
+
} catch {
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const decision = judgeDecision(answers);
|
|
230
|
+
ctx.emit({ kind: "refine:judge", ts: Date.now(), path: [...path], answers, reason: decision.reason });
|
|
231
|
+
return { answers, decision };
|
|
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
|
+
}
|
|
256
|
+
|
|
122
257
|
// src/core/dag.ts
|
|
123
258
|
function slug(s) {
|
|
124
259
|
return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "node";
|
|
@@ -158,10 +293,11 @@ function validateKickbackBudget(budget, nodes) {
|
|
|
158
293
|
message: `dag maxKickbacks names unknown target "${target}"`
|
|
159
294
|
});
|
|
160
295
|
}
|
|
296
|
+
if (isJudge(limit2)) continue;
|
|
161
297
|
if (!Number.isInteger(limit2) || limit2 < 0) {
|
|
162
298
|
throw new LoopError({
|
|
163
299
|
code: "CONFIG",
|
|
164
|
-
message: `dag maxKickbacks for "${target}" must be a non-negative integer`
|
|
300
|
+
message: `dag maxKickbacks for "${target}" must be a non-negative integer or a judge`
|
|
165
301
|
});
|
|
166
302
|
}
|
|
167
303
|
}
|
|
@@ -220,8 +356,18 @@ function dag(config) {
|
|
|
220
356
|
const stopOnError = config.stopOnError ?? true;
|
|
221
357
|
const maxKickbacks = config.maxKickbacks ?? 0;
|
|
222
358
|
const perTargetBudget = typeof maxKickbacks !== "number";
|
|
223
|
-
const
|
|
359
|
+
const targetJudge = (target) => {
|
|
360
|
+
if (typeof maxKickbacks === "number") return void 0;
|
|
361
|
+
const budget = maxKickbacks[target];
|
|
362
|
+
return isJudge(budget) ? budget : void 0;
|
|
363
|
+
};
|
|
364
|
+
const targetLimit = (target) => {
|
|
365
|
+
if (typeof maxKickbacks === "number") return maxKickbacks;
|
|
366
|
+
const budget = maxKickbacks[target] ?? 0;
|
|
367
|
+
return isJudge(budget) ? budget.cap : budget;
|
|
368
|
+
};
|
|
224
369
|
const routeKickbacks = config.maxKickbacks !== void 0 && (perTargetBudget || maxKickbacks > 0);
|
|
370
|
+
const identity = interactionIdentity(config);
|
|
225
371
|
const dependents = new Map(names.map((n) => [n, []]));
|
|
226
372
|
for (const [dep, name] of edges) dependents.get(dep).push(name);
|
|
227
373
|
const ancestorsOf = (name) => {
|
|
@@ -253,12 +399,17 @@ function dag(config) {
|
|
|
253
399
|
const path = [...parent.path, config.name];
|
|
254
400
|
const depth = parent.depth + 1;
|
|
255
401
|
const ts = () => Date.now();
|
|
256
|
-
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 ?? {}));
|
|
257
408
|
parent.emit({ kind: "dag:start", ts: ts(), path, depth, nodes: names });
|
|
258
409
|
const limit2 = pLimit(limitN);
|
|
259
|
-
const results =
|
|
260
|
-
const memo =
|
|
261
|
-
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 ?? {}));
|
|
262
413
|
let stopped = false;
|
|
263
414
|
const pendingKickback = /* @__PURE__ */ new Map();
|
|
264
415
|
const nodeContext = (name) => {
|
|
@@ -497,8 +648,8 @@ function dag(config) {
|
|
|
497
648
|
};
|
|
498
649
|
await Promise.all(names.map(run2));
|
|
499
650
|
if (routeKickbacks) {
|
|
500
|
-
let used = 0;
|
|
501
|
-
const rejected =
|
|
651
|
+
let used = Number(saved?.used ?? 0);
|
|
652
|
+
const rejected = new Set(saved?.rejected ?? []);
|
|
502
653
|
const emitKickback = (from, to, reason, accepted, count, limit3, note) => parent.emit({
|
|
503
654
|
kind: "dag:kickback",
|
|
504
655
|
ts: ts(),
|
|
@@ -523,7 +674,7 @@ function dag(config) {
|
|
|
523
674
|
const request = revisionFromOutcome(results.get(from));
|
|
524
675
|
const to = request.target;
|
|
525
676
|
const { reason } = request;
|
|
526
|
-
const count = (targetCounts.get(to) ?? 0) + 1;
|
|
677
|
+
const count = pending?.from === from ? pending.count : (targetCounts.get(to) ?? 0) + 1;
|
|
527
678
|
targetCounts.set(to, count);
|
|
528
679
|
const limit3 = targetLimit(to);
|
|
529
680
|
const allow = nodes.get(from).acceptsKickbackTo;
|
|
@@ -533,6 +684,71 @@ function dag(config) {
|
|
|
533
684
|
emitKickback(from, to, reason, false, count, limit3, note);
|
|
534
685
|
continue;
|
|
535
686
|
}
|
|
687
|
+
const requestFindings = request.findings ?? [];
|
|
688
|
+
const cfgJudge = targetJudge(to);
|
|
689
|
+
const withinBudget = perTargetBudget ? count <= limit3 : used < limit3;
|
|
690
|
+
let effectiveReason = reason;
|
|
691
|
+
if (cfgJudge !== void 0 && withinBudget && !hasBlockFinding(requestFindings)) {
|
|
692
|
+
const history = judgeHistory.get(to) ?? [];
|
|
693
|
+
const state = pending?.from === from ? pending.state : {
|
|
694
|
+
...productFeedback.has(to) ? { productFeedback: productFeedback.get(to) } : {},
|
|
695
|
+
latestFindings: requestFindings,
|
|
696
|
+
rounds: history,
|
|
697
|
+
round: count,
|
|
698
|
+
cap: cfgJudge.cap
|
|
699
|
+
};
|
|
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;
|
|
722
|
+
judgeHistory.set(to, [
|
|
723
|
+
...history,
|
|
724
|
+
{ round: count, findings: requestFindings, counts: countBySeverity(requestFindings) }
|
|
725
|
+
]);
|
|
726
|
+
if (!decision.again) {
|
|
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);
|
|
748
|
+
continue;
|
|
749
|
+
}
|
|
750
|
+
effectiveReason = `${reason} (${decision.reason})`;
|
|
751
|
+
}
|
|
536
752
|
if (perTargetBudget ? count > limit3 : used >= limit3) {
|
|
537
753
|
emitKickback(
|
|
538
754
|
from,
|
|
@@ -550,7 +766,7 @@ function dag(config) {
|
|
|
550
766
|
break;
|
|
551
767
|
}
|
|
552
768
|
used += 1;
|
|
553
|
-
emitKickback(from, to,
|
|
769
|
+
emitKickback(from, to, effectiveReason, true, count, limit3);
|
|
554
770
|
const dirty = dirtyFrom(to);
|
|
555
771
|
for (const d of dirty) {
|
|
556
772
|
memo.delete(d);
|
|
@@ -559,8 +775,8 @@ function dag(config) {
|
|
|
559
775
|
}
|
|
560
776
|
pendingKickback.set(to, {
|
|
561
777
|
status: "fail",
|
|
562
|
-
summary: `Kicked back from "${from}": ${
|
|
563
|
-
revision: { ...request, source: request.source ?? from }
|
|
778
|
+
summary: `Kicked back from "${from}": ${effectiveReason}`,
|
|
779
|
+
revision: { ...request, reason: effectiveReason, source: request.source ?? from }
|
|
564
780
|
});
|
|
565
781
|
stopped = false;
|
|
566
782
|
await Promise.all(names.map(run2));
|
|
@@ -608,7 +824,7 @@ function dag(config) {
|
|
|
608
824
|
parent.emit({ kind: "dag:end", ts: ts(), path, outcome });
|
|
609
825
|
return outcome;
|
|
610
826
|
};
|
|
611
|
-
return setMeta(job, {
|
|
827
|
+
return interactionDeclaration(setMeta(job, {
|
|
612
828
|
kind: "dag",
|
|
613
829
|
name: config.name,
|
|
614
830
|
...config.maxKickbacks !== void 0 ? { maxKickbacks: config.maxKickbacks } : {},
|
|
@@ -629,7 +845,7 @@ function dag(config) {
|
|
|
629
845
|
job: jobMeta(nodeJob)
|
|
630
846
|
};
|
|
631
847
|
})
|
|
632
|
-
});
|
|
848
|
+
}), config);
|
|
633
849
|
}
|
|
634
850
|
function sequence(name, ...jobs) {
|
|
635
851
|
const nodes = {};
|
|
@@ -802,9 +1018,15 @@ function loop(config) {
|
|
|
802
1018
|
const path = [...parent.path, config.name];
|
|
803
1019
|
const depth = parent.depth + 1;
|
|
804
1020
|
const ts = () => Date.now();
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
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;
|
|
808
1030
|
const ctxAt = (iter, lastOutcome) => childContext(parent, {
|
|
809
1031
|
depth,
|
|
810
1032
|
path,
|
|
@@ -815,6 +1037,7 @@ function loop(config) {
|
|
|
815
1037
|
});
|
|
816
1038
|
parent.emit({ kind: "loop:start", ts: ts(), path, depth, max: config.max });
|
|
817
1039
|
const finish = async (outcome, iterations) => {
|
|
1040
|
+
if (checkpointed && outcome.status !== "paused" && outcome.status !== "aborted") checkpointInteraction(parent, checkpointPath, identity, null);
|
|
818
1041
|
parent.emit({ kind: "loop:end", ts: ts(), path, outcome, iterations });
|
|
819
1042
|
if (config.onComplete) {
|
|
820
1043
|
try {
|
|
@@ -861,7 +1084,7 @@ function loop(config) {
|
|
|
861
1084
|
config.until ? prepareCondition(config.until, entryContext) : void 0,
|
|
862
1085
|
config.stopOn ? prepareCondition(config.stopOn, entryContext) : void 0
|
|
863
1086
|
]);
|
|
864
|
-
if (start) {
|
|
1087
|
+
if (start && !saved) {
|
|
865
1088
|
const r = await gate(start, "start", ctxAt(0), void 0);
|
|
866
1089
|
parent.emit({
|
|
867
1090
|
kind: "loop:condition",
|
|
@@ -877,11 +1100,30 @@ function loop(config) {
|
|
|
877
1100
|
0
|
|
878
1101
|
);
|
|
879
1102
|
}
|
|
880
|
-
let last;
|
|
881
|
-
let consecutiveErrors = 0;
|
|
882
|
-
let consecutiveReviewFails = 0;
|
|
1103
|
+
let last = saved?.last;
|
|
1104
|
+
let consecutiveErrors = Number(saved?.consecutiveErrors ?? 0);
|
|
1105
|
+
let consecutiveReviewFails = Number(saved?.consecutiveReviewFails ?? 0);
|
|
883
1106
|
const tracker = noProgress ? new ProgressTracker(noProgress) : void 0;
|
|
1107
|
+
const samples = [...saved?.samples ?? []];
|
|
1108
|
+
for (const sample of samples) tracker?.record(sample);
|
|
884
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
|
+
});
|
|
885
1127
|
const evaluateConvergence = async (at, gateContext, bodyOutcome) => {
|
|
886
1128
|
const conv = until ? await gate(until, "until", gateContext, bodyOutcome) : {
|
|
887
1129
|
met: bodyOutcome.status === "pass",
|
|
@@ -926,7 +1168,7 @@ function loop(config) {
|
|
|
926
1168
|
}
|
|
927
1169
|
let reviewOutcome;
|
|
928
1170
|
try {
|
|
929
|
-
reviewOutcome = await config.review(ctxAt(at, bodyOutcome));
|
|
1171
|
+
reviewOutcome = await config.review(checkpointContext(ctxAt(at, bodyOutcome), "review"));
|
|
930
1172
|
} catch (e) {
|
|
931
1173
|
throw LoopError.from(e, {
|
|
932
1174
|
code: "VALIDATION",
|
|
@@ -993,7 +1235,7 @@ function loop(config) {
|
|
|
993
1235
|
}
|
|
994
1236
|
return { conv, turnReview: reviewOutcome };
|
|
995
1237
|
};
|
|
996
|
-
if (config.checkFirst) {
|
|
1238
|
+
if (config.checkFirst && !saved) {
|
|
997
1239
|
await yieldToLoop();
|
|
998
1240
|
if (parent.signal.aborted)
|
|
999
1241
|
return finish(
|
|
@@ -1010,7 +1252,7 @@ function loop(config) {
|
|
|
1010
1252
|
{ status: "aborted", summary: "aborted by signal" },
|
|
1011
1253
|
iteration
|
|
1012
1254
|
);
|
|
1013
|
-
if (config.max != null && iteration >= config.max) {
|
|
1255
|
+
if (resumePhase !== "review" && config.max != null && iteration >= config.max) {
|
|
1014
1256
|
return finish(
|
|
1015
1257
|
{
|
|
1016
1258
|
status: "exhausted",
|
|
@@ -1022,13 +1264,15 @@ function loop(config) {
|
|
|
1022
1264
|
iteration
|
|
1023
1265
|
);
|
|
1024
1266
|
}
|
|
1025
|
-
|
|
1267
|
+
const resumingReview = resumePhase === "review";
|
|
1268
|
+
resumePhase = void 0;
|
|
1269
|
+
if (!resumingReview) iteration += 1;
|
|
1026
1270
|
const ctx = ctxAt(iteration, last);
|
|
1027
1271
|
let turnReview;
|
|
1028
|
-
parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
|
|
1272
|
+
if (!resumingReview) parent.emit({ kind: "loop:iteration", ts: ts(), path, iteration });
|
|
1029
1273
|
let bodyThrew = false;
|
|
1030
1274
|
try {
|
|
1031
|
-
last = await config.body(ctx);
|
|
1275
|
+
if (!resumingReview) last = await config.body(checkpointContext(ctx, "body"));
|
|
1032
1276
|
consecutiveErrors = 0;
|
|
1033
1277
|
} catch (e) {
|
|
1034
1278
|
bodyThrew = true;
|
|
@@ -1119,7 +1363,7 @@ function loop(config) {
|
|
|
1119
1363
|
iteration
|
|
1120
1364
|
);
|
|
1121
1365
|
}
|
|
1122
|
-
if (config.onIteration) {
|
|
1366
|
+
if (config.onIteration && !resumingReview) {
|
|
1123
1367
|
try {
|
|
1124
1368
|
await config.onIteration(last, ctx);
|
|
1125
1369
|
} catch (e) {
|
|
@@ -1131,7 +1375,7 @@ function loop(config) {
|
|
|
1131
1375
|
});
|
|
1132
1376
|
}
|
|
1133
1377
|
}
|
|
1134
|
-
if (stopOn) {
|
|
1378
|
+
if (stopOn && !resumingReview) {
|
|
1135
1379
|
const r = await gate(stopOn, "stopOn", ctx, last);
|
|
1136
1380
|
parent.emit({
|
|
1137
1381
|
kind: "loop:condition",
|
|
@@ -1179,7 +1423,7 @@ function loop(config) {
|
|
|
1179
1423
|
});
|
|
1180
1424
|
}
|
|
1181
1425
|
}
|
|
1182
|
-
const
|
|
1426
|
+
const sample = {
|
|
1183
1427
|
iteration,
|
|
1184
1428
|
fingerprint,
|
|
1185
1429
|
signal: signalValue,
|
|
@@ -1188,7 +1432,9 @@ function loop(config) {
|
|
|
1188
1432
|
gate: noProgress.gate && until && !conv.met ? conv.output : void 0,
|
|
1189
1433
|
confidence: turnReview?.confidence ?? conv.confidence ?? last.confidence,
|
|
1190
1434
|
reason: turnReview ? turnReview.summary ?? "review rejected" : conv.reason
|
|
1191
|
-
}
|
|
1435
|
+
};
|
|
1436
|
+
samples.push(Object.fromEntries(Object.entries(sample).filter(([, value]) => value !== void 0)));
|
|
1437
|
+
const report = tracker.record(sample);
|
|
1192
1438
|
if (!warnedInert && tracker.isInert()) {
|
|
1193
1439
|
warnedInert = true;
|
|
1194
1440
|
parent.log(
|
|
@@ -1244,7 +1490,7 @@ function loop(config) {
|
|
|
1244
1490
|
);
|
|
1245
1491
|
}
|
|
1246
1492
|
};
|
|
1247
|
-
return setMeta(job, {
|
|
1493
|
+
return interactionDeclaration(setMeta(job, {
|
|
1248
1494
|
kind: "loop",
|
|
1249
1495
|
name: config.name,
|
|
1250
1496
|
max: config.max,
|
|
@@ -1256,7 +1502,7 @@ function loop(config) {
|
|
|
1256
1502
|
review: !!config.review,
|
|
1257
1503
|
maxReviewRestarts: config.maxReviewRestarts,
|
|
1258
1504
|
body: jobMeta(config.body)
|
|
1259
|
-
});
|
|
1505
|
+
}), config);
|
|
1260
1506
|
}
|
|
1261
1507
|
function delay(ms, signal) {
|
|
1262
1508
|
if (signal.aborted) return Promise.resolve();
|
|
@@ -1475,11 +1721,13 @@ var NOISE = /* @__PURE__ */ new Set([
|
|
|
1475
1721
|
"engine:thinking"
|
|
1476
1722
|
]);
|
|
1477
1723
|
function readResumeRecord(path) {
|
|
1724
|
+
const receipts = [];
|
|
1725
|
+
const interactions = /* @__PURE__ */ new Map();
|
|
1478
1726
|
const anchors = /* @__PURE__ */ new Map();
|
|
1479
1727
|
const stages = /* @__PURE__ */ new Map();
|
|
1480
1728
|
const usage2 = /* @__PURE__ */ new Map();
|
|
1481
1729
|
const started = /* @__PURE__ */ new Set();
|
|
1482
|
-
if (!existsSync(path)) return { outcomes: { anchors, stages }, usage: usage2 };
|
|
1730
|
+
if (!existsSync(path)) return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
|
|
1483
1731
|
const lines = readFileSync(path, "utf8").split(/\r?\n/);
|
|
1484
1732
|
for (const [lineNumber, line] of lines.entries()) {
|
|
1485
1733
|
if (!line) continue;
|
|
@@ -1489,11 +1737,19 @@ function readResumeRecord(path) {
|
|
|
1489
1737
|
} catch {
|
|
1490
1738
|
continue;
|
|
1491
1739
|
}
|
|
1492
|
-
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") {
|
|
1493
1746
|
const key = event.path.join("/");
|
|
1494
1747
|
const prior = anchors.get(key);
|
|
1495
1748
|
if (prior?.identity !== event.identity || prior.workspace !== event.workspace) {
|
|
1496
1749
|
const prefix = key ? `${key}/` : "";
|
|
1750
|
+
for (const stage2 of interactions.keys()) {
|
|
1751
|
+
if (stage2.startsWith(prefix)) interactions.delete(stage2);
|
|
1752
|
+
}
|
|
1497
1753
|
for (const stage2 of stages.keys()) {
|
|
1498
1754
|
if (stage2.startsWith(prefix)) stages.delete(stage2);
|
|
1499
1755
|
}
|
|
@@ -1529,7 +1785,7 @@ function readResumeRecord(path) {
|
|
|
1529
1785
|
}
|
|
1530
1786
|
}
|
|
1531
1787
|
}
|
|
1532
|
-
return { outcomes: { anchors, stages }, usage: usage2 };
|
|
1788
|
+
return { outcomes: { anchors, stages, interactions }, usage: usage2, receipts };
|
|
1533
1789
|
}
|
|
1534
1790
|
function ensureDir(path) {
|
|
1535
1791
|
const dir = dirname(path);
|
|
@@ -1613,259 +1869,6 @@ function assertSafeDirectory(root, dir, label) {
|
|
|
1613
1869
|
throw new Error(`unsafe ${label}: resolves outside the workspace`);
|
|
1614
1870
|
}
|
|
1615
1871
|
}
|
|
1616
|
-
var NOISE2 = /* @__PURE__ */ new Set([
|
|
1617
|
-
"engine:text",
|
|
1618
|
-
"engine:thinking"
|
|
1619
|
-
]);
|
|
1620
|
-
function runsHome() {
|
|
1621
|
-
return join(process.env.OBVERSA_HOME ?? join(homedir(), ".obversa"), "runs");
|
|
1622
|
-
}
|
|
1623
|
-
function slug2(value) {
|
|
1624
|
-
return value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 32) || "run";
|
|
1625
|
-
}
|
|
1626
|
-
function newRunId(title) {
|
|
1627
|
-
return `${slug2(title)}-${randomBytes(3).toString("hex")}`;
|
|
1628
|
-
}
|
|
1629
|
-
function startSupervisor(input) {
|
|
1630
|
-
const dir = join(runsHome(), input.runId);
|
|
1631
|
-
const eventsPath = join(dir, "events.jsonl");
|
|
1632
|
-
const proofsPath = join(dir, "proofs.jsonl");
|
|
1633
|
-
const statusPath = join(dir, "status.json");
|
|
1634
|
-
mkdirSync(dir, { recursive: true });
|
|
1635
|
-
writeBestEffort(eventsPath, "");
|
|
1636
|
-
writeBestEffort(proofsPath, "");
|
|
1637
|
-
const startedAt = Date.now();
|
|
1638
|
-
const status = {
|
|
1639
|
-
runId: input.runId,
|
|
1640
|
-
pid: process.pid,
|
|
1641
|
-
cwd: input.cwd,
|
|
1642
|
-
title: input.title,
|
|
1643
|
-
startedAt,
|
|
1644
|
-
updatedAt: startedAt,
|
|
1645
|
-
status: "running",
|
|
1646
|
-
shape: input.shape,
|
|
1647
|
-
live: {
|
|
1648
|
-
path: [],
|
|
1649
|
-
iteration: 0,
|
|
1650
|
-
usage: {
|
|
1651
|
-
inputTokens: 0,
|
|
1652
|
-
outputTokens: 0,
|
|
1653
|
-
cacheReadInputTokens: 0,
|
|
1654
|
-
cacheCreationInputTokens: 0,
|
|
1655
|
-
calls: 0,
|
|
1656
|
-
unmeasuredCalls: 0
|
|
1657
|
-
}
|
|
1658
|
-
}
|
|
1659
|
-
};
|
|
1660
|
-
const active = /* @__PURE__ */ new Map();
|
|
1661
|
-
let proofCount = 0;
|
|
1662
|
-
const writeStatus = () => {
|
|
1663
|
-
status.updatedAt = Date.now();
|
|
1664
|
-
writeBestEffort(statusPath, JSON.stringify(status, null, 2));
|
|
1665
|
-
};
|
|
1666
|
-
const refreshCurrent = () => {
|
|
1667
|
-
status.live.active = active.size ? Object.fromEntries(active.entries()) : void 0;
|
|
1668
|
-
status.live.current = [...active.values()].sort((a, b) => {
|
|
1669
|
-
const deadline = (a.deadlineAt ?? Number.POSITIVE_INFINITY) - (b.deadlineAt ?? Number.POSITIVE_INFINITY);
|
|
1670
|
-
return deadline || b.startedAt - a.startedAt;
|
|
1671
|
-
})[0];
|
|
1672
|
-
};
|
|
1673
|
-
const activeKey = (kind, path) => `${kind}:${path.join("\0")}`;
|
|
1674
|
-
writeStatus();
|
|
1675
|
-
const sink = (event) => {
|
|
1676
|
-
if (!NOISE2.has(event.kind)) appendBestEffort(eventsPath, event);
|
|
1677
|
-
switch (event.kind) {
|
|
1678
|
-
case "run:end":
|
|
1679
|
-
status.live.lastOutcome = outcomeSummary(event.outcome);
|
|
1680
|
-
break;
|
|
1681
|
-
case "loop:iteration":
|
|
1682
|
-
status.live.path = event.path;
|
|
1683
|
-
status.live.iteration = event.iteration;
|
|
1684
|
-
break;
|
|
1685
|
-
case "loop:condition":
|
|
1686
|
-
status.live.lastGate = {
|
|
1687
|
-
which: event.which,
|
|
1688
|
-
met: event.result.met,
|
|
1689
|
-
confidence: event.result.confidence,
|
|
1690
|
-
reason: event.result.reason
|
|
1691
|
-
};
|
|
1692
|
-
break;
|
|
1693
|
-
case "condition:result":
|
|
1694
|
-
status.live.lastGate = {
|
|
1695
|
-
which: event.label,
|
|
1696
|
-
met: event.result.met,
|
|
1697
|
-
confidence: event.result.confidence,
|
|
1698
|
-
reason: event.result.reason
|
|
1699
|
-
};
|
|
1700
|
-
break;
|
|
1701
|
-
case "dag:node": {
|
|
1702
|
-
const path = [...event.path, event.node];
|
|
1703
|
-
status.live.path = path;
|
|
1704
|
-
if (event.phase === "start") {
|
|
1705
|
-
active.set(activeKey("dag-node", path), {
|
|
1706
|
-
kind: "dag-node",
|
|
1707
|
-
path,
|
|
1708
|
-
node: event.node,
|
|
1709
|
-
startedAt: event.ts,
|
|
1710
|
-
timeoutMs: event.timeoutMs,
|
|
1711
|
-
deadlineAt: event.timeoutMs ? event.ts + event.timeoutMs : void 0
|
|
1712
|
-
});
|
|
1713
|
-
} else {
|
|
1714
|
-
active.delete(activeKey("dag-node", path));
|
|
1715
|
-
}
|
|
1716
|
-
refreshCurrent();
|
|
1717
|
-
break;
|
|
1718
|
-
}
|
|
1719
|
-
case "job:start": {
|
|
1720
|
-
const path = [...event.path, event.label];
|
|
1721
|
-
active.set(activeKey("job", path), {
|
|
1722
|
-
kind: "job",
|
|
1723
|
-
path: event.path,
|
|
1724
|
-
label: event.label,
|
|
1725
|
-
startedAt: event.ts,
|
|
1726
|
-
timeoutMs: event.timeoutMs,
|
|
1727
|
-
deadlineAt: event.timeoutMs ? event.ts + event.timeoutMs : void 0
|
|
1728
|
-
});
|
|
1729
|
-
refreshCurrent();
|
|
1730
|
-
break;
|
|
1731
|
-
}
|
|
1732
|
-
case "loop:end":
|
|
1733
|
-
case "dag:end":
|
|
1734
|
-
case "job:end":
|
|
1735
|
-
status.live.path = event.path;
|
|
1736
|
-
status.live.lastOutcome = outcomeSummary(event.outcome);
|
|
1737
|
-
if (event.kind === "job:end") {
|
|
1738
|
-
active.delete(activeKey("job", [...event.path, event.label]));
|
|
1739
|
-
refreshCurrent();
|
|
1740
|
-
}
|
|
1741
|
-
break;
|
|
1742
|
-
case "engine:usage":
|
|
1743
|
-
status.live.usage.calls += 1;
|
|
1744
|
-
if (event.usage.kind === "unknown") {
|
|
1745
|
-
status.live.usage.unmeasuredCalls += 1;
|
|
1746
|
-
} else {
|
|
1747
|
-
status.live.usage.inputTokens += event.usage.inputTokens;
|
|
1748
|
-
status.live.usage.outputTokens += event.usage.outputTokens;
|
|
1749
|
-
status.live.usage.cacheReadInputTokens += event.usage.cacheReadInputTokens ?? 0;
|
|
1750
|
-
status.live.usage.cacheCreationInputTokens += event.usage.cacheCreationInputTokens ?? 0;
|
|
1751
|
-
}
|
|
1752
|
-
break;
|
|
1753
|
-
case "proof": {
|
|
1754
|
-
const proof = {
|
|
1755
|
-
name: event.name,
|
|
1756
|
-
path: event.path,
|
|
1757
|
-
artifact: event.artifact
|
|
1758
|
-
};
|
|
1759
|
-
proofCount += 1;
|
|
1760
|
-
appendBestEffort(proofsPath, proof);
|
|
1761
|
-
status.evidence = {
|
|
1762
|
-
count: proofCount,
|
|
1763
|
-
indexPath: proofsPath,
|
|
1764
|
-
latest: proof
|
|
1765
|
-
};
|
|
1766
|
-
break;
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
if (!NOISE2.has(event.kind)) writeStatus();
|
|
1770
|
-
};
|
|
1771
|
-
const finish = (outcome) => {
|
|
1772
|
-
status.status = outcome.status;
|
|
1773
|
-
status.endedAt = Date.now();
|
|
1774
|
-
status.live.current = void 0;
|
|
1775
|
-
status.live.active = void 0;
|
|
1776
|
-
status.live.lastOutcome = outcomeSummary(outcome);
|
|
1777
|
-
writeStatus();
|
|
1778
|
-
};
|
|
1779
|
-
return { runId: input.runId, dir, sink, finish };
|
|
1780
|
-
}
|
|
1781
|
-
function outcomeSummary(outcome) {
|
|
1782
|
-
return {
|
|
1783
|
-
status: outcome.status,
|
|
1784
|
-
summary: outcome.summary,
|
|
1785
|
-
late: outcome.late
|
|
1786
|
-
};
|
|
1787
|
-
}
|
|
1788
|
-
function writeBestEffort(path, text6) {
|
|
1789
|
-
try {
|
|
1790
|
-
writeFileSync(path, text6);
|
|
1791
|
-
} catch {
|
|
1792
|
-
}
|
|
1793
|
-
}
|
|
1794
|
-
function appendBestEffort(path, value) {
|
|
1795
|
-
try {
|
|
1796
|
-
appendFileSync(path, `${JSON.stringify(value)}
|
|
1797
|
-
`);
|
|
1798
|
-
} catch {
|
|
1799
|
-
}
|
|
1800
|
-
}
|
|
1801
|
-
function runningTotal(totals) {
|
|
1802
|
-
const read = totals.cacheReadInputTokens ?? 0;
|
|
1803
|
-
const unmeasured = totals.unmeasuredCalls ?? 0;
|
|
1804
|
-
const missing = unmeasured > 0 ? `, usage unknown on ${unmeasured} call${unmeasured === 1 ? "" : "s"}` : "";
|
|
1805
|
-
const base = `${totals.inputTokens}/${totals.outputTokens} tok`;
|
|
1806
|
-
return `${base}${read > 0 ? `, ${read} tok from cache` : ""}${missing}`;
|
|
1807
|
-
}
|
|
1808
|
-
function toLine(value) {
|
|
1809
|
-
return value.replace(/[\u0000-\u001f\u007f]+/g, " ");
|
|
1810
|
-
}
|
|
1811
|
-
function formatEvent(event, totals) {
|
|
1812
|
-
return toLine(renderEvent(event, totals));
|
|
1813
|
-
}
|
|
1814
|
-
function renderEvent(event, totals) {
|
|
1815
|
-
const at = event.path.length ? `${event.path.join(" \u203A ")} ` : "";
|
|
1816
|
-
switch (event.kind) {
|
|
1817
|
-
case "run:start":
|
|
1818
|
-
return "\u25B8 run";
|
|
1819
|
-
case "run:end":
|
|
1820
|
-
return `\u25C2 run ${event.outcome.status}${event.outcome.late ? " late" : ""} (${runningTotal(event.usage)})`;
|
|
1821
|
-
case "loop:start":
|
|
1822
|
-
return `${at}\u25B8 loop${event.max ? ` (max ${event.max})` : ""}`;
|
|
1823
|
-
case "dag:start":
|
|
1824
|
-
return `${at}\u25B8 dag (${event.nodes.length} nodes)`;
|
|
1825
|
-
case "loop:iteration":
|
|
1826
|
-
return `${at}\xB7 iteration ${event.iteration}`;
|
|
1827
|
-
case "loop:condition":
|
|
1828
|
-
return `${at}\xB7 ${event.which} ${event.result.met ? "met" : "not met"}: ${event.result.reason}`;
|
|
1829
|
-
case "condition:result":
|
|
1830
|
-
return `${at}\xB7 ${event.label} ${event.result.met ? "met" : "not met"}: ${event.result.reason}`;
|
|
1831
|
-
case "loop:review":
|
|
1832
|
-
return `${at}\xB7 review: ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
|
|
1833
|
-
case "loop:end":
|
|
1834
|
-
return `${at}\u25C2 ${event.outcome.status}${event.outcome.late ? " late" : ""} (${event.iterations} iter)`;
|
|
1835
|
-
case "dag:node":
|
|
1836
|
-
return `${at}\xB7 node ${event.node}: ${event.phase}${event.outcome ? ` (${event.outcome.status}${event.outcome.late ? " late" : ""})` : ""}`;
|
|
1837
|
-
case "dag:kickback":
|
|
1838
|
-
return `${at}\u21A9 kickback ${event.accepted ? "accepted" : "rejected"} ${event.from} -> ${event.to} [${event.count}/${event.limit}]: ${event.reason}${event.note ? ` (${event.note})` : ""}`;
|
|
1839
|
-
case "dag:end":
|
|
1840
|
-
return `${at}\u25C2 dag ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
|
|
1841
|
-
case "job:start":
|
|
1842
|
-
return `${at}\u2022 ${event.label}`;
|
|
1843
|
-
case "advisor:consult":
|
|
1844
|
-
return `${at}\u25C7 advisor ${event.label} #${event.call}: ${event.question}`;
|
|
1845
|
-
case "proof":
|
|
1846
|
-
return `${at}\u25C8 proof ${event.name}: ${event.artifact.title ?? event.artifact.path ?? event.artifact.kind}`;
|
|
1847
|
-
case "job:end":
|
|
1848
|
-
return `${at}\u2022 ${event.label}: ${event.outcome.status}${event.outcome.late ? " late" : ""}`;
|
|
1849
|
-
case "engine:tool":
|
|
1850
|
-
return `${at} tool ${event.name} ${event.phase}`;
|
|
1851
|
-
case "engine:usage": {
|
|
1852
|
-
const call = event.usage.kind === "unknown" ? "usage unknown" : `${event.usage.inputTokens}/${event.usage.outputTokens} tok`;
|
|
1853
|
-
return `${at} ${event.model}: ${call}${totals ? ` (run ${runningTotal(totals)})` : ""}`;
|
|
1854
|
-
}
|
|
1855
|
-
case "loop:stall":
|
|
1856
|
-
return `${at}\u23F9 stalled after ${event.report.iterations.length} no-progress iterations: ${event.report.reason}`;
|
|
1857
|
-
case "limit:wait":
|
|
1858
|
-
return `${at}\u23F8 limit ${event.code}: waiting ${Math.round(event.waitMs / 1e3)}s`;
|
|
1859
|
-
case "limit:pause":
|
|
1860
|
-
return `${at}\u23F8 paused (${event.code}): ${event.reason}`;
|
|
1861
|
-
case "log":
|
|
1862
|
-
return `${at}${event.message}`;
|
|
1863
|
-
case "error":
|
|
1864
|
-
return `${at}\u2717 ${event.code}: ${event.message}`;
|
|
1865
|
-
default:
|
|
1866
|
-
return `${at}${event.kind}`;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
1872
|
function applyEvent(states, event) {
|
|
1870
1873
|
switch (event.kind) {
|
|
1871
1874
|
case "callback-requested": {
|
|
@@ -2162,14 +2165,11 @@ var MonitorFold = class {
|
|
|
2162
2165
|
...event.note !== void 0 ? { note: event.note } : {}
|
|
2163
2166
|
});
|
|
2164
2167
|
}
|
|
2165
|
-
const line =
|
|
2166
|
-
if (
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
line.summary = event.outcome.summary;
|
|
2168
|
+
const line = formatEvent(event, this.usage);
|
|
2169
|
+
if (line !== "") {
|
|
2170
|
+
this.events.push({ ts: event.ts, line });
|
|
2171
|
+
if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
|
|
2170
2172
|
}
|
|
2171
|
-
this.events.push(line);
|
|
2172
|
-
if (this.events.length > EVENT_TAIL) this.events.splice(0, this.events.length - EVENT_TAIL);
|
|
2173
2173
|
}
|
|
2174
2174
|
finish(outcome) {
|
|
2175
2175
|
this.status = "done";
|
|
@@ -2359,6 +2359,15 @@ function page(name) {
|
|
|
2359
2359
|
(() => {
|
|
2360
2360
|
const el = (id) => document.getElementById(id);
|
|
2361
2361
|
const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c]));
|
|
2362
|
+
// What the question is about: the raw input, and a link when it names one,
|
|
2363
|
+
// so a reviewer can open the thing being approved instead of taking the
|
|
2364
|
+
// question's word for it.
|
|
2365
|
+
function aboutHtml(input) {
|
|
2366
|
+
if (input === null || input === undefined) return '';
|
|
2367
|
+
const url = input && typeof input === 'object' && !Array.isArray(input) && typeof input.url === 'string' ? input.url : null;
|
|
2368
|
+
const link = url ? '<p><a href="' + esc(url) + '" target="_blank" rel="noopener">' + esc(url) + '</a></p>' : '';
|
|
2369
|
+
return '<pre>' + esc(JSON.stringify(input, null, 2)) + '</pre>' + link;
|
|
2370
|
+
}
|
|
2362
2371
|
async function answer(requestId, approved) {
|
|
2363
2372
|
const box = document.querySelector('[data-note="' + CSS.escape(requestId) + '"]');
|
|
2364
2373
|
const note = box && box.value ? box.value : '';
|
|
@@ -2384,8 +2393,8 @@ function page(name) {
|
|
|
2384
2393
|
+ '<span class="phase" data-phase="' + esc(v.phase) + '" data-status="' + esc(v.outcome ? v.outcome.status : '') + '">' + esc(v.phase === 'done' && v.outcome ? v.outcome.status : v.phase) + '</span></li>'; }).join('');
|
|
2385
2394
|
el('kickbacks').innerHTML = s.kickbacks.map((k) => '<p class="kick">' + esc(k.from) + ' sent work back to ' + esc(k.to) + (k.accepted ? '' : ' (not accepted' + (k.note ? ': ' + esc(k.note) : '') + ')') + ': ' + esc(k.reason) + ' (' + k.count + ' of ' + k.limit + ')</p>').join('');
|
|
2386
2395
|
el('pending-title').hidden = s.pending.length === 0;
|
|
2387
|
-
el('pending').innerHTML = s.pending.map((p) => '<div><p>' + esc(p.decisionText) + '</p
|
|
2388
|
-
el('record').textContent = s.events.slice(-40).map((e) => new Date(e.ts).toISOString().slice(11, 19) + ' ' + e.
|
|
2396
|
+
el('pending').innerHTML = s.pending.map((p) => '<div><p>' + esc(p.decisionText) + '</p>' + aboutHtml(p.input) + '<form onsubmit="return false"><input data-note="' + esc(p.requestId) + '" placeholder="a note, if any"><button class="yes" data-answer="yes" data-request="' + esc(p.requestId) + '">Yes</button><button data-answer="no" data-request="' + esc(p.requestId) + '">No</button></form></div>').join('');
|
|
2397
|
+
el('record').textContent = s.events.slice(-40).map((e) => new Date(e.ts).toISOString().slice(11, 19) + ' ' + e.line).join('\\n');
|
|
2389
2398
|
if (s.status !== 'done') setTimeout(render, 1000);
|
|
2390
2399
|
}
|
|
2391
2400
|
render();
|
|
@@ -2475,7 +2484,10 @@ async function run(job, options = {}) {
|
|
|
2475
2484
|
thin: options.recordTo === "auto",
|
|
2476
2485
|
...resumed === void 0 ? {} : { resume: true }
|
|
2477
2486
|
}));
|
|
2478
|
-
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
|
+
}
|
|
2479
2491
|
initialState[RESUME_STAGE_OUTCOMES] = resumed.outcomes;
|
|
2480
2492
|
initialState[RESUME_RECORDED_USAGE] = resumed.usage;
|
|
2481
2493
|
}
|
|
@@ -2706,8 +2718,8 @@ function parseFrontMatter(source) {
|
|
|
2706
2718
|
function briefFromFile(path) {
|
|
2707
2719
|
return parseFrontMatter(readFileSync(path, "utf8"));
|
|
2708
2720
|
}
|
|
2709
|
-
function person(question) {
|
|
2710
|
-
return { kind: "person", question: text(question, "person question") };
|
|
2721
|
+
function person(question, options = {}) {
|
|
2722
|
+
return { kind: "person", question: text(question, "person question"), ...options };
|
|
2711
2723
|
}
|
|
2712
2724
|
function stage(name, config) {
|
|
2713
2725
|
return { name: text(name, "stage name"), config: { ...config } };
|
|
@@ -2733,24 +2745,29 @@ function writesOf(config) {
|
|
|
2733
2745
|
if (!writes.length || new Set(writes).size !== writes.length) throw new TypeError("writes must contain unique paths");
|
|
2734
2746
|
return writes;
|
|
2735
2747
|
}
|
|
2736
|
-
function
|
|
2737
|
-
if (
|
|
2738
|
-
if (!Number.isSafeInteger(
|
|
2739
|
-
return
|
|
2748
|
+
function refineCount(refine, label) {
|
|
2749
|
+
if (refine === void 0) return 0;
|
|
2750
|
+
if (!Number.isSafeInteger(refine) || refine < 0) throw new TypeError(`${label} must be a non-negative integer`);
|
|
2751
|
+
return refine;
|
|
2752
|
+
}
|
|
2753
|
+
function refineCap(refine) {
|
|
2754
|
+
return isJudge(refine) ? refine.cap : refine;
|
|
2740
2755
|
}
|
|
2741
2756
|
function optionalFlag(optional) {
|
|
2742
2757
|
if (optional === void 0) return void 0;
|
|
2743
2758
|
if (typeof optional !== "boolean") throw new TypeError("optional must be a boolean");
|
|
2744
2759
|
return optional;
|
|
2745
2760
|
}
|
|
2746
|
-
function
|
|
2747
|
-
|
|
2761
|
+
function refineOf(config) {
|
|
2762
|
+
if (config.refine === void 0) return 1;
|
|
2763
|
+
if (isJudge(config.refine)) return config.refine;
|
|
2764
|
+
return refineCount(config.refine, "refine");
|
|
2748
2765
|
}
|
|
2749
|
-
function
|
|
2750
|
-
if ("reviewedBy" in config && config.reviewedBy) return
|
|
2751
|
-
if (receivesKickback) return
|
|
2752
|
-
if (config.
|
|
2753
|
-
throw new TypeError("
|
|
2766
|
+
function refineForStage(config, receivesKickback) {
|
|
2767
|
+
if ("reviewedBy" in config && config.reviewedBy) return refineOf(config);
|
|
2768
|
+
if (receivesKickback) return refineOf(config);
|
|
2769
|
+
if (config.refine !== void 0) {
|
|
2770
|
+
throw new TypeError("refine must be on a reviewed stage or a kickback target");
|
|
2754
2771
|
}
|
|
2755
2772
|
return 0;
|
|
2756
2773
|
}
|
|
@@ -2882,6 +2899,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2882
2899
|
try {
|
|
2883
2900
|
const all2 = recordedUsage(ctx);
|
|
2884
2901
|
const beforeLength = all2.length;
|
|
2902
|
+
const resumingInteraction = hasSavedInteraction(ctx, ctx.path);
|
|
2885
2903
|
const writerSide = (records) => records.filter(
|
|
2886
2904
|
(record4) => record4.role === "writer" && record4.stage !== void 0 && writerStageNames.includes(record4.stage)
|
|
2887
2905
|
);
|
|
@@ -2893,7 +2911,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2893
2911
|
const current = recordedUsage(ctx);
|
|
2894
2912
|
const after = current.slice(beforeLength);
|
|
2895
2913
|
const afterWriters = writerSide(after);
|
|
2896
|
-
if (writerRunsInsidePanel && afterWriters.length === 0) {
|
|
2914
|
+
if (writerRunsInsidePanel && afterWriters.length === 0 && !resumingInteraction) {
|
|
2897
2915
|
throw new LoopError({
|
|
2898
2916
|
code: "BODY",
|
|
2899
2917
|
phase: "review",
|
|
@@ -2901,7 +2919,7 @@ function recordedFamilyGate(panel, reviewers, writerStageNames, writerFamilies,
|
|
|
2901
2919
|
});
|
|
2902
2920
|
}
|
|
2903
2921
|
assertRecordedFamilies(afterWriters, reviewerDeclarations);
|
|
2904
|
-
const reviewerSide = after.filter((record4) => record4.role === "reviewer");
|
|
2922
|
+
const reviewerSide = (resumingInteraction ? current : after).filter((record4) => record4.role === "reviewer" && record4.stage === panelStage);
|
|
2905
2923
|
if (reviewerSide.length === 0) {
|
|
2906
2924
|
throw new LoopError({
|
|
2907
2925
|
code: "BODY",
|
|
@@ -3006,6 +3024,26 @@ function guardedAgent(brief, named, seat, files, declaredFiles) {
|
|
|
3006
3024
|
return requireNoFiles(named.name, required, ctx.workspace.dir, forbidden, "body")(ctx);
|
|
3007
3025
|
};
|
|
3008
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
|
+
}
|
|
3009
3047
|
function unchangedNoteGuard(label, job, writes) {
|
|
3010
3048
|
const previousHashKey = `declarativePreviousHash:${label}`;
|
|
3011
3049
|
return async (ctx) => {
|
|
@@ -3035,6 +3073,84 @@ function unchangedNoteGuard(label, job, writes) {
|
|
|
3035
3073
|
return outcome;
|
|
3036
3074
|
};
|
|
3037
3075
|
}
|
|
3076
|
+
function lineDiffCount(before, after) {
|
|
3077
|
+
if (before === void 0) return after.split("\n").length;
|
|
3078
|
+
const a = new Set(before.split("\n"));
|
|
3079
|
+
const b = new Set(after.split("\n"));
|
|
3080
|
+
let changed = 0;
|
|
3081
|
+
for (const line of a) if (!b.has(line)) changed += 1;
|
|
3082
|
+
for (const line of b) if (!a.has(line)) changed += 1;
|
|
3083
|
+
return changed;
|
|
3084
|
+
}
|
|
3085
|
+
function judgedReview(brief, config, cfgJudge, panel) {
|
|
3086
|
+
const useCase = /^Use case:\s*([\s\S]*?)\n\s*\n/m.exec(brief.brief)?.[1]?.replace(/\s+/g, " ").trim();
|
|
3087
|
+
const file = writesOf(config)[0];
|
|
3088
|
+
const history = [];
|
|
3089
|
+
let previousDraft;
|
|
3090
|
+
let productFeedback = [];
|
|
3091
|
+
const identity = interactionIdentity({ brief, config, cfgJudge });
|
|
3092
|
+
return async (ctx) => {
|
|
3093
|
+
const checkpointPath = [...ctx.path, "@judge-review"];
|
|
3094
|
+
let saved = savedInteraction(ctx, checkpointPath, identity);
|
|
3095
|
+
let draft;
|
|
3096
|
+
let changedLines;
|
|
3097
|
+
if (file !== void 0) {
|
|
3098
|
+
try {
|
|
3099
|
+
draft = await readFile(join(ctx.workspace.dir, file), "utf8");
|
|
3100
|
+
changedLines = lineDiffCount(previousDraft, draft);
|
|
3101
|
+
} catch {
|
|
3102
|
+
}
|
|
3103
|
+
previousDraft = draft;
|
|
3104
|
+
}
|
|
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({
|
|
3112
|
+
...ctx,
|
|
3113
|
+
depth: ctx.depth + 1,
|
|
3114
|
+
path: [...ctx.path, "review-panel"]
|
|
3115
|
+
});
|
|
3116
|
+
if (panelOutcome.status === "pass") return panelOutcome;
|
|
3117
|
+
const findings = revisionFromOutcome(panelOutcome)?.findings ?? [];
|
|
3118
|
+
if (hasBlockFinding(findings)) return panelOutcome;
|
|
3119
|
+
const round2 = history.length + 1;
|
|
3120
|
+
const state = saved ? saved.state : {
|
|
3121
|
+
...productFeedback.length ? { productFeedback } : {},
|
|
3122
|
+
...useCase !== void 0 ? { useCase } : {},
|
|
3123
|
+
...file !== void 0 ? { file } : {},
|
|
3124
|
+
...draft !== void 0 ? { draft } : {},
|
|
3125
|
+
latestFindings: findings,
|
|
3126
|
+
rounds: history,
|
|
3127
|
+
round: round2,
|
|
3128
|
+
cap: cfgJudge.cap
|
|
3129
|
+
};
|
|
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;
|
|
3139
|
+
history.push({
|
|
3140
|
+
round: round2,
|
|
3141
|
+
findings,
|
|
3142
|
+
counts: countBySeverity(findings),
|
|
3143
|
+
...changedLines !== void 0 ? { changedLines } : {}
|
|
3144
|
+
});
|
|
3145
|
+
if (!decision.again) {
|
|
3146
|
+
if (decision.stop === "fail") {
|
|
3147
|
+
throw new LoopError({ code: "VALIDATION", phase: "review", path: ctx.path, message: decision.reason });
|
|
3148
|
+
}
|
|
3149
|
+
return { status: "pass", confidence: panelOutcome.confidence, summary: decision.reason, data: panelOutcome.data };
|
|
3150
|
+
}
|
|
3151
|
+
return { ...panelOutcome, summary: `${panelOutcome.summary} (${decision.reason})` };
|
|
3152
|
+
};
|
|
3153
|
+
}
|
|
3038
3154
|
function stageJob(brief, named, roles, files, declaredFiles, targetFiles, familyTargetStageNames, familyTargetFamilies) {
|
|
3039
3155
|
const config = named.config;
|
|
3040
3156
|
if ("agent" in config && config.agent !== void 0) {
|
|
@@ -3042,9 +3158,33 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
|
|
|
3042
3158
|
const guarded = guardedAgent(brief, named, seatRole(roles, config.agent), files, declaredFiles);
|
|
3043
3159
|
const job = config.reviewedBy === void 0 ? guarded : unchangedNoteGuard(named.name, guarded, writes);
|
|
3044
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
|
+
}
|
|
3045
3179
|
const reviewers = panelRole(roles, config.reviewedBy);
|
|
3046
3180
|
const panel = reviewerPanel(brief, named, reviewers, files, declaredFiles, void 0);
|
|
3047
|
-
const
|
|
3181
|
+
const refine = refineOf(config);
|
|
3182
|
+
const cap = refineCap(refine);
|
|
3183
|
+
const review = isJudge(refine) ? judgedReview(brief, config, refine, panel) : async (ctx) => panel({
|
|
3184
|
+
...ctx,
|
|
3185
|
+
depth: ctx.depth + 1,
|
|
3186
|
+
path: [...ctx.path, "review-panel"]
|
|
3187
|
+
});
|
|
3048
3188
|
const reviewLoop = loop({
|
|
3049
3189
|
name: `${named.name}-review`,
|
|
3050
3190
|
body: job,
|
|
@@ -3060,15 +3200,9 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
|
|
|
3060
3200
|
}
|
|
3061
3201
|
return true;
|
|
3062
3202
|
}, `${named.name} writes`),
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
3066
|
-
...ctx,
|
|
3067
|
-
depth: ctx.depth + 1,
|
|
3068
|
-
path: [...ctx.path, "review-panel"]
|
|
3069
|
-
}),
|
|
3070
|
-
max: retry + 1,
|
|
3071
|
-
maxReviewRestarts: retry,
|
|
3203
|
+
review,
|
|
3204
|
+
max: cap + 1,
|
|
3205
|
+
maxReviewRestarts: cap,
|
|
3072
3206
|
noProgress: { window: 2, gate: true }
|
|
3073
3207
|
});
|
|
3074
3208
|
return copyJobMeta(
|
|
@@ -3102,9 +3236,27 @@ function stageJob(brief, named, roles, files, declaredFiles, targetFiles, family
|
|
|
3102
3236
|
}
|
|
3103
3237
|
if ("input" in config && config.input !== void 0) {
|
|
3104
3238
|
const personRole = inputRole(roles, config.input);
|
|
3105
|
-
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
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
if ("fn" in config && config.fn !== void 0) {
|
|
3257
|
+
return guardedFn(config.fn, named, declaredFiles);
|
|
3106
3258
|
}
|
|
3107
|
-
throw new TypeError(`stage ${named.name} must declare agent, run, panel or
|
|
3259
|
+
throw new TypeError(`stage ${named.name} must declare agent, run, panel, input or fn`);
|
|
3108
3260
|
}
|
|
3109
3261
|
function workflowRecord(outcome) {
|
|
3110
3262
|
return {
|
|
@@ -3129,26 +3281,13 @@ function panelReviewFiles(stages, index) {
|
|
|
3129
3281
|
const targetWrites = target === void 0 ? [] : writesOf(target.config);
|
|
3130
3282
|
return targetWrites.length ? targetWrites : preceding === void 0 ? [] : writesOf(preceding.config);
|
|
3131
3283
|
}
|
|
3132
|
-
function canonicalForDigest(value) {
|
|
3133
|
-
if (typeof value === "function") return `fn:${value.toString()}`;
|
|
3134
|
-
if (Array.isArray(value)) return value.map(canonicalForDigest);
|
|
3135
|
-
if (value !== null && typeof value === "object") {
|
|
3136
|
-
return Object.fromEntries(
|
|
3137
|
-
Object.keys(value).sort().map((key) => [
|
|
3138
|
-
key,
|
|
3139
|
-
canonicalForDigest(value[key])
|
|
3140
|
-
])
|
|
3141
|
-
);
|
|
3142
|
-
}
|
|
3143
|
-
return value;
|
|
3144
|
-
}
|
|
3145
3284
|
function resumeIdentity(name, config) {
|
|
3146
3285
|
const declared = {
|
|
3147
3286
|
name,
|
|
3148
3287
|
brief: config.brief,
|
|
3149
3288
|
stages: config.stages.map((named) => ({
|
|
3150
3289
|
name: named.name,
|
|
3151
|
-
config:
|
|
3290
|
+
config: interactionIdentity(named.config)
|
|
3152
3291
|
})),
|
|
3153
3292
|
roles: Object.fromEntries(Object.entries(config.roles).map(([key, value]) => [
|
|
3154
3293
|
key,
|
|
@@ -3183,6 +3322,10 @@ function resumeGuard(job, identity, label, retrySafe) {
|
|
|
3183
3322
|
const parentPath = ctx.path.slice(0, -1).join("/");
|
|
3184
3323
|
const anchor = resumed?.anchors.get(parentPath);
|
|
3185
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
|
+
}
|
|
3186
3329
|
if (ctx.graph?.attempt === 1 && recorded !== void 0) {
|
|
3187
3330
|
if (recorded.kind === "interrupted") {
|
|
3188
3331
|
if (!retrySafe) {
|
|
@@ -3206,7 +3349,7 @@ function resumeGuard(job, identity, label, retrySafe) {
|
|
|
3206
3349
|
}
|
|
3207
3350
|
return delegateNodeJob(ctx, guarded, job, ctx);
|
|
3208
3351
|
};
|
|
3209
|
-
return guarded;
|
|
3352
|
+
return interactionDeclaration(guarded, { identity, label, job });
|
|
3210
3353
|
}
|
|
3211
3354
|
async function reconcileInterrupted(ctx, owner, job, label, identity, recordId, startLine) {
|
|
3212
3355
|
const question = approval(`reconcile ${label}`, {
|
|
@@ -3267,7 +3410,7 @@ function workflow(name, config) {
|
|
|
3267
3410
|
if (reviewedBy !== void 0 && stageConfig.sendsBackTo !== void 0) {
|
|
3268
3411
|
throw new TypeError(`stage ${stageName} cannot use both reviewedBy and sendsBackTo`);
|
|
3269
3412
|
}
|
|
3270
|
-
|
|
3413
|
+
refineForStage(stageConfig, incomingTargets.has(stageName));
|
|
3271
3414
|
optionalFlag(stageConfig.optional);
|
|
3272
3415
|
if (stageConfig.retrySafe !== void 0 && typeof stageConfig.retrySafe !== "boolean") {
|
|
3273
3416
|
throw new TypeError("retrySafe must be a boolean");
|
|
@@ -3277,10 +3420,12 @@ function workflow(name, config) {
|
|
|
3277
3420
|
for (const [index, named] of config.stages.entries()) {
|
|
3278
3421
|
const stageConfig = named.config;
|
|
3279
3422
|
if ("agent" in stageConfig && stageConfig.agent !== void 0 && stageConfig.reviewedBy !== void 0) {
|
|
3280
|
-
|
|
3423
|
+
const reviewer = role(config.roles, stageConfig.reviewedBy);
|
|
3424
|
+
if (Array.isArray(reviewer)) assertDistinctSeats([
|
|
3281
3425
|
seatRole(config.roles, stageConfig.agent),
|
|
3282
3426
|
...panelRole(config.roles, stageConfig.reviewedBy)
|
|
3283
3427
|
]);
|
|
3428
|
+
else if (!("kind" in reviewer) || reviewer.kind !== "person") throw new TypeError("reviewedBy must name a reviewer panel or person");
|
|
3284
3429
|
}
|
|
3285
3430
|
if ("panel" in stageConfig && stageConfig.panel !== void 0) {
|
|
3286
3431
|
const preceding = precedingAgent(config.stages, index);
|
|
@@ -3307,10 +3452,9 @@ function workflow(name, config) {
|
|
|
3307
3452
|
for (const named of config.stages) {
|
|
3308
3453
|
if (named.config.sendsBackTo !== void 0) {
|
|
3309
3454
|
const target = config.stages.find((candidate) => candidate.name === named.config.sendsBackTo);
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
3313
|
-
);
|
|
3455
|
+
const refine = refineOf(target.config);
|
|
3456
|
+
const existing = maxKickbacks[named.config.sendsBackTo];
|
|
3457
|
+
maxKickbacks[named.config.sendsBackTo] = isJudge(refine) ? refine : Math.max(isJudge(existing) ? 0 : existing ?? 0, refine);
|
|
3314
3458
|
}
|
|
3315
3459
|
}
|
|
3316
3460
|
const stageJobIdentity = resumeIdentity(workflowName, config);
|
|
@@ -9160,7 +9304,7 @@ function pipeline(name, stages, opts) {
|
|
|
9160
9304
|
});
|
|
9161
9305
|
return dag({ name, nodes, ...opts });
|
|
9162
9306
|
}
|
|
9163
|
-
function
|
|
9307
|
+
function slug2(s) {
|
|
9164
9308
|
return s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/(^-+|-+$)/g, "") || "x";
|
|
9165
9309
|
}
|
|
9166
9310
|
function tournament(config) {
|
|
@@ -9193,7 +9337,7 @@ function tournament(config) {
|
|
|
9193
9337
|
Array.from(
|
|
9194
9338
|
{ length: config.n },
|
|
9195
9339
|
(_, i) => limit2(async () => {
|
|
9196
|
-
const branch = `lines/${
|
|
9340
|
+
const branch = `lines/${slug2(config.name)}-cand-${i}`;
|
|
9197
9341
|
const wt = await addWorktree(base.dir, {
|
|
9198
9342
|
branch,
|
|
9199
9343
|
base: "HEAD",
|
|
@@ -9271,7 +9415,7 @@ function tournament(config) {
|
|
|
9271
9415
|
}
|
|
9272
9416
|
|
|
9273
9417
|
// src/core/isolated.ts
|
|
9274
|
-
var
|
|
9418
|
+
var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "job";
|
|
9275
9419
|
var forkSeq = 0;
|
|
9276
9420
|
function isolated(job, opts = {}) {
|
|
9277
9421
|
const label = opts.label ?? "isolated";
|
|
@@ -9292,7 +9436,7 @@ function isolated(job, opts = {}) {
|
|
|
9292
9436
|
);
|
|
9293
9437
|
return job(parent);
|
|
9294
9438
|
}
|
|
9295
|
-
const branch = `lines/${
|
|
9439
|
+
const branch = `lines/${slug3(label)}-${forkSeq += 1}`;
|
|
9296
9440
|
const wt = await addWorktree(base.dir, {
|
|
9297
9441
|
branch,
|
|
9298
9442
|
base: "HEAD",
|
|
@@ -9326,7 +9470,7 @@ function isolated(job, opts = {}) {
|
|
|
9326
9470
|
...outcome.summary === void 0 ? {} : { summary: outcome.summary }
|
|
9327
9471
|
}) : void 0;
|
|
9328
9472
|
const sha = await commit(
|
|
9329
|
-
message ?? { subject: `chore(${
|
|
9473
|
+
message ?? { subject: `chore(${slug3(label)}): worktree changes` },
|
|
9330
9474
|
{ cwd: wt.dir, signal: parent.signal }
|
|
9331
9475
|
);
|
|
9332
9476
|
const merged = await mergeLock(
|
|
@@ -9770,6 +9914,6 @@ function defineJob(job) {
|
|
|
9770
9914
|
return job;
|
|
9771
9915
|
}
|
|
9772
9916
|
|
|
9773
|
-
export { EXIT_PAUSED, RESUME_STAGE_OUTCOMES, assertGraph, briefFromFile, confidenceCondition, confidenceFromText, convergence, costReport, createAcceptedResultRecord, createCallbackClient, createGitWorktreeProvider, createGraphExecutor, createProofCache, createStoredCallbackClient, dag, dag2 as dagGraphType, defineAgentFromMarkdown, defineJob, directRouter, exitCodeFor, fallbackEngine, formatCostReport,
|
|
9917
|
+
export { EXIT_PAUSED, RESUME_STAGE_OUTCOMES, assertGraph, briefFromFile, confidenceCondition, confidenceFromText, convergence, costReport, createAcceptedResultRecord, createCallbackClient, createGitWorktreeProvider, createGraphExecutor, createProofCache, createStoredCallbackClient, dag, dag2 as dagGraphType, defineAgentFromMarkdown, defineJob, directRouter, exitCodeFor, fallbackEngine, formatCostReport, formatPreflight, interruptRunPreflight, isJudge, isolated, judge, lastDecisionLine, lastGateBrief, loop, parallel, person, pipeline, preflight, preflightEngine, projectTeamRooms, ratchet, readRunPreflight, replayCallbackClient, resolveAcceptedResult, resolveApproval, run, sampled, sequence, stage, stopQuestions, team, teamGraphType, tournament, workflow, writeProofArtifact, writeScope };
|
|
9774
9918
|
//# sourceMappingURL=api.js.map
|
|
9775
9919
|
//# sourceMappingURL=api.js.map
|