@narumitw/pi-subagents 0.49.3 → 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.
Files changed (83) hide show
  1. package/README.md +362 -53
  2. package/package.json +10 -7
  3. package/src/adaptive-scheduler.ts +224 -0
  4. package/src/admission-benchmark.ts +95 -0
  5. package/src/admission-policy.ts +78 -0
  6. package/src/agent-projection.ts +53 -0
  7. package/src/agents.ts +58 -1
  8. package/src/auto-transport.ts +114 -0
  9. package/src/blocking-status.ts +63 -0
  10. package/src/capabilities.ts +145 -0
  11. package/src/capability-grant.ts +115 -0
  12. package/src/capability-router.ts +107 -0
  13. package/src/completion-delivery.ts +257 -0
  14. package/src/config-status.ts +221 -0
  15. package/src/config-ui.ts +215 -236
  16. package/src/consult-resources.ts +4 -27
  17. package/src/consult.ts +9 -1
  18. package/src/create-stateful-transport.ts +55 -0
  19. package/src/delegation-contract.ts +417 -0
  20. package/src/execution-plan.ts +322 -0
  21. package/src/execution-profiles.ts +95 -0
  22. package/src/execution-ui.ts +320 -0
  23. package/src/execution.ts +1098 -158
  24. package/src/in-process-transport.ts +269 -25
  25. package/src/inspect-render.ts +101 -1
  26. package/src/inspect.ts +321 -3
  27. package/src/integration-controller.ts +98 -0
  28. package/src/limits.ts +3 -0
  29. package/src/orchestration-metrics.ts +109 -0
  30. package/src/outcome.ts +61 -0
  31. package/src/panel-child-group.ts +35 -0
  32. package/src/panel-contract.ts +343 -0
  33. package/src/panel-evidence.ts +59 -0
  34. package/src/panel-execution.ts +770 -0
  35. package/src/panel-failure.ts +56 -0
  36. package/src/panel-planning.ts +175 -0
  37. package/src/panel-prompts.ts +132 -0
  38. package/src/panel-reconciliation.ts +57 -0
  39. package/src/panel-render.ts +103 -0
  40. package/src/parallel-limit-ui.ts +112 -0
  41. package/src/params.ts +179 -3
  42. package/src/persistence.ts +182 -32
  43. package/src/prompt-resources.ts +38 -0
  44. package/src/registry-types.ts +175 -0
  45. package/src/registry.ts +466 -143
  46. package/src/render.ts +72 -6
  47. package/src/result-contract.ts +416 -0
  48. package/src/retained-semantic-state.ts +100 -0
  49. package/src/rpc-timeout-finalization.ts +207 -0
  50. package/src/rpc-transport-metadata.ts +65 -0
  51. package/src/rpc-transport.ts +990 -0
  52. package/src/rpc-turn-capture.ts +142 -0
  53. package/src/runner-result.ts +55 -0
  54. package/src/runner-usage.ts +48 -0
  55. package/src/runner.ts +325 -73
  56. package/src/semantic-snapshot.ts +214 -0
  57. package/src/settings.ts +254 -35
  58. package/src/spawn-idempotency.ts +61 -0
  59. package/src/stateful-config.ts +13 -0
  60. package/src/stateful-guidance.ts +1 -0
  61. package/src/stateful-lifecycle.ts +45 -2
  62. package/src/stateful-limit-ui.ts +246 -0
  63. package/src/stateful-limits.ts +96 -0
  64. package/src/stateful-prompt.ts +11 -2
  65. package/src/stateful-render.ts +48 -3
  66. package/src/stateful.ts +467 -357
  67. package/src/subagents.ts +114 -46
  68. package/src/subprocess-transport.ts +64 -5
  69. package/src/supervision.ts +103 -0
  70. package/src/timeout-checkpoint.ts +305 -0
  71. package/src/timeout-finalization.ts +75 -0
  72. package/src/transport-types.ts +68 -0
  73. package/src/transport-ui.ts +169 -0
  74. package/src/transport.ts +16 -4
  75. package/src/turn-budget.ts +109 -0
  76. package/src/verification-policy.ts +67 -0
  77. package/src/work-item-ledger.ts +931 -0
  78. package/src/work-item-persistence.ts +223 -0
  79. package/src/workflow-planning.ts +162 -0
  80. package/src/workflow-tree-identity.ts +289 -0
  81. package/src/workflow-ui.ts +61 -0
  82. package/src/workflow-verification.ts +296 -0
  83. package/src/workspace.ts +69 -12
