@narumitw/pi-subagents 0.51.0 → 0.52.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +50 -1
- package/package.json +1 -1
- package/src/adaptive-scheduler.ts +29 -1
- package/src/execution.ts +291 -41
- package/src/inspect.ts +25 -0
- package/src/orchestration-metrics.ts +31 -0
- package/src/panel-execution.ts +0 -2
- package/src/params.ts +8 -1
- package/src/verification-policy.ts +50 -0
- package/src/work-item-ledger.ts +267 -18
- package/src/work-item-persistence.ts +5 -0
- package/src/workflow-planning.ts +13 -1
- package/src/workflow-tree-identity.ts +289 -0
- package/src/workflow-verification.ts +296 -0
package/README.md
CHANGED
|
@@ -413,11 +413,58 @@ Run an explicit dependency workflow:
|
|
|
413
413
|
}
|
|
414
414
|
```
|
|
415
415
|
|
|
416
|
+
A verification-gated implementation declares one distinct verifier:
|
|
417
|
+
|
|
418
|
+
```json
|
|
419
|
+
{
|
|
420
|
+
"workflow": {
|
|
421
|
+
"tasks": [
|
|
422
|
+
{
|
|
423
|
+
"id": "implementation",
|
|
424
|
+
"agent": "worker",
|
|
425
|
+
"task": "Implement the contracted change.",
|
|
426
|
+
"resultFormat": "structured-v2",
|
|
427
|
+
"contract": {
|
|
428
|
+
"version": "pi-subagents:delegation:v2",
|
|
429
|
+
"level": "full",
|
|
430
|
+
"taskId": "implementation",
|
|
431
|
+
"objective": "Implement the contracted change",
|
|
432
|
+
"admission": {
|
|
433
|
+
"contextPressure": "medium",
|
|
434
|
+
"independentWorkItems": 1,
|
|
435
|
+
"coupling": "dense",
|
|
436
|
+
"verificationRequired": true,
|
|
437
|
+
"verificationAvailable": true,
|
|
438
|
+
"budgetAllowsChildren": true,
|
|
439
|
+
"requirementsComplete": true
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
"id": "verification",
|
|
445
|
+
"agent": "reviewer",
|
|
446
|
+
"task": "Independently verify the staged result.",
|
|
447
|
+
"dependsOn": ["implementation"],
|
|
448
|
+
"verifierFor": "implementation",
|
|
449
|
+
"resultFormat": "structured-v2"
|
|
450
|
+
}
|
|
451
|
+
]
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
416
456
|
Cycles, missing dependencies, conflicting integration owners, recursive workflow grandchildren, and unsafe retry or hedge policies fail before child launch.
|
|
417
457
|
Workflow scheduling starts at most two mutating tasks concurrently, while declared read-only work may use the existing four-child ceiling.
|
|
418
458
|
Set `workflow.honorAdmission: true` only when explicit contract admission metadata should be allowed to decline parent-owned or insufficient-evidence work before launch; admission never silently widens the requested architecture.
|
|
419
459
|
Workflow result details include the final ledger, scheduling decisions, artifact versions, task generations, attempts, hedge use, accepted plan identity, and bounded capability-grant metadata.
|
|
420
|
-
|
|
460
|
+
A task that explicitly requires independent verification must have exactly one direct-dependent `verifierFor` task using a different agent, and both tasks must request `structured-v2`.
|
|
461
|
+
The producer stops in `awaiting-verification`, its own passing verification claims remain untrusted, and ordinary downstream tasks stay blocked until the executor records an accepted verifier receipt.
|
|
462
|
+
The verifier runs alone in a fresh subprocess context against one bounded Git-visible tree identity and must encode `verification-accepted`, `verification-rework`, or `verification-rejected` through the documented `structured-v2` status and reason fields.
|
|
463
|
+
Dirty-tree identity covers at most 1 MiB across separately framed staged and unstaged binary diffs plus bounded non-ignored untracked paths and bytes; submodules, unsupported states, and changing trees fail closed.
|
|
464
|
+
A rework or rejection preserves bounded evidence but does not replay the producer automatically.
|
|
465
|
+
This acceptance gate does not isolate operating-system effects and does not make shared-workspace mutation into manager-controlled patch integration.
|
|
466
|
+
Explicit workflow transitions are also atomically persisted as mode-0600, private-text-redacted snapshots for current-session `list_workflows` and `get_workflow` inspection; running and awaiting-verification tasks inspect as `interrupted`, and no prior side effect is automatically resumed.
|
|
467
|
+
When a v1 ledger is restored, legacy self-reported verification flags and artifact trust are cleared because they have no executor receipt.
|
|
421
468
|
|
|
422
469
|
## 🔁 Stateful agents
|
|
423
470
|
|
|
@@ -890,6 +937,8 @@ packages/pi-subagents/
|
|
|
890
937
|
│ ├── execution-plan.ts # Executor-owned authority and resource resolution
|
|
891
938
|
│ ├── work-item-ledger.ts # Persistent dependency and artifact state machine
|
|
892
939
|
│ ├── work-item-persistence.ts # Atomic redacted workflow state and inspection
|
|
940
|
+
│ ├── workflow-verification.ts # Executor-owned independent-verifier receipts
|
|
941
|
+
│ ├── workflow-tree-identity.ts # Bounded exact Git-visible tree identities
|
|
893
942
|
│ ├── integration-controller.ts # Fail-closed canonical integration admission
|
|
894
943
|
│ ├── adaptive-scheduler.ts # Dependency, capacity, budget, and conflict scheduling
|
|
895
944
|
│ ├── semantic-snapshot.ts # Privacy-safe continuation compatibility checks
|
package/package.json
CHANGED
|
@@ -9,7 +9,8 @@ export type SchedulingReason =
|
|
|
9
9
|
| "state-not-ready"
|
|
10
10
|
| "budget-exhausted"
|
|
11
11
|
| "capacity-exhausted"
|
|
12
|
-
| "scope-conflict"
|
|
12
|
+
| "scope-conflict"
|
|
13
|
+
| "verification-barrier";
|
|
13
14
|
|
|
14
15
|
export interface SchedulingDecisionItem {
|
|
15
16
|
id: string;
|
|
@@ -57,6 +58,33 @@ export class AdaptiveScheduler {
|
|
|
57
58
|
),
|
|
58
59
|
);
|
|
59
60
|
const effectiveConcurrency = options.remainingBudgetMs > 0 ? availableSlots : 0;
|
|
61
|
+
const readyVerifier = ready.find((item) => item.verifierFor !== undefined);
|
|
62
|
+
if (readyVerifier) {
|
|
63
|
+
const verifierMayStart = effectiveConcurrency > 0 && options.activeCount === 0;
|
|
64
|
+
return {
|
|
65
|
+
policy: ADAPTIVE_SCHEDULER_POLICY,
|
|
66
|
+
workflowId: snapshot.workflowId,
|
|
67
|
+
workflowGeneration: snapshot.generation,
|
|
68
|
+
effectiveConcurrency: verifierMayStart ? 1 : 0,
|
|
69
|
+
selected: verifierMayStart ? [readyVerifier.id] : [],
|
|
70
|
+
decisions: snapshot.items
|
|
71
|
+
.map((item) => ({
|
|
72
|
+
id: item.id,
|
|
73
|
+
reason:
|
|
74
|
+
item.id === readyVerifier.id
|
|
75
|
+
? verifierMayStart
|
|
76
|
+
? ("selected" as const)
|
|
77
|
+
: ("capacity-exhausted" as const)
|
|
78
|
+
: item.state === "ready"
|
|
79
|
+
? ("verification-barrier" as const)
|
|
80
|
+
: item.state === "pending"
|
|
81
|
+
? ("dependency-not-ready" as const)
|
|
82
|
+
: ("state-not-ready" as const),
|
|
83
|
+
criticalPathDepth: depth.get(item.id) ?? 0,
|
|
84
|
+
}))
|
|
85
|
+
.sort((left, right) => left.id.localeCompare(right.id)),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
60
88
|
const selected: string[] = [];
|
|
61
89
|
let mutatingCount = options.activeMutatingCount ?? 0;
|
|
62
90
|
const maxMutatingConcurrency = options.maxMutatingConcurrency ?? 2;
|
package/src/execution.ts
CHANGED
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
startSubagentStatus,
|
|
22
22
|
} from "./blocking-status.js";
|
|
23
23
|
import { issueCapabilityGrant } from "./capability-grant.js";
|
|
24
|
+
import { redactPrivateText } from "./context.js";
|
|
24
25
|
import {
|
|
25
26
|
assertDelegationTargetAllowed,
|
|
26
27
|
type ResolvedSubagentTarget,
|
|
@@ -69,13 +70,21 @@ import {
|
|
|
69
70
|
import { isRetryableResult, runHedgedAttempt, supervisionDelay } from "./supervision.js";
|
|
70
71
|
import { TimeoutProgressJournal, TURN_TERMINATION_VERSION } from "./timeout-checkpoint.js";
|
|
71
72
|
import type { TurnLimits } from "./turn-budget.js";
|
|
72
|
-
import {
|
|
73
|
+
import {
|
|
74
|
+
requiresIndependentVerification,
|
|
75
|
+
validateWorkflowVerificationGraph,
|
|
76
|
+
} from "./verification-policy.js";
|
|
73
77
|
import type { WorkItemLedger } from "./work-item-ledger.js";
|
|
74
78
|
import {
|
|
75
79
|
createSessionWorkItemPersistence,
|
|
76
80
|
type WorkItemPersistence,
|
|
77
81
|
} from "./work-item-persistence.js";
|
|
78
82
|
import { createBlockingWorkLedger, resolveWorkflowTasks } from "./workflow-planning.js";
|
|
83
|
+
import { captureWorkflowTreeIdentity, sameWorkflowTreeIdentity } from "./workflow-tree-identity.js";
|
|
84
|
+
import {
|
|
85
|
+
createWorkflowVerificationReceipt,
|
|
86
|
+
workflowVerificationInstruction,
|
|
87
|
+
} from "./workflow-verification.js";
|
|
79
88
|
|
|
80
89
|
export const FALLBACK_TIMEOUT_MS = 10 * 60 * 1000;
|
|
81
90
|
|
|
@@ -173,6 +182,7 @@ export async function executeSubagent(
|
|
|
173
182
|
Number(hasSingle);
|
|
174
183
|
let workLedger: WorkItemLedger | undefined;
|
|
175
184
|
const workflowScheduling: ReturnType<AdaptiveScheduler["decide"]>[] = [];
|
|
185
|
+
const verificationTargetIds = new Set<string>();
|
|
176
186
|
|
|
177
187
|
const makeDetails =
|
|
178
188
|
(mode: "single" | "parallel" | "chain" | "workflow" | "panel") =>
|
|
@@ -191,6 +201,38 @@ export async function executeSubagent(
|
|
|
191
201
|
metrics: calculateOrchestrationMetrics(workflow, metricResults),
|
|
192
202
|
};
|
|
193
203
|
};
|
|
204
|
+
const workflowFailureResult = (
|
|
205
|
+
agentName: string,
|
|
206
|
+
task: string,
|
|
207
|
+
reasonCode: string,
|
|
208
|
+
message: string,
|
|
209
|
+
thinkingLevel?: SubagentThinkingLevel,
|
|
210
|
+
): SingleResult => ({
|
|
211
|
+
agent: agentName,
|
|
212
|
+
agentSource: agents.find((agent) => agent.name === agentName)?.source ?? "unknown",
|
|
213
|
+
task,
|
|
214
|
+
exitCode: 1,
|
|
215
|
+
messages: [],
|
|
216
|
+
stderr: message,
|
|
217
|
+
errorMessage: message,
|
|
218
|
+
usage: {
|
|
219
|
+
input: 0,
|
|
220
|
+
output: 0,
|
|
221
|
+
cacheRead: 0,
|
|
222
|
+
cacheWrite: 0,
|
|
223
|
+
cost: 0,
|
|
224
|
+
contextTokens: 0,
|
|
225
|
+
turns: 0,
|
|
226
|
+
},
|
|
227
|
+
thinkingLevel,
|
|
228
|
+
finalOutput: "",
|
|
229
|
+
outcome: {
|
|
230
|
+
status: "failed",
|
|
231
|
+
reasonCode,
|
|
232
|
+
recoveryActions: ["revalidate"],
|
|
233
|
+
retryable: false,
|
|
234
|
+
},
|
|
235
|
+
});
|
|
194
236
|
const exhaustedResult = (
|
|
195
237
|
agentName: string,
|
|
196
238
|
task: string,
|
|
@@ -292,6 +334,7 @@ export async function executeSubagent(
|
|
|
292
334
|
}
|
|
293
335
|
if (hasWorkflow && params.workflow) {
|
|
294
336
|
for (const task of resolvedWorkflowTasks) {
|
|
337
|
+
if (task.verifierFor) verificationTargetIds.add(task.verifierFor);
|
|
295
338
|
const contract = normalizeDelegationContract(task.contract);
|
|
296
339
|
if (params.workflow.honorAdmission) {
|
|
297
340
|
const admission = contract?.admission;
|
|
@@ -322,17 +365,7 @@ export async function executeSubagent(
|
|
|
322
365
|
requiredCapabilities: task.requiredCapabilities ?? [],
|
|
323
366
|
})
|
|
324
367
|
) {
|
|
325
|
-
|
|
326
|
-
(candidate) =>
|
|
327
|
-
candidate.verifierFor === task.id &&
|
|
328
|
-
candidate.dependsOn?.includes(task.id) &&
|
|
329
|
-
candidate.agent !== task.agent,
|
|
330
|
-
);
|
|
331
|
-
if (!verifier) {
|
|
332
|
-
throw new Error(
|
|
333
|
-
`Workflow task ${task.id} requires a distinct dependent verifier before launch`,
|
|
334
|
-
);
|
|
335
|
-
}
|
|
368
|
+
verificationTargetIds.add(task.id);
|
|
336
369
|
}
|
|
337
370
|
if (!task.retryPolicy && !task.hedgeAfterMs) continue;
|
|
338
371
|
const policy = contract?.sideEffectPolicy;
|
|
@@ -345,6 +378,13 @@ export async function executeSubagent(
|
|
|
345
378
|
);
|
|
346
379
|
}
|
|
347
380
|
}
|
|
381
|
+
validateWorkflowVerificationGraph(
|
|
382
|
+
resolvedWorkflowTasks.map((task) => ({
|
|
383
|
+
...task,
|
|
384
|
+
resultFormat: task.resultFormat ?? params.resultFormat,
|
|
385
|
+
})),
|
|
386
|
+
verificationTargetIds,
|
|
387
|
+
);
|
|
348
388
|
}
|
|
349
389
|
workLedger = createBlockingWorkLedger(params, resolvedWorkflowTasks, aggregator);
|
|
350
390
|
let workflowPersistence: WorkItemPersistence | undefined;
|
|
@@ -512,6 +552,19 @@ export async function executeSubagent(
|
|
|
512
552
|
);
|
|
513
553
|
}
|
|
514
554
|
|
|
555
|
+
const artifactsFromResult = (result: SingleResult) => {
|
|
556
|
+
const structured =
|
|
557
|
+
result.structuredResult?.version === "pi-subagents:result:v2"
|
|
558
|
+
? result.structuredResult
|
|
559
|
+
: undefined;
|
|
560
|
+
return (structured?.artifacts ?? []).map((artifact) => ({
|
|
561
|
+
id: artifact.id,
|
|
562
|
+
kind: artifact.kind,
|
|
563
|
+
version: artifact.version ?? artifact.digest ?? "unversioned",
|
|
564
|
+
digest: artifact.digest,
|
|
565
|
+
verified: false,
|
|
566
|
+
}));
|
|
567
|
+
};
|
|
515
568
|
const startWorkItem = (id: string, agentName: string) => {
|
|
516
569
|
if (workLedger?.get(id)?.state === "ready") {
|
|
517
570
|
return workLedger.start(id, `agent:${agentName}`);
|
|
@@ -548,24 +601,10 @@ export async function executeSubagent(
|
|
|
548
601
|
);
|
|
549
602
|
return;
|
|
550
603
|
}
|
|
551
|
-
const structured =
|
|
552
|
-
result.structuredResult?.version === "pi-subagents:result:v2"
|
|
553
|
-
? result.structuredResult
|
|
554
|
-
: undefined;
|
|
555
604
|
workLedger.complete(id, {
|
|
556
605
|
taskGeneration,
|
|
557
606
|
executionPlanId: result.executionPlan?.id,
|
|
558
|
-
artifacts: (
|
|
559
|
-
id: artifact.id,
|
|
560
|
-
kind: artifact.kind,
|
|
561
|
-
version: artifact.version ?? artifact.digest ?? "unversioned",
|
|
562
|
-
digest: artifact.digest,
|
|
563
|
-
verified:
|
|
564
|
-
structured?.verification.some((verification) => verification.status === "passed") ??
|
|
565
|
-
false,
|
|
566
|
-
})),
|
|
567
|
-
verificationAccepted:
|
|
568
|
-
structured?.verification.some((verification) => verification.status === "passed") ?? false,
|
|
607
|
+
artifacts: artifactsFromResult(result),
|
|
569
608
|
});
|
|
570
609
|
};
|
|
571
610
|
|
|
@@ -659,7 +698,9 @@ export async function executeSubagent(
|
|
|
659
698
|
const deadline = orchestrationDeadline;
|
|
660
699
|
const cancelWorkflowGeneration = () => {
|
|
661
700
|
for (const item of workLedger.snapshot().items) {
|
|
662
|
-
if (item.state === "running"
|
|
701
|
+
if (item.state === "running" || item.state === "awaiting-verification") {
|
|
702
|
+
workLedger.invalidate(item.id, "parent-aborted");
|
|
703
|
+
}
|
|
663
704
|
}
|
|
664
705
|
};
|
|
665
706
|
signal?.addEventListener("abort", cancelWorkflowGeneration, { once: true });
|
|
@@ -691,17 +732,73 @@ export async function executeSubagent(
|
|
|
691
732
|
const dependencies = (task.dependsOn ?? [])
|
|
692
733
|
.map((dependency) => resultsById.get(dependency))
|
|
693
734
|
.filter((result): result is SingleResult => result !== undefined);
|
|
694
|
-
const
|
|
695
|
-
?
|
|
696
|
-
:
|
|
735
|
+
const verifierDependency = task.verifierFor
|
|
736
|
+
? resultsById.get(task.verifierFor)
|
|
737
|
+
: undefined;
|
|
738
|
+
const verifierStructuredResult =
|
|
739
|
+
verifierDependency?.structuredResult?.version === "pi-subagents:result:v2"
|
|
740
|
+
? verifierDependency.structuredResult
|
|
741
|
+
: undefined;
|
|
742
|
+
const dependencyContext = task.verifierFor
|
|
743
|
+
? verifierStructuredResult
|
|
744
|
+
? `\n\nStaged target result:\n${redactPrivateText(
|
|
745
|
+
JSON.stringify(verifierStructuredResult),
|
|
746
|
+
)}`
|
|
747
|
+
: ""
|
|
748
|
+
: dependencies.length
|
|
749
|
+
? `\n\nDependency results:\n${buildFanInContext(dependencies)}`
|
|
750
|
+
: "";
|
|
697
751
|
const displayTask = task.task;
|
|
698
|
-
const taskWithContext = truncateUtf8(
|
|
699
|
-
`${task.task}${dependencyContext}`,
|
|
700
|
-
DEFAULT_MAX_CONTEXT_BYTES,
|
|
701
|
-
).text;
|
|
702
|
-
const prepared = prepareTask(taskWithContext, task);
|
|
703
752
|
const target = workflowTargets[index];
|
|
704
753
|
const thinkingLevel = resolveThinkingLevel(task.agent, task.thinkingLevel);
|
|
754
|
+
let verifierTreeIdentity:
|
|
755
|
+
| Awaited<ReturnType<typeof captureWorkflowTreeIdentity>>
|
|
756
|
+
| undefined;
|
|
757
|
+
let verifierPreflightError: string | undefined;
|
|
758
|
+
let verifierPreflightCode: string | undefined;
|
|
759
|
+
if (task.verifierFor) {
|
|
760
|
+
const staged = workLedger.get(task.verifierFor);
|
|
761
|
+
if (!staged?.stagedTreeIdentity) {
|
|
762
|
+
verifierPreflightCode = "verification-tree-unavailable";
|
|
763
|
+
verifierPreflightError = `Verification target ${task.verifierFor} has no staged tree identity`;
|
|
764
|
+
} else {
|
|
765
|
+
try {
|
|
766
|
+
verifierTreeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
|
|
767
|
+
if (!sameWorkflowTreeIdentity(staged.stagedTreeIdentity, verifierTreeIdentity)) {
|
|
768
|
+
verifierPreflightCode = "verification-tree-mismatch";
|
|
769
|
+
verifierPreflightError =
|
|
770
|
+
"Workflow verification tree changed before verifier launch";
|
|
771
|
+
}
|
|
772
|
+
} catch (error) {
|
|
773
|
+
if (signal?.aborted) throw error;
|
|
774
|
+
verifierPreflightCode = "verification-tree-unavailable";
|
|
775
|
+
verifierPreflightError = error instanceof Error ? error.message : String(error);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
const verificationTargetTask = task.verifierFor
|
|
780
|
+
? taskById.get(task.verifierFor)?.task
|
|
781
|
+
: undefined;
|
|
782
|
+
const verificationTargetContract = normalizeDelegationContract(
|
|
783
|
+
verificationTargetTask?.contract,
|
|
784
|
+
);
|
|
785
|
+
const verificationSuffix =
|
|
786
|
+
task.verifierFor && verifierTreeIdentity
|
|
787
|
+
? `\n\n${workflowVerificationInstruction(task.verifierFor, verifierTreeIdentity, {
|
|
788
|
+
acceptanceCriteria: [
|
|
789
|
+
...(verificationTargetTask?.acceptanceCriteria ?? []),
|
|
790
|
+
...(verificationTargetContract?.acceptanceCriteria ?? []),
|
|
791
|
+
],
|
|
792
|
+
requiredEvidence: verificationTargetContract?.requiredEvidence ?? [],
|
|
793
|
+
})}`
|
|
794
|
+
: "";
|
|
795
|
+
const baseTask = `${task.task}${dependencyContext}`;
|
|
796
|
+
const baseBudget = Math.max(
|
|
797
|
+
0,
|
|
798
|
+
DEFAULT_MAX_CONTEXT_BYTES - Buffer.byteLength(verificationSuffix, "utf8"),
|
|
799
|
+
);
|
|
800
|
+
const taskWithContext = `${truncateUtf8(baseTask, baseBudget).text}${verificationSuffix}`;
|
|
801
|
+
const prepared = prepareTask(taskWithContext, task);
|
|
705
802
|
const startedItem = startWorkItem(workItemId, task.agent);
|
|
706
803
|
const acceptedTaskGeneration = startedItem?.taskGeneration ?? 0;
|
|
707
804
|
await persistWorkLedger();
|
|
@@ -737,9 +834,20 @@ export async function executeSubagent(
|
|
|
737
834
|
);
|
|
738
835
|
};
|
|
739
836
|
const maxAttempts = task.retryPolicy?.maxAttempts ?? 1;
|
|
740
|
-
let result: SingleResult | undefined
|
|
837
|
+
let result: SingleResult | undefined = verifierPreflightError
|
|
838
|
+
? attachTarget(
|
|
839
|
+
workflowFailureResult(
|
|
840
|
+
task.agent,
|
|
841
|
+
displayTask,
|
|
842
|
+
verifierPreflightCode ?? "verification-tree-unavailable",
|
|
843
|
+
verifierPreflightError,
|
|
844
|
+
thinkingLevel,
|
|
845
|
+
),
|
|
846
|
+
target,
|
|
847
|
+
)
|
|
848
|
+
: undefined;
|
|
741
849
|
let hedged = false;
|
|
742
|
-
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
850
|
+
for (let attempt = 1; !verifierPreflightError && attempt <= maxAttempts; attempt++) {
|
|
743
851
|
if (attempt > 1 && deadline !== undefined && Date.now() >= deadline) break;
|
|
744
852
|
const attempted = await runHedgedAttempt(runAttempt, signal, task.hedgeAfterMs);
|
|
745
853
|
hedged ||= attempted.hedged;
|
|
@@ -765,7 +873,123 @@ export async function executeSubagent(
|
|
|
765
873
|
};
|
|
766
874
|
}
|
|
767
875
|
resultsById.set(workItemId, result);
|
|
768
|
-
|
|
876
|
+
if (result.outcome?.status === "stale") {
|
|
877
|
+
// Cancellation or replacement already rotated and invalidated this generation.
|
|
878
|
+
} else if (task.verifierFor) {
|
|
879
|
+
const staged = workLedger.get(task.verifierFor);
|
|
880
|
+
const structured =
|
|
881
|
+
result.structuredResult?.version === "pi-subagents:result:v2"
|
|
882
|
+
? result.structuredResult
|
|
883
|
+
: undefined;
|
|
884
|
+
let failureReason: string | undefined = verifierPreflightError;
|
|
885
|
+
let failureCode: string | undefined = verifierPreflightCode;
|
|
886
|
+
let postVerifierIdentity = verifierTreeIdentity;
|
|
887
|
+
if (!failureReason) {
|
|
888
|
+
try {
|
|
889
|
+
postVerifierIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
|
|
890
|
+
if (
|
|
891
|
+
!verifierTreeIdentity ||
|
|
892
|
+
!staged?.stagedTreeIdentity ||
|
|
893
|
+
!sameWorkflowTreeIdentity(verifierTreeIdentity, postVerifierIdentity) ||
|
|
894
|
+
!sameWorkflowTreeIdentity(staged.stagedTreeIdentity, postVerifierIdentity)
|
|
895
|
+
) {
|
|
896
|
+
failureCode = "verification-tree-mismatch";
|
|
897
|
+
failureReason = "Workflow verification tree changed during verifier execution";
|
|
898
|
+
}
|
|
899
|
+
} catch (error) {
|
|
900
|
+
if (signal?.aborted) throw error;
|
|
901
|
+
failureCode = "verification-tree-unavailable";
|
|
902
|
+
failureReason = error instanceof Error ? error.message : String(error);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (
|
|
906
|
+
!failureReason &&
|
|
907
|
+
structured &&
|
|
908
|
+
staged?.acceptedExecutionPlanId &&
|
|
909
|
+
result.executionPlan?.id &&
|
|
910
|
+
postVerifierIdentity
|
|
911
|
+
) {
|
|
912
|
+
try {
|
|
913
|
+
const receipt = createWorkflowVerificationReceipt(structured, {
|
|
914
|
+
targetTaskId: staged.id,
|
|
915
|
+
targetTaskGeneration: staged.taskGeneration,
|
|
916
|
+
targetExecutionPlanId: staged.acceptedExecutionPlanId,
|
|
917
|
+
verifierTaskId: workItemId,
|
|
918
|
+
verifierTaskGeneration: acceptedTaskGeneration,
|
|
919
|
+
verifierExecutionPlanId: result.executionPlan.id,
|
|
920
|
+
treeIdentity: postVerifierIdentity,
|
|
921
|
+
sourceTruncated: result.truncated === true,
|
|
922
|
+
});
|
|
923
|
+
workLedger.completeVerification(workItemId, {
|
|
924
|
+
taskGeneration: acceptedTaskGeneration,
|
|
925
|
+
executionPlanId: result.executionPlan.id,
|
|
926
|
+
receipt,
|
|
927
|
+
});
|
|
928
|
+
if (receipt.decision !== "accept") {
|
|
929
|
+
const targetResult = resultsById.get(staged.id);
|
|
930
|
+
if (targetResult) {
|
|
931
|
+
targetResult.outcome = {
|
|
932
|
+
status: receipt.decision === "rework" ? "blocked" : "failed",
|
|
933
|
+
reasonCode:
|
|
934
|
+
receipt.decision === "rework"
|
|
935
|
+
? "verification-rework"
|
|
936
|
+
: "verification-rejected",
|
|
937
|
+
recoveryActions:
|
|
938
|
+
receipt.decision === "rework" ? ["replan", "verify"] : ["stop"],
|
|
939
|
+
retryable: false,
|
|
940
|
+
};
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
} catch (error) {
|
|
944
|
+
failureCode = "verification-receipt-invalid";
|
|
945
|
+
failureReason = error instanceof Error ? error.message : String(error);
|
|
946
|
+
}
|
|
947
|
+
} else if (!failureReason) {
|
|
948
|
+
failureCode = "verification-receipt-invalid";
|
|
949
|
+
failureReason = "Workflow verifier did not return a current structured-v2 result";
|
|
950
|
+
}
|
|
951
|
+
if (failureReason) {
|
|
952
|
+
const reasonCode = failureCode ?? "verification-receipt-invalid";
|
|
953
|
+
result.outcome = {
|
|
954
|
+
status:
|
|
955
|
+
reasonCode === "verification-receipt-invalid" ? "contract-invalid" : "failed",
|
|
956
|
+
reasonCode,
|
|
957
|
+
recoveryActions:
|
|
958
|
+
reasonCode === "verification-receipt-invalid"
|
|
959
|
+
? ["repair-contract"]
|
|
960
|
+
: ["revalidate"],
|
|
961
|
+
retryable: false,
|
|
962
|
+
};
|
|
963
|
+
result.errorMessage = failureReason;
|
|
964
|
+
workLedger.failVerification(workItemId, reasonCode);
|
|
965
|
+
}
|
|
966
|
+
} else if (verificationTargetIds.has(workItemId) && !isResultError(result)) {
|
|
967
|
+
try {
|
|
968
|
+
const treeIdentity = await captureWorkflowTreeIdentity(target.cwd, { signal });
|
|
969
|
+
if (!result.executionPlan?.id) {
|
|
970
|
+
throw new Error("Verification-required producer has no accepted ExecutionPlan");
|
|
971
|
+
}
|
|
972
|
+
workLedger.stageForVerification(workItemId, {
|
|
973
|
+
taskGeneration: acceptedTaskGeneration,
|
|
974
|
+
executionPlanId: result.executionPlan.id,
|
|
975
|
+
artifacts: artifactsFromResult(result),
|
|
976
|
+
treeIdentity,
|
|
977
|
+
});
|
|
978
|
+
} catch (error) {
|
|
979
|
+
if (signal?.aborted) throw error;
|
|
980
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
981
|
+
result.outcome = {
|
|
982
|
+
status: "failed",
|
|
983
|
+
reasonCode: "verification-tree-unavailable",
|
|
984
|
+
recoveryActions: ["revalidate"],
|
|
985
|
+
retryable: false,
|
|
986
|
+
};
|
|
987
|
+
result.errorMessage = message;
|
|
988
|
+
settleWorkItem(workItemId, result, acceptedTaskGeneration);
|
|
989
|
+
}
|
|
990
|
+
} else {
|
|
991
|
+
settleWorkItem(workItemId, result, acceptedTaskGeneration);
|
|
992
|
+
}
|
|
769
993
|
await persistWorkLedger();
|
|
770
994
|
return result;
|
|
771
995
|
},
|
|
@@ -774,11 +998,19 @@ export async function executeSubagent(
|
|
|
774
998
|
if (batch.length === 0 || signal?.aborted) break;
|
|
775
999
|
}
|
|
776
1000
|
for (const item of workLedger.snapshot().items) {
|
|
777
|
-
if (
|
|
1001
|
+
if (
|
|
1002
|
+
item.state !== "pending" &&
|
|
1003
|
+
item.state !== "ready" &&
|
|
1004
|
+
item.state !== "awaiting-verification"
|
|
1005
|
+
) {
|
|
1006
|
+
continue;
|
|
1007
|
+
}
|
|
778
1008
|
if (signal?.aborted) {
|
|
779
1009
|
workLedger.settle(item.id, "interrupted", "parent-aborted");
|
|
780
1010
|
} else if (deadline !== undefined && Date.now() >= deadline) {
|
|
781
1011
|
workLedger.settle(item.id, "blocked", "budget-exhausted");
|
|
1012
|
+
} else if (item.state === "awaiting-verification") {
|
|
1013
|
+
workLedger.settle(item.id, "blocked", "verification-not-completed");
|
|
782
1014
|
} else {
|
|
783
1015
|
const dependencyBlocked = item.dependencies.some(
|
|
784
1016
|
(dependency) => workLedger.get(dependency)?.state !== "completed",
|
|
@@ -793,8 +1025,26 @@ export async function executeSubagent(
|
|
|
793
1025
|
await persistWorkLedger();
|
|
794
1026
|
const results = resolvedWorkflowTasks.map((task) => {
|
|
795
1027
|
const completed = resultsById.get(task.id);
|
|
796
|
-
if (completed) return completed;
|
|
797
1028
|
const item = workLedger.get(task.id);
|
|
1029
|
+
if (completed) {
|
|
1030
|
+
if (item && item.state !== "completed" && !isResultError(completed)) {
|
|
1031
|
+
completed.outcome = {
|
|
1032
|
+
status:
|
|
1033
|
+
item.state === "needs-input"
|
|
1034
|
+
? "needs-input"
|
|
1035
|
+
: item.state === "interrupted"
|
|
1036
|
+
? "interrupted"
|
|
1037
|
+
: item.state === "failed"
|
|
1038
|
+
? "failed"
|
|
1039
|
+
: "blocked",
|
|
1040
|
+
reasonCode: item.outcomeReason ?? "verification-not-accepted",
|
|
1041
|
+
recoveryActions:
|
|
1042
|
+
item.outcomeReason === "verification-rework" ? ["replan", "verify"] : ["stop"],
|
|
1043
|
+
retryable: false,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1046
|
+
return completed;
|
|
1047
|
+
}
|
|
798
1048
|
const outcomeStatus =
|
|
799
1049
|
item?.state === "interrupted"
|
|
800
1050
|
? "interrupted"
|
package/src/inspect.ts
CHANGED
|
@@ -537,6 +537,31 @@ function projectWorkflow(workflow: WorkItemLedgerSnapshot): Record<string, unkno
|
|
|
537
537
|
verified: artifact.verified,
|
|
538
538
|
})),
|
|
539
539
|
verificationAccepted: item.verificationAccepted,
|
|
540
|
+
stagedTreeIdentity: item.stagedTreeIdentity
|
|
541
|
+
? {
|
|
542
|
+
version: item.stagedTreeIdentity.version,
|
|
543
|
+
kind: item.stagedTreeIdentity.kind,
|
|
544
|
+
digest: item.stagedTreeIdentity.digest,
|
|
545
|
+
}
|
|
546
|
+
: undefined,
|
|
547
|
+
verificationReceipt: item.verificationReceipt
|
|
548
|
+
? {
|
|
549
|
+
version: item.verificationReceipt.version,
|
|
550
|
+
decision: item.verificationReceipt.decision,
|
|
551
|
+
targetTaskId: boundedPrivateText(item.verificationReceipt.targetTaskId, 256),
|
|
552
|
+
targetTaskGeneration: item.verificationReceipt.targetTaskGeneration,
|
|
553
|
+
targetExecutionPlanId: item.verificationReceipt.targetExecutionPlanId,
|
|
554
|
+
verifierTaskId: boundedPrivateText(item.verificationReceipt.verifierTaskId, 256),
|
|
555
|
+
verifierTaskGeneration: item.verificationReceipt.verifierTaskGeneration,
|
|
556
|
+
verifierExecutionPlanId: item.verificationReceipt.verifierExecutionPlanId,
|
|
557
|
+
treeIdentity: item.verificationReceipt.treeIdentity,
|
|
558
|
+
summary: boundedPrivateText(item.verificationReceipt.summary, 8 * 1024),
|
|
559
|
+
evidenceCount: item.verificationReceipt.evidence.length,
|
|
560
|
+
limitationCount: item.verificationReceipt.limitations.length,
|
|
561
|
+
createdAt: item.verificationReceipt.createdAt,
|
|
562
|
+
truncated: item.verificationReceipt.truncated,
|
|
563
|
+
}
|
|
564
|
+
: undefined,
|
|
540
565
|
outcomeReason: item.outcomeReason
|
|
541
566
|
? boundedPrivateText(item.outcomeReason, 2 * 1024)
|
|
542
567
|
: undefined,
|
|
@@ -14,6 +14,12 @@ export interface OrchestrationMetrics {
|
|
|
14
14
|
requestedTools: number;
|
|
15
15
|
effectiveRequestedTools: number;
|
|
16
16
|
permissionPrecision: number;
|
|
17
|
+
workerReportedVerification: number;
|
|
18
|
+
executorAcceptedVerification: number;
|
|
19
|
+
verificationRework: number;
|
|
20
|
+
verificationRejected: number;
|
|
21
|
+
verificationInvalid: number;
|
|
22
|
+
verificationTreeMismatch: number;
|
|
17
23
|
panelValidReviews?: number;
|
|
18
24
|
panelFailedReviews?: number;
|
|
19
25
|
panelBlockingObjections?: number;
|
|
@@ -65,6 +71,31 @@ export function calculateOrchestrationMetrics(
|
|
|
65
71
|
requestedTools,
|
|
66
72
|
effectiveRequestedTools,
|
|
67
73
|
permissionPrecision: requestedTools === 0 ? 1 : effectiveRequestedTools / requestedTools,
|
|
74
|
+
workerReportedVerification: results.filter(
|
|
75
|
+
(result, index) =>
|
|
76
|
+
!items[index]?.verifierFor &&
|
|
77
|
+
result.structuredResult?.version === "pi-subagents:result:v2" &&
|
|
78
|
+
result.structuredResult.verification.some(
|
|
79
|
+
(verification) => verification.status === "passed",
|
|
80
|
+
),
|
|
81
|
+
).length,
|
|
82
|
+
executorAcceptedVerification: items.filter((item) => item.verificationAccepted).length,
|
|
83
|
+
verificationRework: items.filter(
|
|
84
|
+
(item) => !item.verifierFor && item.verificationReceipt?.decision === "rework",
|
|
85
|
+
).length,
|
|
86
|
+
verificationRejected: items.filter(
|
|
87
|
+
(item) => !item.verifierFor && item.verificationReceipt?.decision === "reject",
|
|
88
|
+
).length,
|
|
89
|
+
verificationInvalid: items.filter(
|
|
90
|
+
(item) => !item.verifierFor && item.outcomeReason === "verification-receipt-invalid",
|
|
91
|
+
).length,
|
|
92
|
+
verificationTreeMismatch: items.filter(
|
|
93
|
+
(item) =>
|
|
94
|
+
!item.verifierFor &&
|
|
95
|
+
["verification-tree-mismatch", "verification-tree-unavailable"].includes(
|
|
96
|
+
item.outcomeReason ?? "",
|
|
97
|
+
),
|
|
98
|
+
).length,
|
|
68
99
|
...(panel
|
|
69
100
|
? {
|
|
70
101
|
panelValidReviews: panel.validReviewCount,
|
package/src/panel-execution.ts
CHANGED
|
@@ -389,7 +389,6 @@ export async function executePanel(input: PanelExecutionInput): Promise<PanelToo
|
|
|
389
389
|
},
|
|
390
390
|
]
|
|
391
391
|
: [],
|
|
392
|
-
verificationAccepted: false,
|
|
393
392
|
});
|
|
394
393
|
}
|
|
395
394
|
completed += 1;
|
|
@@ -554,7 +553,6 @@ export async function executePanel(input: PanelExecutionInput): Promise<PanelToo
|
|
|
554
553
|
workLedger.complete("synthesis", {
|
|
555
554
|
taskGeneration: synthesisWork.taskGeneration,
|
|
556
555
|
executionPlanId: synthesisResult.executionPlan?.id,
|
|
557
|
-
verificationAccepted: false,
|
|
558
556
|
});
|
|
559
557
|
}
|
|
560
558
|
await persistWork();
|
package/src/params.ts
CHANGED
|
@@ -106,7 +106,14 @@ const WorkflowTaskItem = Type.Object({
|
|
|
106
106
|
ownershipKeys: Type.Optional(Type.Array(Type.String({ maxLength: 256 }), { maxItems: 50 })),
|
|
107
107
|
acceptanceCriteria: Type.Optional(Type.Array(Type.String({ maxLength: 4096 }), { maxItems: 50 })),
|
|
108
108
|
integrationOwner: Type.Optional(Type.Boolean()),
|
|
109
|
-
verifierFor: Type.Optional(
|
|
109
|
+
verifierFor: Type.Optional(
|
|
110
|
+
Type.String({
|
|
111
|
+
minLength: 1,
|
|
112
|
+
maxLength: 256,
|
|
113
|
+
description:
|
|
114
|
+
"Target task ID for one distinct direct-dependent structured-v2 verifier. The executor gates target acceptance on a current exact-tree receipt.",
|
|
115
|
+
}),
|
|
116
|
+
),
|
|
110
117
|
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process" })),
|
|
111
118
|
timeoutMs: Type.Optional(TimeoutMs),
|
|
112
119
|
...TurnLimitFields,
|