@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
|
@@ -6,6 +6,14 @@ export interface VerificationRiskInput {
|
|
|
6
6
|
requiredCapabilities: string[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
export interface WorkflowVerificationTaskProjection {
|
|
10
|
+
id: string;
|
|
11
|
+
agent: string;
|
|
12
|
+
dependsOn?: readonly string[];
|
|
13
|
+
verifierFor?: string;
|
|
14
|
+
resultFormat?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
9
17
|
export function requiresIndependentVerification(input: VerificationRiskInput): boolean {
|
|
10
18
|
return (
|
|
11
19
|
input.contract?.admission?.verificationRequired === true ||
|
|
@@ -15,3 +23,45 @@ export function requiresIndependentVerification(input: VerificationRiskInput): b
|
|
|
15
23
|
(input.integrationOwner && input.contract?.sideEffectPolicy !== "read-only")
|
|
16
24
|
);
|
|
17
25
|
}
|
|
26
|
+
|
|
27
|
+
export function validateWorkflowVerificationGraph(
|
|
28
|
+
tasks: readonly WorkflowVerificationTaskProjection[],
|
|
29
|
+
requiredTargetIds: ReadonlySet<string>,
|
|
30
|
+
): void {
|
|
31
|
+
const byId = new Map(tasks.map((task) => [task.id, task]));
|
|
32
|
+
const verifierByTarget = new Map<string, WorkflowVerificationTaskProjection[]>();
|
|
33
|
+
for (const task of tasks) {
|
|
34
|
+
if (!task.verifierFor) continue;
|
|
35
|
+
const target = byId.get(task.verifierFor);
|
|
36
|
+
if (!target) throw new Error(`Workflow verifier ${task.id} targets a missing task`);
|
|
37
|
+
if (target.verifierFor) {
|
|
38
|
+
throw new Error(`Workflow verifier ${task.id} cannot verify another verifier`);
|
|
39
|
+
}
|
|
40
|
+
if (target.resultFormat !== "structured-v2") {
|
|
41
|
+
throw new Error(`Workflow verification target ${target.id} must request structured-v2`);
|
|
42
|
+
}
|
|
43
|
+
if (task.dependsOn?.length !== 1 || task.dependsOn[0] !== target.id) {
|
|
44
|
+
throw new Error(`Workflow verifier ${task.id} must depend directly and only on ${target.id}`);
|
|
45
|
+
}
|
|
46
|
+
if (task.agent === target.agent) {
|
|
47
|
+
throw new Error(`Workflow verifier ${task.id} must use a distinct agent`);
|
|
48
|
+
}
|
|
49
|
+
if (task.resultFormat !== "structured-v2") {
|
|
50
|
+
throw new Error(`Workflow verifier ${task.id} must request structured-v2`);
|
|
51
|
+
}
|
|
52
|
+
const entries = verifierByTarget.get(target.id) ?? [];
|
|
53
|
+
entries.push(task);
|
|
54
|
+
verifierByTarget.set(target.id, entries);
|
|
55
|
+
}
|
|
56
|
+
for (const targetId of requiredTargetIds) {
|
|
57
|
+
const verifiers = verifierByTarget.get(targetId) ?? [];
|
|
58
|
+
if (verifiers.length !== 1) {
|
|
59
|
+
throw new Error(`Workflow task ${targetId} requires exactly one independent verifier`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
for (const [targetId, verifiers] of verifierByTarget) {
|
|
63
|
+
if (verifiers.length !== 1) {
|
|
64
|
+
throw new Error(`Workflow task ${targetId} must have exactly one verifier`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/work-item-ledger.ts
CHANGED
|
@@ -3,13 +3,24 @@ import {
|
|
|
3
3
|
type ManagedIntegrationExpectation,
|
|
4
4
|
verifyManagedIntegration,
|
|
5
5
|
} from "./integration-controller.js";
|
|
6
|
+
import {
|
|
7
|
+
isWorkflowTreeIdentity,
|
|
8
|
+
sameWorkflowTreeIdentity,
|
|
9
|
+
type WorkflowTreeIdentity,
|
|
10
|
+
} from "./workflow-tree-identity.js";
|
|
11
|
+
import {
|
|
12
|
+
isWorkflowVerificationReceipt,
|
|
13
|
+
type WorkflowVerificationReceipt,
|
|
14
|
+
} from "./workflow-verification.js";
|
|
6
15
|
|
|
7
|
-
export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:
|
|
16
|
+
export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v2" as const;
|
|
17
|
+
const LEGACY_WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v1" as const;
|
|
8
18
|
|
|
9
19
|
export type WorkItemState =
|
|
10
20
|
| "pending"
|
|
11
21
|
| "ready"
|
|
12
22
|
| "running"
|
|
23
|
+
| "awaiting-verification"
|
|
13
24
|
| "blocked"
|
|
14
25
|
| "needs-input"
|
|
15
26
|
| "completed"
|
|
@@ -74,6 +85,8 @@ export interface WorkItemRecord {
|
|
|
74
85
|
verifierFor?: string;
|
|
75
86
|
dependencyPolicy: "completed" | "settled";
|
|
76
87
|
verificationAccepted: boolean;
|
|
88
|
+
stagedTreeIdentity?: WorkflowTreeIdentity;
|
|
89
|
+
verificationReceipt?: WorkflowVerificationReceipt;
|
|
77
90
|
invalidationReasons: string[];
|
|
78
91
|
outcomeReason?: string;
|
|
79
92
|
}
|
|
@@ -94,7 +107,17 @@ export interface CompleteWorkItemInput {
|
|
|
94
107
|
taskGeneration: number;
|
|
95
108
|
executionPlanId?: string;
|
|
96
109
|
artifacts?: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>;
|
|
97
|
-
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface StageWorkItemVerificationInput extends CompleteWorkItemInput {
|
|
113
|
+
executionPlanId: string;
|
|
114
|
+
treeIdentity: WorkflowTreeIdentity;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export interface CompleteWorkItemVerificationInput {
|
|
118
|
+
taskGeneration: number;
|
|
119
|
+
executionPlanId: string;
|
|
120
|
+
receipt: WorkflowVerificationReceipt;
|
|
98
121
|
}
|
|
99
122
|
|
|
100
123
|
const MAX_ITEMS = 64;
|
|
@@ -165,18 +188,107 @@ export class WorkItemLedger {
|
|
|
165
188
|
item.acceptedExecutionPlanId = input.executionPlanId?.slice(0, 256);
|
|
166
189
|
item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
|
|
167
190
|
item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
|
|
168
|
-
item.verificationAccepted =
|
|
191
|
+
item.verificationAccepted = false;
|
|
192
|
+
item.stagedTreeIdentity = undefined;
|
|
193
|
+
item.verificationReceipt = undefined;
|
|
169
194
|
item.state = "completed";
|
|
170
195
|
item.generation = ++this.generation;
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
196
|
+
this.refreshReadyState();
|
|
197
|
+
return structuredClone(item);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
stageForVerification(id: string, input: StageWorkItemVerificationInput): WorkItemRecord {
|
|
201
|
+
const item = this.require(id);
|
|
202
|
+
this.assertMutable(item);
|
|
203
|
+
if (item.state !== "running") {
|
|
204
|
+
throw new Error(`WorkItem ${id} cannot stage verification while ${item.state}`);
|
|
205
|
+
}
|
|
206
|
+
if (input.taskGeneration !== item.taskGeneration) {
|
|
207
|
+
throw new Error(`WorkItem ${id} rejected a stale task generation`);
|
|
208
|
+
}
|
|
209
|
+
validatePlanId(input.executionPlanId, "staged execution plan");
|
|
210
|
+
if (!isWorkflowTreeIdentity(input.treeIdentity)) {
|
|
211
|
+
throw new Error(`WorkItem ${id} received an invalid staged tree identity`);
|
|
175
212
|
}
|
|
213
|
+
item.acceptedExecutionPlanId = input.executionPlanId;
|
|
214
|
+
item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
|
|
215
|
+
item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
|
|
216
|
+
item.verificationAccepted = false;
|
|
217
|
+
item.stagedTreeIdentity = structuredClone(input.treeIdentity);
|
|
218
|
+
item.verificationReceipt = undefined;
|
|
219
|
+
item.state = "awaiting-verification";
|
|
220
|
+
item.generation = ++this.generation;
|
|
176
221
|
this.refreshReadyState();
|
|
177
222
|
return structuredClone(item);
|
|
178
223
|
}
|
|
179
224
|
|
|
225
|
+
completeVerification(
|
|
226
|
+
verifierId: string,
|
|
227
|
+
input: CompleteWorkItemVerificationInput,
|
|
228
|
+
): { target: WorkItemRecord; verifier: WorkItemRecord } {
|
|
229
|
+
const verifier = this.require(verifierId);
|
|
230
|
+
this.assertMutable(verifier);
|
|
231
|
+
if (verifier.state !== "running" || !verifier.verifierFor) {
|
|
232
|
+
throw new Error(`WorkItem ${verifierId} is not a running verifier`);
|
|
233
|
+
}
|
|
234
|
+
if (input.taskGeneration !== verifier.taskGeneration) {
|
|
235
|
+
throw new Error(`WorkItem ${verifierId} rejected a stale verifier generation`);
|
|
236
|
+
}
|
|
237
|
+
validatePlanId(input.executionPlanId, "verifier execution plan");
|
|
238
|
+
if (!isWorkflowVerificationReceipt(input.receipt)) {
|
|
239
|
+
throw new Error(`WorkItem ${verifierId} received an invalid verification receipt`);
|
|
240
|
+
}
|
|
241
|
+
const target = this.require(verifier.verifierFor);
|
|
242
|
+
if (target.state !== "awaiting-verification") {
|
|
243
|
+
throw new Error(`WorkItem ${target.id} is not awaiting verification`);
|
|
244
|
+
}
|
|
245
|
+
assertReceiptMatches(target, verifier, input.executionPlanId, input.receipt);
|
|
246
|
+
verifier.acceptedExecutionPlanId = input.executionPlanId;
|
|
247
|
+
verifier.verificationReceipt = undefined;
|
|
248
|
+
verifier.verificationAccepted = false;
|
|
249
|
+
verifier.state = "completed";
|
|
250
|
+
verifier.generation = ++this.generation;
|
|
251
|
+
target.verificationReceipt = structuredClone(input.receipt);
|
|
252
|
+
target.verificationAccepted = input.receipt.decision === "accept";
|
|
253
|
+
target.outcomeReason =
|
|
254
|
+
input.receipt.decision === "accept"
|
|
255
|
+
? undefined
|
|
256
|
+
: input.receipt.decision === "rework"
|
|
257
|
+
? "verification-rework"
|
|
258
|
+
: "verification-rejected";
|
|
259
|
+
if (input.receipt.decision === "accept") {
|
|
260
|
+
target.artifacts = target.artifacts.map((artifact) => ({ ...artifact, verified: true }));
|
|
261
|
+
target.state = "completed";
|
|
262
|
+
} else {
|
|
263
|
+
target.state = input.receipt.decision === "rework" ? "blocked" : "failed";
|
|
264
|
+
this.invalidateDependents(target.id, verifier.id, target.outcomeReason);
|
|
265
|
+
}
|
|
266
|
+
target.generation = ++this.generation;
|
|
267
|
+
this.refreshReadyState();
|
|
268
|
+
return { target: structuredClone(target), verifier: structuredClone(verifier) };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
failVerification(verifierId: string, reason: string): WorkItemRecord[] {
|
|
272
|
+
const verifier = this.require(verifierId);
|
|
273
|
+
if (!verifier.verifierFor) throw new Error(`WorkItem ${verifierId} is not a verifier`);
|
|
274
|
+
const target = this.require(verifier.verifierFor);
|
|
275
|
+
const boundedReason = bounded(reason, MAX_TEXT_LENGTH);
|
|
276
|
+
if (!boundedReason) throw new Error("Verification failure requires a reason");
|
|
277
|
+
if (!TERMINAL_STATES.has(verifier.state)) {
|
|
278
|
+
verifier.state = "failed";
|
|
279
|
+
verifier.outcomeReason = boundedReason;
|
|
280
|
+
verifier.generation = ++this.generation;
|
|
281
|
+
}
|
|
282
|
+
if (target.state === "awaiting-verification") {
|
|
283
|
+
target.state = "failed";
|
|
284
|
+
target.verificationAccepted = false;
|
|
285
|
+
target.outcomeReason = boundedReason;
|
|
286
|
+
target.generation = ++this.generation;
|
|
287
|
+
}
|
|
288
|
+
const invalidated = this.invalidateDependents(target.id, verifier.id, boundedReason);
|
|
289
|
+
return [structuredClone(target), structuredClone(verifier), ...invalidated];
|
|
290
|
+
}
|
|
291
|
+
|
|
180
292
|
settle(
|
|
181
293
|
id: string,
|
|
182
294
|
state: "blocked" | "needs-input" | "failed" | "interrupted",
|
|
@@ -184,7 +296,12 @@ export class WorkItemLedger {
|
|
|
184
296
|
): WorkItemRecord {
|
|
185
297
|
const item = this.require(id);
|
|
186
298
|
this.assertMutable(item);
|
|
187
|
-
if (
|
|
299
|
+
if (
|
|
300
|
+
item.state !== "running" &&
|
|
301
|
+
item.state !== "awaiting-verification" &&
|
|
302
|
+
item.state !== "ready" &&
|
|
303
|
+
item.state !== "pending"
|
|
304
|
+
) {
|
|
188
305
|
throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
|
|
189
306
|
}
|
|
190
307
|
item.state = state;
|
|
@@ -230,6 +347,8 @@ export class WorkItemLedger {
|
|
|
230
347
|
item.acceptedExecutionPlanId = undefined;
|
|
231
348
|
item.outcomeReason = undefined;
|
|
232
349
|
item.verificationAccepted = false;
|
|
350
|
+
item.stagedTreeIdentity = undefined;
|
|
351
|
+
item.verificationReceipt = undefined;
|
|
233
352
|
item.generation = ++this.generation;
|
|
234
353
|
this.refreshReadyState();
|
|
235
354
|
return structuredClone(item);
|
|
@@ -271,12 +390,14 @@ export class WorkItemLedger {
|
|
|
271
390
|
static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
|
|
272
391
|
if (
|
|
273
392
|
!snapshot ||
|
|
274
|
-
snapshot.version !== WORK_ITEM_LEDGER_VERSION
|
|
393
|
+
(snapshot.version !== WORK_ITEM_LEDGER_VERSION &&
|
|
394
|
+
(snapshot.version as string) !== LEGACY_WORK_ITEM_LEDGER_VERSION) ||
|
|
275
395
|
!Number.isSafeInteger(snapshot.generation) ||
|
|
276
396
|
snapshot.generation < 0
|
|
277
397
|
) {
|
|
278
398
|
throw new Error("Unsupported or malformed WorkItem ledger snapshot");
|
|
279
399
|
}
|
|
400
|
+
const isLegacySnapshot = (snapshot.version as string) === LEGACY_WORK_ITEM_LEDGER_VERSION;
|
|
280
401
|
if (
|
|
281
402
|
!Array.isArray(snapshot.items) ||
|
|
282
403
|
snapshot.items.length < 1 ||
|
|
@@ -309,25 +430,60 @@ export class WorkItemLedger {
|
|
|
309
430
|
ledger.generation = snapshot.generation;
|
|
310
431
|
for (const stored of snapshot.items) {
|
|
311
432
|
const item = ledger.require(stored.id);
|
|
312
|
-
item.state =
|
|
433
|
+
item.state =
|
|
434
|
+
stored.state === "running" || stored.state === "awaiting-verification"
|
|
435
|
+
? "interrupted"
|
|
436
|
+
: stored.state;
|
|
313
437
|
item.generation = stored.generation;
|
|
314
438
|
item.taskGeneration = stored.taskGeneration ?? 1;
|
|
315
439
|
item.assignedAgentId = stored.assignedAgentId;
|
|
316
440
|
item.acceptedExecutionPlanId = stored.acceptedExecutionPlanId;
|
|
317
441
|
item.inputArtifactVersions = { ...stored.inputArtifactVersions };
|
|
318
|
-
item.artifacts = normalizeStoredArtifacts(
|
|
442
|
+
item.artifacts = normalizeStoredArtifacts(
|
|
443
|
+
stored.artifacts,
|
|
444
|
+
stored.id,
|
|
445
|
+
stored.generation,
|
|
446
|
+
!isLegacySnapshot,
|
|
447
|
+
);
|
|
319
448
|
item.artifactHistory = normalizeStoredArtifacts(
|
|
320
449
|
stored.artifactHistory ?? [],
|
|
321
450
|
stored.id,
|
|
322
451
|
stored.generation,
|
|
452
|
+
!isLegacySnapshot,
|
|
323
453
|
);
|
|
324
|
-
item.verificationAccepted = stored.verificationAccepted;
|
|
454
|
+
item.verificationAccepted = !isLegacySnapshot && stored.verificationAccepted;
|
|
455
|
+
item.stagedTreeIdentity =
|
|
456
|
+
!isLegacySnapshot && stored.stagedTreeIdentity
|
|
457
|
+
? structuredClone(stored.stagedTreeIdentity)
|
|
458
|
+
: undefined;
|
|
459
|
+
item.verificationReceipt =
|
|
460
|
+
!isLegacySnapshot && stored.verificationReceipt
|
|
461
|
+
? structuredClone(stored.verificationReceipt)
|
|
462
|
+
: undefined;
|
|
325
463
|
item.invalidationReasons = [...stored.invalidationReasons];
|
|
326
464
|
item.outcomeReason = stored.outcomeReason;
|
|
327
465
|
}
|
|
466
|
+
ledger.validateRestoredVerificationLinks();
|
|
328
467
|
return ledger;
|
|
329
468
|
}
|
|
330
469
|
|
|
470
|
+
private validateRestoredVerificationLinks(): void {
|
|
471
|
+
for (const target of this.items.values()) {
|
|
472
|
+
const receipt = target.verificationReceipt;
|
|
473
|
+
if (!receipt) continue;
|
|
474
|
+
const verifier = this.items.get(receipt.verifierTaskId);
|
|
475
|
+
if (
|
|
476
|
+
!verifier ||
|
|
477
|
+
verifier.verifierFor !== target.id ||
|
|
478
|
+
verifier.state !== "completed" ||
|
|
479
|
+
verifier.taskGeneration !== receipt.verifierTaskGeneration ||
|
|
480
|
+
verifier.acceptedExecutionPlanId !== receipt.verifierExecutionPlanId
|
|
481
|
+
) {
|
|
482
|
+
throw new Error(`Malformed stored WorkItem verification link for ${target.id}`);
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
331
487
|
private addDefinition(definition: WorkItemDefinition): void {
|
|
332
488
|
validateIdentifier(definition.id, "WorkItem id");
|
|
333
489
|
if (this.items.has(definition.id)) throw new Error(`Duplicate WorkItem id ${definition.id}`);
|
|
@@ -365,6 +521,8 @@ export class WorkItemLedger {
|
|
|
365
521
|
verifierFor: definition.verifierFor,
|
|
366
522
|
dependencyPolicy: definition.dependencyPolicy ?? "completed",
|
|
367
523
|
verificationAccepted: false,
|
|
524
|
+
stagedTreeIdentity: undefined,
|
|
525
|
+
verificationReceipt: undefined,
|
|
368
526
|
invalidationReasons: [],
|
|
369
527
|
});
|
|
370
528
|
}
|
|
@@ -406,10 +564,18 @@ export class WorkItemLedger {
|
|
|
406
564
|
for (const item of this.items.values()) {
|
|
407
565
|
if (item.state !== "pending") continue;
|
|
408
566
|
const dependencies = item.dependencies.map((id) => this.require(id));
|
|
409
|
-
const dependenciesReady =
|
|
410
|
-
|
|
567
|
+
const dependenciesReady = item.verifierFor
|
|
568
|
+
? dependencies.every((dependency) =>
|
|
569
|
+
dependency.id === item.verifierFor
|
|
570
|
+
? dependency.state === "awaiting-verification"
|
|
571
|
+
: dependency.state === "completed",
|
|
572
|
+
)
|
|
573
|
+
: item.dependencyPolicy === "settled"
|
|
411
574
|
? dependencies.every(
|
|
412
|
-
(dependency) =>
|
|
575
|
+
(dependency) =>
|
|
576
|
+
!["pending", "ready", "running", "awaiting-verification"].includes(
|
|
577
|
+
dependency.state,
|
|
578
|
+
),
|
|
413
579
|
)
|
|
414
580
|
: dependencies.every((dependency) => dependency.state === "completed");
|
|
415
581
|
if (!dependenciesReady) continue;
|
|
@@ -433,6 +599,31 @@ export class WorkItemLedger {
|
|
|
433
599
|
}
|
|
434
600
|
}
|
|
435
601
|
|
|
602
|
+
private invalidateDependents(
|
|
603
|
+
targetId: string,
|
|
604
|
+
excludedId: string,
|
|
605
|
+
reason: string | undefined,
|
|
606
|
+
): WorkItemRecord[] {
|
|
607
|
+
const target = this.require(targetId);
|
|
608
|
+
const normalizedReason = bounded(reason ?? "verification-not-accepted", MAX_TEXT_LENGTH);
|
|
609
|
+
const queue = target.dependents.filter((id) => id !== excludedId);
|
|
610
|
+
const seen = new Set<string>();
|
|
611
|
+
const affected: WorkItemRecord[] = [];
|
|
612
|
+
while (queue.length > 0) {
|
|
613
|
+
const currentId = queue.shift();
|
|
614
|
+
if (!currentId || seen.has(currentId)) continue;
|
|
615
|
+
seen.add(currentId);
|
|
616
|
+
const current = this.require(currentId);
|
|
617
|
+
current.state = "invalidated";
|
|
618
|
+
current.taskGeneration++;
|
|
619
|
+
current.invalidationReasons.push(`${targetId}:${normalizedReason}`);
|
|
620
|
+
current.generation = ++this.generation;
|
|
621
|
+
affected.push(structuredClone(current));
|
|
622
|
+
queue.push(...current.dependents.filter((id) => id !== excludedId));
|
|
623
|
+
}
|
|
624
|
+
return affected;
|
|
625
|
+
}
|
|
626
|
+
|
|
436
627
|
private require(id: string): WorkItemRecord {
|
|
437
628
|
const item = this.items.get(id);
|
|
438
629
|
if (!item) throw new Error(`Unknown WorkItem ${id}`);
|
|
@@ -446,11 +637,36 @@ export class WorkItemLedger {
|
|
|
446
637
|
}
|
|
447
638
|
}
|
|
448
639
|
|
|
640
|
+
function assertReceiptMatches(
|
|
641
|
+
target: WorkItemRecord,
|
|
642
|
+
verifier: WorkItemRecord,
|
|
643
|
+
verifierExecutionPlanId: string,
|
|
644
|
+
receipt: WorkflowVerificationReceipt,
|
|
645
|
+
): void {
|
|
646
|
+
if (
|
|
647
|
+
receipt.targetTaskId !== target.id ||
|
|
648
|
+
receipt.targetTaskGeneration !== target.taskGeneration ||
|
|
649
|
+
receipt.targetExecutionPlanId !== target.acceptedExecutionPlanId ||
|
|
650
|
+
receipt.verifierTaskId !== verifier.id ||
|
|
651
|
+
receipt.verifierTaskGeneration !== verifier.taskGeneration ||
|
|
652
|
+
receipt.verifierExecutionPlanId !== verifierExecutionPlanId ||
|
|
653
|
+
!target.stagedTreeIdentity ||
|
|
654
|
+
!sameWorkflowTreeIdentity(receipt.treeIdentity, target.stagedTreeIdentity)
|
|
655
|
+
) {
|
|
656
|
+
throw new Error("WorkItem verification receipt has stale or mismatched executor identity");
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function validatePlanId(value: string, label: string): void {
|
|
661
|
+
if (!/^[a-f0-9]{64}$/u.test(value)) throw new Error(`Invalid ${label}`);
|
|
662
|
+
}
|
|
663
|
+
|
|
449
664
|
function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
|
|
450
665
|
const states: WorkItemState[] = [
|
|
451
666
|
"pending",
|
|
452
667
|
"ready",
|
|
453
668
|
"running",
|
|
669
|
+
"awaiting-verification",
|
|
454
670
|
"blocked",
|
|
455
671
|
"needs-input",
|
|
456
672
|
"completed",
|
|
@@ -545,12 +761,41 @@ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): v
|
|
|
545
761
|
(typeof item.outcomeReason !== "string" ||
|
|
546
762
|
item.outcomeReason.length > MAX_TEXT_LENGTH ||
|
|
547
763
|
item.outcomeReason.trim() !== item.outcomeReason)) ||
|
|
548
|
-
typeof item.verificationAccepted !== "boolean"
|
|
764
|
+
typeof item.verificationAccepted !== "boolean" ||
|
|
765
|
+
(item.stagedTreeIdentity !== undefined && !isWorkflowTreeIdentity(item.stagedTreeIdentity)) ||
|
|
766
|
+
(item.verificationReceipt !== undefined &&
|
|
767
|
+
(!isWorkflowVerificationReceipt(item.verificationReceipt) ||
|
|
768
|
+
!storedVerificationMatchesItem(item, item.verificationReceipt))) ||
|
|
769
|
+
(item.state === "awaiting-verification" &&
|
|
770
|
+
(!item.stagedTreeIdentity || !item.acceptedExecutionPlanId)) ||
|
|
771
|
+
(item.stagedTreeIdentity !== undefined &&
|
|
772
|
+
item.state === "completed" &&
|
|
773
|
+
item.verificationReceipt === undefined)
|
|
549
774
|
) {
|
|
550
775
|
throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
|
|
551
776
|
}
|
|
552
777
|
}
|
|
553
778
|
|
|
779
|
+
function storedVerificationMatchesItem(
|
|
780
|
+
item: WorkItemRecord,
|
|
781
|
+
receipt: WorkflowVerificationReceipt,
|
|
782
|
+
): boolean {
|
|
783
|
+
return (
|
|
784
|
+
!item.verifierFor &&
|
|
785
|
+
receipt.targetTaskId === item.id &&
|
|
786
|
+
receipt.targetTaskGeneration === item.taskGeneration &&
|
|
787
|
+
receipt.targetExecutionPlanId === item.acceptedExecutionPlanId &&
|
|
788
|
+
item.stagedTreeIdentity !== undefined &&
|
|
789
|
+
sameWorkflowTreeIdentity(receipt.treeIdentity, item.stagedTreeIdentity) &&
|
|
790
|
+
item.verificationAccepted === (receipt.decision === "accept") &&
|
|
791
|
+
(receipt.decision === "accept"
|
|
792
|
+
? item.state === "completed"
|
|
793
|
+
: receipt.decision === "rework"
|
|
794
|
+
? item.state === "blocked"
|
|
795
|
+
: item.state === "failed")
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
|
|
554
799
|
function validStoredArtifacts(
|
|
555
800
|
values: WorkArtifactReference[],
|
|
556
801
|
producerTaskId: string,
|
|
@@ -599,11 +844,15 @@ function normalizeStoredArtifacts(
|
|
|
599
844
|
values: WorkArtifactReference[],
|
|
600
845
|
defaultProducerTaskId: string,
|
|
601
846
|
defaultGeneration: number,
|
|
847
|
+
preserveVerification: boolean,
|
|
602
848
|
): WorkArtifactReference[] {
|
|
603
849
|
if (!validStoredArtifacts(values, defaultProducerTaskId, defaultGeneration)) {
|
|
604
850
|
throw new Error(`Malformed stored artifacts for WorkItem ${defaultProducerTaskId}`);
|
|
605
851
|
}
|
|
606
|
-
return values.map((value) =>
|
|
852
|
+
return values.map((value) => ({
|
|
853
|
+
...structuredClone(value),
|
|
854
|
+
verified: preserveVerification && value.verified,
|
|
855
|
+
}));
|
|
607
856
|
}
|
|
608
857
|
|
|
609
858
|
function normalizeArtifacts(
|
|
@@ -627,7 +876,7 @@ function normalizeArtifacts(
|
|
|
627
876
|
...(value.digest ? { digest: bounded(value.digest, MAX_TEXT_LENGTH) } : {}),
|
|
628
877
|
producerTaskId,
|
|
629
878
|
generation,
|
|
630
|
-
verified:
|
|
879
|
+
verified: false,
|
|
631
880
|
};
|
|
632
881
|
});
|
|
633
882
|
}
|
|
@@ -203,6 +203,11 @@ function sanitizeWorkflowSnapshot(snapshot: WorkItemLedgerSnapshot): WorkItemLed
|
|
|
203
203
|
item.acceptanceCriteria = item.acceptanceCriteria.map(redact);
|
|
204
204
|
item.invalidationReasons = item.invalidationReasons.map(redact);
|
|
205
205
|
item.outcomeReason = item.outcomeReason ? redact(item.outcomeReason) : undefined;
|
|
206
|
+
if (item.verificationReceipt) {
|
|
207
|
+
item.verificationReceipt.summary = redact(item.verificationReceipt.summary);
|
|
208
|
+
item.verificationReceipt.evidence = item.verificationReceipt.evidence.map(redact);
|
|
209
|
+
item.verificationReceipt.limitations = item.verificationReceipt.limitations.map(redact);
|
|
210
|
+
}
|
|
206
211
|
for (const artifact of [...item.artifacts, ...item.artifactHistory]) {
|
|
207
212
|
artifact.kind = redact(artifact.kind);
|
|
208
213
|
artifact.version = redact(artifact.version);
|
package/src/workflow-planning.ts
CHANGED
|
@@ -89,6 +89,18 @@ export function createBlockingWorkLedger(
|
|
|
89
89
|
const hasExplicitIntegrationOwner = resolvedWorkflowTasks.some(
|
|
90
90
|
(task) => task.integrationOwner === true,
|
|
91
91
|
);
|
|
92
|
+
let defaultIntegrationOwnerIndex = -1;
|
|
93
|
+
if (!hasExplicitIntegrationOwner) {
|
|
94
|
+
for (let index = resolvedWorkflowTasks.length - 1; index >= 0; index--) {
|
|
95
|
+
if (resolvedWorkflowTasks[index]?.verifierFor === undefined) {
|
|
96
|
+
defaultIntegrationOwnerIndex = index;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (!hasExplicitIntegrationOwner && defaultIntegrationOwnerIndex < 0) {
|
|
102
|
+
throw new Error("Workflow has no non-verifier integration owner candidate");
|
|
103
|
+
}
|
|
92
104
|
return WorkItemLedger.create({
|
|
93
105
|
workflowId: params.workflow.id ?? "blocking-workflow",
|
|
94
106
|
items: resolvedWorkflowTasks.map((task, index) =>
|
|
@@ -96,7 +108,7 @@ export function createBlockingWorkLedger(
|
|
|
96
108
|
...task,
|
|
97
109
|
integrationOwner:
|
|
98
110
|
task.integrationOwner ??
|
|
99
|
-
(!hasExplicitIntegrationOwner && index ===
|
|
111
|
+
(!hasExplicitIntegrationOwner && index === defaultIntegrationOwnerIndex),
|
|
100
112
|
}),
|
|
101
113
|
),
|
|
102
114
|
});
|