@@ -0,0 +1,931 @@
1
+ import {
2
+ type ManagedIntegrationCandidate,
3
+ type ManagedIntegrationExpectation,
4
+ verifyManagedIntegration,
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";
15
+
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;
18
+
19
+ export type WorkItemState =
20
+ | "pending"
21
+ | "ready"
22
+ | "running"
23
+ | "awaiting-verification"
24
+ | "blocked"
25
+ | "needs-input"
26
+ | "completed"
27
+ | "failed"
28
+ | "interrupted"
29
+ | "stale"
30
+ | "invalidated";
31
+
32
+ export interface WorkArtifactReference {
33
+ id: string;
34
+ kind: string;
35
+ version: string;
36
+ digest?: string;
37
+ producerTaskId?: string;
38
+ generation?: number;
39
+ verified?: boolean;
40
+ }
41
+
42
+ export interface WorkItemDefinition {
43
+ id: string;
44
+ objective: string;
45
+ dependencies: string[];
46
+ inputArtifacts?: string[];
47
+ inputArtifactVersions?: Record<string, string>;
48
+ requiredCapabilities?: string[];
49
+ requiredTools?: string[];
50
+ selectedAgentName?: string;
51
+ sideEffectPolicy?: "read-only" | "idempotent" | "mutating";
52
+ readPaths?: string[];
53
+ writePaths?: string[];
54
+ ownershipKeys?: string[];
55
+ acceptanceCriteria?: string[];
56
+ integrationOwner?: boolean;
57
+ verifierFor?: string;
58
+ dependencyPolicy?: "completed" | "settled";
59
+ }
60
+
61
+ export interface WorkItemRecord {
62
+ id: string;
63
+ objective: string;
64
+ dependencies: string[];
65
+ dependents: string[];
66
+ state: WorkItemState;
67
+ generation: number;
68
+ taskGeneration: number;
69
+ assignedAgentId?: string;
70
+ acceptedExecutionPlanId?: string;
71
+ inputArtifacts: string[];
72
+ inputArtifactVersions: Record<string, string>;
73
+ requiredArtifactVersions: Record<string, string>;
74
+ requiredCapabilities: string[];
75
+ requiredTools: string[];
76
+ selectedAgentName?: string;
77
+ sideEffectPolicy: "read-only" | "idempotent" | "mutating";
78
+ artifacts: WorkArtifactReference[];
79
+ artifactHistory: WorkArtifactReference[];
80
+ readPaths: string[];
81
+ writePaths: string[];
82
+ ownershipKeys: string[];
83
+ acceptanceCriteria: string[];
84
+ integrationOwner: boolean;
85
+ verifierFor?: string;
86
+ dependencyPolicy: "completed" | "settled";
87
+ verificationAccepted: boolean;
88
+ stagedTreeIdentity?: WorkflowTreeIdentity;
89
+ verificationReceipt?: WorkflowVerificationReceipt;
90
+ invalidationReasons: string[];
91
+ outcomeReason?: string;
92
+ }
93
+
94
+ export interface WorkItemLedgerSnapshot {
95
+ version: typeof WORK_ITEM_LEDGER_VERSION;
96
+ workflowId: string;
97
+ generation: number;
98
+ items: WorkItemRecord[];
99
+ }
100
+
101
+ export interface CreateWorkItemLedgerInput {
102
+ workflowId: string;
103
+ items: WorkItemDefinition[];
104
+ }
105
+
106
+ export interface CompleteWorkItemInput {
107
+ taskGeneration: number;
108
+ executionPlanId?: string;
109
+ artifacts?: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>;
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;
121
+ }
122
+
123
+ const MAX_ITEMS = 64;
124
+ const MAX_IDENTIFIER_LENGTH = 256;
125
+ const MAX_TEXT_LENGTH = 16 * 1024;
126
+ const MAX_LIST_ITEMS = 50;
127
+
128
+ const TERMINAL_STATES = new Set<WorkItemState>([
129
+ "completed",
130
+ "failed",
131
+ "interrupted",
132
+ "stale",
133
+ "invalidated",
134
+ ]);
135
+
136
+ export class WorkItemLedger {
137
+ private readonly items = new Map<string, WorkItemRecord>();
138
+ private generation = 0;
139
+
140
+ private constructor(readonly workflowId: string) {}
141
+
142
+ static create(input: CreateWorkItemLedgerInput): WorkItemLedger {
143
+ validateIdentifier(input.workflowId, "workflowId");
144
+ if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > MAX_ITEMS) {
145
+ throw new Error(`WorkItem workflow must contain 1-${MAX_ITEMS} items`);
146
+ }
147
+ const ledger = new WorkItemLedger(input.workflowId);
148
+ for (const definition of input.items) ledger.addDefinition(definition);
149
+ ledger.linkAndValidate();
150
+ ledger.refreshReadyState();
151
+ return ledger;
152
+ }
153
+
154
+ get(id: string): WorkItemRecord | undefined {
155
+ const item = this.items.get(id);
156
+ return item ? structuredClone(item) : undefined;
157
+ }
158
+
159
+ readyItems(): WorkItemRecord[] {
160
+ this.refreshReadyState();
161
+ return [...this.items.values()]
162
+ .filter((item) => item.state === "ready")
163
+ .sort((left, right) => left.id.localeCompare(right.id))
164
+ .map((item) => structuredClone(item));
165
+ }
166
+
167
+ start(id: string, agentId: string): WorkItemRecord {
168
+ const item = this.require(id);
169
+ if (item.state !== "ready") {
170
+ throw new Error(`WorkItem ${id} cannot start while ${item.state}`);
171
+ }
172
+ validateIdentifier(agentId, "agentId");
173
+ item.state = "running";
174
+ item.assignedAgentId = agentId;
175
+ item.generation = ++this.generation;
176
+ return structuredClone(item);
177
+ }
178
+
179
+ complete(id: string, input: CompleteWorkItemInput): WorkItemRecord {
180
+ const item = this.require(id);
181
+ this.assertMutable(item);
182
+ if (item.state !== "running") {
183
+ throw new Error(`WorkItem ${id} cannot complete while ${item.state}`);
184
+ }
185
+ if (input.taskGeneration !== item.taskGeneration) {
186
+ throw new Error(`WorkItem ${id} rejected a stale task generation`);
187
+ }
188
+ item.acceptedExecutionPlanId = input.executionPlanId?.slice(0, 256);
189
+ item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
190
+ item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
191
+ item.verificationAccepted = false;
192
+ item.stagedTreeIdentity = undefined;
193
+ item.verificationReceipt = undefined;
194
+ item.state = "completed";
195
+ item.generation = ++this.generation;
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`);
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;
221
+ this.refreshReadyState();
222
+ return structuredClone(item);
223
+ }
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
+
292
+ settle(
293
+ id: string,
294
+ state: "blocked" | "needs-input" | "failed" | "interrupted",
295
+ reason?: string,
296
+ ): WorkItemRecord {
297
+ const item = this.require(id);
298
+ this.assertMutable(item);
299
+ if (
300
+ item.state !== "running" &&
301
+ item.state !== "awaiting-verification" &&
302
+ item.state !== "ready" &&
303
+ item.state !== "pending"
304
+ ) {
305
+ throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
306
+ }
307
+ item.state = state;
308
+ item.outcomeReason = reason ? bounded(reason, MAX_TEXT_LENGTH) : undefined;
309
+ item.generation = ++this.generation;
310
+ this.refreshReadyState();
311
+ return structuredClone(item);
312
+ }
313
+
314
+ acceptIntegration(
315
+ id: string,
316
+ expected: ManagedIntegrationExpectation,
317
+ candidate: ManagedIntegrationCandidate,
318
+ ): WorkItemRecord {
319
+ const item = this.require(id);
320
+ if (!item.integrationOwner) {
321
+ throw new Error(`WorkItem ${id} is not the integration owner`);
322
+ }
323
+ if (item.state !== "running") {
324
+ throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
325
+ }
326
+ if (expected.taskId !== id || expected.taskGeneration !== item.taskGeneration) {
327
+ throw new Error(`WorkItem ${id} integration expectation has a stale generation`);
328
+ }
329
+ verifyManagedIntegration(expected, candidate);
330
+ item.verificationAccepted = true;
331
+ item.generation = ++this.generation;
332
+ return structuredClone(item);
333
+ }
334
+
335
+ rerun(id: string): WorkItemRecord {
336
+ const item = this.require(id);
337
+ if (
338
+ !["blocked", "needs-input", "failed", "interrupted", "stale", "invalidated"].includes(
339
+ item.state,
340
+ )
341
+ ) {
342
+ throw new Error(`WorkItem ${id} cannot rerun while ${item.state}`);
343
+ }
344
+ item.state = "pending";
345
+ item.taskGeneration++;
346
+ item.assignedAgentId = undefined;
347
+ item.acceptedExecutionPlanId = undefined;
348
+ item.outcomeReason = undefined;
349
+ item.verificationAccepted = false;
350
+ item.stagedTreeIdentity = undefined;
351
+ item.verificationReceipt = undefined;
352
+ item.generation = ++this.generation;
353
+ this.refreshReadyState();
354
+ return structuredClone(item);
355
+ }
356
+
357
+ invalidate(id: string, reason: string): WorkItemRecord[] {
358
+ const root = this.require(id);
359
+ const normalizedReason = bounded(reason, MAX_TEXT_LENGTH);
360
+ if (!normalizedReason) throw new Error("WorkItem invalidation requires a reason");
361
+ const affected: WorkItemRecord[] = [];
362
+ const queue = [root.id];
363
+ const seen = new Set<string>();
364
+ while (queue.length > 0) {
365
+ const currentId = queue.shift();
366
+ if (!currentId || seen.has(currentId)) continue;
367
+ seen.add(currentId);
368
+ const item = this.require(currentId);
369
+ item.state = currentId === root.id ? "stale" : "invalidated";
370
+ item.taskGeneration++;
371
+ item.invalidationReasons.push(`${root.id}:${normalizedReason}`);
372
+ item.generation = ++this.generation;
373
+ affected.push(structuredClone(item));
374
+ queue.push(...item.dependents);
375
+ }
376
+ return affected;
377
+ }
378
+
379
+ snapshot(): WorkItemLedgerSnapshot {
380
+ return {
381
+ version: WORK_ITEM_LEDGER_VERSION,
382
+ workflowId: this.workflowId,
383
+ generation: this.generation,
384
+ items: [...this.items.values()]
385
+ .sort((left, right) => left.id.localeCompare(right.id))
386
+ .map((item) => structuredClone(item)),
387
+ };
388
+ }
389
+
390
+ static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
391
+ if (
392
+ !snapshot ||
393
+ (snapshot.version !== WORK_ITEM_LEDGER_VERSION &&
394
+ (snapshot.version as string) !== LEGACY_WORK_ITEM_LEDGER_VERSION) ||
395
+ !Number.isSafeInteger(snapshot.generation) ||
396
+ snapshot.generation < 0
397
+ ) {
398
+ throw new Error("Unsupported or malformed WorkItem ledger snapshot");
399
+ }
400
+ const isLegacySnapshot = (snapshot.version as string) === LEGACY_WORK_ITEM_LEDGER_VERSION;
401
+ if (
402
+ !Array.isArray(snapshot.items) ||
403
+ snapshot.items.length < 1 ||
404
+ snapshot.items.length > MAX_ITEMS
405
+ ) {
406
+ throw new Error("Malformed WorkItem ledger items");
407
+ }
408
+ for (const item of snapshot.items) validateStoredRecord(item, snapshot.generation);
409
+ const ledger = WorkItemLedger.create({
410
+ workflowId: snapshot.workflowId,
411
+ items: snapshot.items.map((item) => ({
412
+ id: item.id,
413
+ objective: item.objective,
414
+ dependencies: item.dependencies,
415
+ inputArtifacts: item.inputArtifacts,
416
+ inputArtifactVersions: item.requiredArtifactVersions,
417
+ requiredCapabilities: item.requiredCapabilities,
418
+ requiredTools: item.requiredTools,
419
+ selectedAgentName: item.selectedAgentName,
420
+ sideEffectPolicy: item.sideEffectPolicy,
421
+ readPaths: item.readPaths,
422
+ writePaths: item.writePaths,
423
+ ownershipKeys: item.ownershipKeys,
424
+ acceptanceCriteria: item.acceptanceCriteria,
425
+ integrationOwner: item.integrationOwner,
426
+ verifierFor: item.verifierFor,
427
+ dependencyPolicy: item.dependencyPolicy,
428
+ })),
429
+ });
430
+ ledger.generation = snapshot.generation;
431
+ for (const stored of snapshot.items) {
432
+ const item = ledger.require(stored.id);
433
+ item.state =
434
+ stored.state === "running" || stored.state === "awaiting-verification"
435
+ ? "interrupted"
436
+ : stored.state;
437
+ item.generation = stored.generation;
438
+ item.taskGeneration = stored.taskGeneration ?? 1;
439
+ item.assignedAgentId = stored.assignedAgentId;
440
+ item.acceptedExecutionPlanId = stored.acceptedExecutionPlanId;
441
+ item.inputArtifactVersions = { ...stored.inputArtifactVersions };
442
+ item.artifacts = normalizeStoredArtifacts(
443
+ stored.artifacts,
444
+ stored.id,
445
+ stored.generation,
446
+ !isLegacySnapshot,
447
+ );
448
+ item.artifactHistory = normalizeStoredArtifacts(
449
+ stored.artifactHistory ?? [],
450
+ stored.id,
451
+ stored.generation,
452
+ !isLegacySnapshot,
453
+ );
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;
463
+ item.invalidationReasons = [...stored.invalidationReasons];
464
+ item.outcomeReason = stored.outcomeReason;
465
+ }
466
+ ledger.validateRestoredVerificationLinks();
467
+ return ledger;
468
+ }
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
+
487
+ private addDefinition(definition: WorkItemDefinition): void {
488
+ validateIdentifier(definition.id, "WorkItem id");
489
+ if (this.items.has(definition.id)) throw new Error(`Duplicate WorkItem id ${definition.id}`);
490
+ const objective = bounded(definition.objective, MAX_TEXT_LENGTH);
491
+ if (!objective) throw new Error(`WorkItem ${definition.id} requires an objective`);
492
+ this.items.set(definition.id, {
493
+ id: definition.id,
494
+ objective,
495
+ dependencies: uniqueBounded(definition.dependencies, "dependency"),
496
+ dependents: [],
497
+ state: "pending",
498
+ generation: 0,
499
+ taskGeneration: 1,
500
+ acceptedExecutionPlanId: undefined,
501
+ inputArtifacts: uniqueBounded(definition.inputArtifacts ?? [], "input artifact"),
502
+ inputArtifactVersions: {},
503
+ requiredArtifactVersions: normalizeVersionMap(definition.inputArtifactVersions),
504
+ requiredCapabilities: uniqueBounded(
505
+ definition.requiredCapabilities ?? [],
506
+ "required capability",
507
+ ),
508
+ requiredTools: uniqueBounded(definition.requiredTools ?? [], "required tool"),
509
+ selectedAgentName: definition.selectedAgentName,
510
+ sideEffectPolicy: definition.sideEffectPolicy ?? "mutating",
511
+ artifacts: [],
512
+ artifactHistory: [],
513
+ readPaths: uniqueBounded(definition.readPaths ?? [], "read path"),
514
+ writePaths: uniqueBounded(definition.writePaths ?? [], "write path"),
515
+ ownershipKeys: uniqueBounded(definition.ownershipKeys ?? [], "ownership key"),
516
+ acceptanceCriteria: uniqueBounded(
517
+ definition.acceptanceCriteria ?? [],
518
+ "acceptance criterion",
519
+ ),
520
+ integrationOwner: definition.integrationOwner === true,
521
+ verifierFor: definition.verifierFor,
522
+ dependencyPolicy: definition.dependencyPolicy ?? "completed",
523
+ verificationAccepted: false,
524
+ stagedTreeIdentity: undefined,
525
+ verificationReceipt: undefined,
526
+ invalidationReasons: [],
527
+ });
528
+ }
529
+
530
+ private linkAndValidate(): void {
531
+ const integrationOwners = [...this.items.values()].filter((item) => item.integrationOwner);
532
+ if (integrationOwners.length > 1)
533
+ throw new Error("Workflow can have only one integration owner");
534
+ for (const item of this.items.values()) {
535
+ for (const dependency of item.dependencies) {
536
+ if (dependency === item.id) throw new Error(`WorkItem ${item.id} has a self cycle`);
537
+ const parent = this.items.get(dependency);
538
+ if (!parent) throw new Error(`WorkItem ${item.id} has missing dependency ${dependency}`);
539
+ parent.dependents.push(item.id);
540
+ }
541
+ if (item.verifierFor && !this.items.has(item.verifierFor)) {
542
+ throw new Error(`WorkItem ${item.id} verifies missing WorkItem ${item.verifierFor}`);
543
+ }
544
+ if (item.verifierFor && !item.dependencies.includes(item.verifierFor)) {
545
+ throw new Error(
546
+ `WorkItem ${item.id} must depend on the WorkItem it verifies (${item.verifierFor})`,
547
+ );
548
+ }
549
+ }
550
+ const visiting = new Set<string>();
551
+ const visited = new Set<string>();
552
+ const visit = (id: string) => {
553
+ if (visiting.has(id)) throw new Error(`WorkItem dependency cycle includes ${id}`);
554
+ if (visited.has(id)) return;
555
+ visiting.add(id);
556
+ for (const dependency of this.require(id).dependencies) visit(dependency);
557
+ visiting.delete(id);
558
+ visited.add(id);
559
+ };
560
+ for (const id of this.items.keys()) visit(id);
561
+ }
562
+
563
+ private refreshReadyState(): void {
564
+ for (const item of this.items.values()) {
565
+ if (item.state !== "pending") continue;
566
+ const dependencies = item.dependencies.map((id) => this.require(id));
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"
574
+ ? dependencies.every(
575
+ (dependency) =>
576
+ !["pending", "ready", "running", "awaiting-verification"].includes(
577
+ dependency.state,
578
+ ),
579
+ )
580
+ : dependencies.every((dependency) => dependency.state === "completed");
581
+ if (!dependenciesReady) continue;
582
+ const available = new Map<string, WorkArtifactReference>();
583
+ for (const dependency of dependencies) {
584
+ for (const artifact of dependency.artifacts) available.set(artifact.id, artifact);
585
+ }
586
+ if (!item.inputArtifacts.every((artifact) => available.has(artifact))) continue;
587
+ if (
588
+ !Object.entries(item.requiredArtifactVersions).every(
589
+ ([id, version]) => available.get(id)?.version === version,
590
+ )
591
+ ) {
592
+ continue;
593
+ }
594
+ item.inputArtifactVersions = Object.fromEntries(
595
+ item.inputArtifacts.map((id) => [id, available.get(id)?.version ?? "unknown"]),
596
+ );
597
+ item.state = "ready";
598
+ item.generation = this.generation;
599
+ }
600
+ }
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
+
627
+ private require(id: string): WorkItemRecord {
628
+ const item = this.items.get(id);
629
+ if (!item) throw new Error(`Unknown WorkItem ${id}`);
630
+ return item;
631
+ }
632
+
633
+ private assertMutable(item: WorkItemRecord): void {
634
+ if (TERMINAL_STATES.has(item.state)) {
635
+ throw new Error(`WorkItem ${item.id} is terminal (${item.state})`);
636
+ }
637
+ }
638
+ }
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
+
664
+ function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
665
+ const states: WorkItemState[] = [
666
+ "pending",
667
+ "ready",
668
+ "running",
669
+ "awaiting-verification",
670
+ "blocked",
671
+ "needs-input",
672
+ "completed",
673
+ "failed",
674
+ "interrupted",
675
+ "stale",
676
+ "invalidated",
677
+ ];
678
+ if (
679
+ !item ||
680
+ typeof item !== "object" ||
681
+ typeof item.objective !== "string" ||
682
+ item.objective.length === 0 ||
683
+ item.objective.length > MAX_TEXT_LENGTH ||
684
+ item.objective.trim() !== item.objective ||
685
+ !states.includes(item.state) ||
686
+ !Number.isSafeInteger(item.generation) ||
687
+ item.generation < 0 ||
688
+ item.generation > ledgerGeneration ||
689
+ !Number.isSafeInteger(item.taskGeneration) ||
690
+ item.taskGeneration < 1 ||
691
+ !Array.isArray(item.artifacts) ||
692
+ !validStoredArtifacts(item.artifacts, item.id, item.generation) ||
693
+ (item.artifactHistory !== undefined && !Array.isArray(item.artifactHistory)) ||
694
+ (item.artifactHistory !== undefined &&
695
+ !validStoredArtifacts(item.artifactHistory, item.id, item.generation)) ||
696
+ !item.inputArtifactVersions ||
697
+ typeof item.inputArtifactVersions !== "object" ||
698
+ Array.isArray(item.inputArtifactVersions) ||
699
+ (item.assignedAgentId !== undefined && !isValidIdentifier(item.assignedAgentId)) ||
700
+ (item.selectedAgentName !== undefined &&
701
+ (typeof item.selectedAgentName !== "string" ||
702
+ item.selectedAgentName.length === 0 ||
703
+ item.selectedAgentName.length > MAX_IDENTIFIER_LENGTH ||
704
+ item.selectedAgentName.trim() !== item.selectedAgentName)) ||
705
+ typeof item.integrationOwner !== "boolean" ||
706
+ !(["completed", "settled"] as const).includes(item.dependencyPolicy) ||
707
+ !(["read-only", "idempotent", "mutating"] as const).includes(item.sideEffectPolicy) ||
708
+ (item.verifierFor !== undefined && !isValidIdentifier(item.verifierFor)) ||
709
+ ![
710
+ item.dependencies,
711
+ item.dependents,
712
+ item.inputArtifacts,
713
+ item.requiredCapabilities,
714
+ item.requiredTools,
715
+ item.readPaths,
716
+ item.writePaths,
717
+ item.ownershipKeys,
718
+ item.acceptanceCriteria,
719
+ ].every(
720
+ (values) =>
721
+ Array.isArray(values) &&
722
+ values.every(
723
+ (value) =>
724
+ typeof value === "string" &&
725
+ value.length > 0 &&
726
+ value.length <= MAX_TEXT_LENGTH &&
727
+ value.trim() === value,
728
+ ),
729
+ ) ||
730
+ !item.requiredArtifactVersions ||
731
+ typeof item.requiredArtifactVersions !== "object" ||
732
+ Array.isArray(item.requiredArtifactVersions) ||
733
+ !Object.entries(item.requiredArtifactVersions).every(
734
+ ([id, version]) =>
735
+ isValidIdentifier(id) &&
736
+ typeof version === "string" &&
737
+ version.length > 0 &&
738
+ version.length <= MAX_IDENTIFIER_LENGTH &&
739
+ version.trim() === version,
740
+ ) ||
741
+ (item.acceptedExecutionPlanId !== undefined &&
742
+ (typeof item.acceptedExecutionPlanId !== "string" ||
743
+ !/^[a-f0-9]{64}$/u.test(item.acceptedExecutionPlanId))) ||
744
+ !Object.entries(item.inputArtifactVersions).every(
745
+ ([id, value]) =>
746
+ isValidIdentifier(id) &&
747
+ typeof value === "string" &&
748
+ value.length > 0 &&
749
+ value.length <= MAX_IDENTIFIER_LENGTH &&
750
+ value.trim() === value,
751
+ ) ||
752
+ !Array.isArray(item.invalidationReasons) ||
753
+ !item.invalidationReasons.every(
754
+ (reason) =>
755
+ typeof reason === "string" &&
756
+ reason.length > 0 &&
757
+ reason.length <= MAX_TEXT_LENGTH &&
758
+ reason.trim() === reason,
759
+ ) ||
760
+ (item.outcomeReason !== undefined &&
761
+ (typeof item.outcomeReason !== "string" ||
762
+ item.outcomeReason.length > MAX_TEXT_LENGTH ||
763
+ item.outcomeReason.trim() !== item.outcomeReason)) ||
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)
774
+ ) {
775
+ throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
776
+ }
777
+ }
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
+
799
+ function validStoredArtifacts(
800
+ values: WorkArtifactReference[],
801
+ producerTaskId: string,
802
+ itemGeneration: number,
803
+ ): boolean {
804
+ const seen = new Set<string>();
805
+ return values.every(
806
+ (artifact) =>
807
+ artifact !== null &&
808
+ typeof artifact === "object" &&
809
+ Object.keys(artifact).every((key) =>
810
+ ["id", "kind", "version", "digest", "producerTaskId", "generation", "verified"].includes(
811
+ key,
812
+ ),
813
+ ) &&
814
+ isValidIdentifier(artifact.id) &&
815
+ claimUnique(artifact.id, seen) &&
816
+ typeof artifact.kind === "string" &&
817
+ artifact.kind.length > 0 &&
818
+ artifact.kind.length <= MAX_IDENTIFIER_LENGTH &&
819
+ artifact.kind.trim() === artifact.kind &&
820
+ typeof artifact.version === "string" &&
821
+ artifact.version.length > 0 &&
822
+ artifact.version.length <= MAX_IDENTIFIER_LENGTH &&
823
+ artifact.version.trim() === artifact.version &&
824
+ (artifact.digest === undefined ||
825
+ (typeof artifact.digest === "string" &&
826
+ artifact.digest.length > 0 &&
827
+ artifact.digest.length <= MAX_TEXT_LENGTH &&
828
+ artifact.digest.trim() === artifact.digest)) &&
829
+ artifact.producerTaskId === producerTaskId &&
830
+ Number.isSafeInteger(artifact.generation) &&
831
+ Number(artifact.generation) >= 0 &&
832
+ Number(artifact.generation) <= itemGeneration &&
833
+ typeof artifact.verified === "boolean",
834
+ );
835
+ }
836
+
837
+ function claimUnique(value: string, seen: Set<string>): boolean {
838
+ if (seen.has(value)) return false;
839
+ seen.add(value);
840
+ return true;
841
+ }
842
+
843
+ function normalizeStoredArtifacts(
844
+ values: WorkArtifactReference[],
845
+ defaultProducerTaskId: string,
846
+ defaultGeneration: number,
847
+ preserveVerification: boolean,
848
+ ): WorkArtifactReference[] {
849
+ if (!validStoredArtifacts(values, defaultProducerTaskId, defaultGeneration)) {
850
+ throw new Error(`Malformed stored artifacts for WorkItem ${defaultProducerTaskId}`);
851
+ }
852
+ return values.map((value) => ({
853
+ ...structuredClone(value),
854
+ verified: preserveVerification && value.verified,
855
+ }));
856
+ }
857
+
858
+ function normalizeArtifacts(
859
+ values: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>,
860
+ producerTaskId: string,
861
+ generation: number,
862
+ ): WorkArtifactReference[] {
863
+ if (values.length > MAX_LIST_ITEMS) throw new Error("Too many WorkItem artifacts");
864
+ const seen = new Set<string>();
865
+ return values.map((value) => {
866
+ validateIdentifier(value.id, "artifact id");
867
+ if (seen.has(value.id)) throw new Error(`Duplicate artifact id ${value.id}`);
868
+ seen.add(value.id);
869
+ const kind = bounded(value.kind, MAX_IDENTIFIER_LENGTH);
870
+ const version = bounded(value.version, MAX_IDENTIFIER_LENGTH);
871
+ if (!kind || !version) throw new Error(`Artifact ${value.id} requires kind and version`);
872
+ return {
873
+ id: value.id,
874
+ kind,
875
+ version,
876
+ ...(value.digest ? { digest: bounded(value.digest, MAX_TEXT_LENGTH) } : {}),
877
+ producerTaskId,
878
+ generation,
879
+ verified: false,
880
+ };
881
+ });
882
+ }
883
+
884
+ function normalizeVersionMap(value: Record<string, string> | undefined): Record<string, string> {
885
+ if (value === undefined) return {};
886
+ const entries = Object.entries(value);
887
+ if (entries.length > MAX_LIST_ITEMS) throw new Error("Too many artifact version requirements");
888
+ return Object.fromEntries(
889
+ entries.map(([id, version]) => {
890
+ validateIdentifier(id, "artifact version id");
891
+ const normalized = bounded(version, MAX_IDENTIFIER_LENGTH);
892
+ if (!normalized) throw new Error(`Artifact ${id} requires a version`);
893
+ return [id, normalized];
894
+ }),
895
+ );
896
+ }
897
+
898
+ function uniqueBounded(values: readonly string[], label: string): string[] {
899
+ if (!Array.isArray(values) || values.length > MAX_LIST_ITEMS) {
900
+ throw new Error(`Too many WorkItem ${label} values`);
901
+ }
902
+ const result: string[] = [];
903
+ const seen = new Set<string>();
904
+ for (const value of values) {
905
+ if (typeof value !== "string") throw new Error(`Invalid WorkItem ${label}`);
906
+ const normalized = bounded(value, MAX_TEXT_LENGTH);
907
+ if (!normalized) throw new Error(`Empty WorkItem ${label}`);
908
+ if (!seen.has(normalized)) result.push(normalized);
909
+ seen.add(normalized);
910
+ }
911
+ return result;
912
+ }
913
+
914
+ function isValidIdentifier(value: unknown): value is string {
915
+ return (
916
+ typeof value === "string" &&
917
+ value.length >= 1 &&
918
+ value.length <= MAX_IDENTIFIER_LENGTH &&
919
+ /^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value)
920
+ );
921
+ }
922
+
923
+ function validateIdentifier(value: string, label: string): void {
924
+ if (!isValidIdentifier(value)) {
925
+ throw new Error(`Invalid ${label}`);
926
+ }
927
+ }
928
+
929
+ function bounded(value: string, maxLength: number): string {
930
+ return value.trim().slice(0, maxLength);
931
+ }