@narumitw/pi-subagents 0.49.3 → 0.51.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 +313 -53
- package/package.json +10 -7
- package/src/adaptive-scheduler.ts +196 -0
- package/src/admission-benchmark.ts +95 -0
- package/src/admission-policy.ts +78 -0
- package/src/agent-projection.ts +53 -0
- package/src/agents.ts +58 -1
- package/src/auto-transport.ts +114 -0
- package/src/blocking-status.ts +63 -0
- package/src/capabilities.ts +145 -0
- package/src/capability-grant.ts +115 -0
- package/src/capability-router.ts +107 -0
- package/src/completion-delivery.ts +257 -0
- package/src/config-status.ts +221 -0
- package/src/config-ui.ts +215 -236
- package/src/consult-resources.ts +4 -27
- package/src/consult.ts +9 -1
- package/src/create-stateful-transport.ts +55 -0
- package/src/delegation-contract.ts +417 -0
- package/src/execution-plan.ts +322 -0
- package/src/execution-profiles.ts +95 -0
- package/src/execution-ui.ts +320 -0
- package/src/execution.ts +848 -158
- package/src/in-process-transport.ts +269 -25
- package/src/inspect-render.ts +101 -1
- package/src/inspect.ts +296 -3
- package/src/integration-controller.ts +98 -0
- package/src/limits.ts +3 -0
- package/src/orchestration-metrics.ts +78 -0
- package/src/outcome.ts +61 -0
- package/src/panel-child-group.ts +35 -0
- package/src/panel-contract.ts +343 -0
- package/src/panel-evidence.ts +59 -0
- package/src/panel-execution.ts +772 -0
- package/src/panel-failure.ts +56 -0
- package/src/panel-planning.ts +175 -0
- package/src/panel-prompts.ts +132 -0
- package/src/panel-reconciliation.ts +57 -0
- package/src/panel-render.ts +103 -0
- package/src/parallel-limit-ui.ts +112 -0
- package/src/params.ts +172 -3
- package/src/persistence.ts +182 -32
- package/src/prompt-resources.ts +38 -0
- package/src/registry-types.ts +175 -0
- package/src/registry.ts +466 -143
- package/src/render.ts +72 -6
- package/src/result-contract.ts +416 -0
- package/src/retained-semantic-state.ts +100 -0
- package/src/rpc-timeout-finalization.ts +207 -0
- package/src/rpc-transport-metadata.ts +65 -0
- package/src/rpc-transport.ts +990 -0
- package/src/rpc-turn-capture.ts +142 -0
- package/src/runner-result.ts +55 -0
- package/src/runner-usage.ts +48 -0
- package/src/runner.ts +325 -73
- package/src/semantic-snapshot.ts +214 -0
- package/src/settings.ts +254 -35
- package/src/spawn-idempotency.ts +61 -0
- package/src/stateful-config.ts +13 -0
- package/src/stateful-guidance.ts +1 -0
- package/src/stateful-lifecycle.ts +45 -2
- package/src/stateful-limit-ui.ts +246 -0
- package/src/stateful-limits.ts +96 -0
- package/src/stateful-prompt.ts +11 -2
- package/src/stateful-render.ts +48 -3
- package/src/stateful.ts +467 -357
- package/src/subagents.ts +114 -46
- package/src/subprocess-transport.ts +64 -5
- package/src/supervision.ts +103 -0
- package/src/timeout-checkpoint.ts +305 -0
- package/src/timeout-finalization.ts +75 -0
- package/src/transport-types.ts +68 -0
- package/src/transport-ui.ts +169 -0
- package/src/transport.ts +16 -4
- package/src/turn-budget.ts +109 -0
- package/src/verification-policy.ts +17 -0
- package/src/work-item-ledger.ts +682 -0
- package/src/work-item-persistence.ts +218 -0
- package/src/workflow-planning.ts +150 -0
- package/src/workflow-ui.ts +61 -0
- package/src/workspace.ts +69 -12
|
@@ -0,0 +1,682 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ManagedIntegrationCandidate,
|
|
3
|
+
type ManagedIntegrationExpectation,
|
|
4
|
+
verifyManagedIntegration,
|
|
5
|
+
} from "./integration-controller.js";
|
|
6
|
+
|
|
7
|
+
export const WORK_ITEM_LEDGER_VERSION = "pi-subagents:work-ledger:v1" as const;
|
|
8
|
+
|
|
9
|
+
export type WorkItemState =
|
|
10
|
+
| "pending"
|
|
11
|
+
| "ready"
|
|
12
|
+
| "running"
|
|
13
|
+
| "blocked"
|
|
14
|
+
| "needs-input"
|
|
15
|
+
| "completed"
|
|
16
|
+
| "failed"
|
|
17
|
+
| "interrupted"
|
|
18
|
+
| "stale"
|
|
19
|
+
| "invalidated";
|
|
20
|
+
|
|
21
|
+
export interface WorkArtifactReference {
|
|
22
|
+
id: string;
|
|
23
|
+
kind: string;
|
|
24
|
+
version: string;
|
|
25
|
+
digest?: string;
|
|
26
|
+
producerTaskId?: string;
|
|
27
|
+
generation?: number;
|
|
28
|
+
verified?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface WorkItemDefinition {
|
|
32
|
+
id: string;
|
|
33
|
+
objective: string;
|
|
34
|
+
dependencies: string[];
|
|
35
|
+
inputArtifacts?: string[];
|
|
36
|
+
inputArtifactVersions?: Record<string, string>;
|
|
37
|
+
requiredCapabilities?: string[];
|
|
38
|
+
requiredTools?: string[];
|
|
39
|
+
selectedAgentName?: string;
|
|
40
|
+
sideEffectPolicy?: "read-only" | "idempotent" | "mutating";
|
|
41
|
+
readPaths?: string[];
|
|
42
|
+
writePaths?: string[];
|
|
43
|
+
ownershipKeys?: string[];
|
|
44
|
+
acceptanceCriteria?: string[];
|
|
45
|
+
integrationOwner?: boolean;
|
|
46
|
+
verifierFor?: string;
|
|
47
|
+
dependencyPolicy?: "completed" | "settled";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface WorkItemRecord {
|
|
51
|
+
id: string;
|
|
52
|
+
objective: string;
|
|
53
|
+
dependencies: string[];
|
|
54
|
+
dependents: string[];
|
|
55
|
+
state: WorkItemState;
|
|
56
|
+
generation: number;
|
|
57
|
+
taskGeneration: number;
|
|
58
|
+
assignedAgentId?: string;
|
|
59
|
+
acceptedExecutionPlanId?: string;
|
|
60
|
+
inputArtifacts: string[];
|
|
61
|
+
inputArtifactVersions: Record<string, string>;
|
|
62
|
+
requiredArtifactVersions: Record<string, string>;
|
|
63
|
+
requiredCapabilities: string[];
|
|
64
|
+
requiredTools: string[];
|
|
65
|
+
selectedAgentName?: string;
|
|
66
|
+
sideEffectPolicy: "read-only" | "idempotent" | "mutating";
|
|
67
|
+
artifacts: WorkArtifactReference[];
|
|
68
|
+
artifactHistory: WorkArtifactReference[];
|
|
69
|
+
readPaths: string[];
|
|
70
|
+
writePaths: string[];
|
|
71
|
+
ownershipKeys: string[];
|
|
72
|
+
acceptanceCriteria: string[];
|
|
73
|
+
integrationOwner: boolean;
|
|
74
|
+
verifierFor?: string;
|
|
75
|
+
dependencyPolicy: "completed" | "settled";
|
|
76
|
+
verificationAccepted: boolean;
|
|
77
|
+
invalidationReasons: string[];
|
|
78
|
+
outcomeReason?: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface WorkItemLedgerSnapshot {
|
|
82
|
+
version: typeof WORK_ITEM_LEDGER_VERSION;
|
|
83
|
+
workflowId: string;
|
|
84
|
+
generation: number;
|
|
85
|
+
items: WorkItemRecord[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface CreateWorkItemLedgerInput {
|
|
89
|
+
workflowId: string;
|
|
90
|
+
items: WorkItemDefinition[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface CompleteWorkItemInput {
|
|
94
|
+
taskGeneration: number;
|
|
95
|
+
executionPlanId?: string;
|
|
96
|
+
artifacts?: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>;
|
|
97
|
+
verificationAccepted?: boolean;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const MAX_ITEMS = 64;
|
|
101
|
+
const MAX_IDENTIFIER_LENGTH = 256;
|
|
102
|
+
const MAX_TEXT_LENGTH = 16 * 1024;
|
|
103
|
+
const MAX_LIST_ITEMS = 50;
|
|
104
|
+
|
|
105
|
+
const TERMINAL_STATES = new Set<WorkItemState>([
|
|
106
|
+
"completed",
|
|
107
|
+
"failed",
|
|
108
|
+
"interrupted",
|
|
109
|
+
"stale",
|
|
110
|
+
"invalidated",
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
export class WorkItemLedger {
|
|
114
|
+
private readonly items = new Map<string, WorkItemRecord>();
|
|
115
|
+
private generation = 0;
|
|
116
|
+
|
|
117
|
+
private constructor(readonly workflowId: string) {}
|
|
118
|
+
|
|
119
|
+
static create(input: CreateWorkItemLedgerInput): WorkItemLedger {
|
|
120
|
+
validateIdentifier(input.workflowId, "workflowId");
|
|
121
|
+
if (!Array.isArray(input.items) || input.items.length < 1 || input.items.length > MAX_ITEMS) {
|
|
122
|
+
throw new Error(`WorkItem workflow must contain 1-${MAX_ITEMS} items`);
|
|
123
|
+
}
|
|
124
|
+
const ledger = new WorkItemLedger(input.workflowId);
|
|
125
|
+
for (const definition of input.items) ledger.addDefinition(definition);
|
|
126
|
+
ledger.linkAndValidate();
|
|
127
|
+
ledger.refreshReadyState();
|
|
128
|
+
return ledger;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
get(id: string): WorkItemRecord | undefined {
|
|
132
|
+
const item = this.items.get(id);
|
|
133
|
+
return item ? structuredClone(item) : undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
readyItems(): WorkItemRecord[] {
|
|
137
|
+
this.refreshReadyState();
|
|
138
|
+
return [...this.items.values()]
|
|
139
|
+
.filter((item) => item.state === "ready")
|
|
140
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
141
|
+
.map((item) => structuredClone(item));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
start(id: string, agentId: string): WorkItemRecord {
|
|
145
|
+
const item = this.require(id);
|
|
146
|
+
if (item.state !== "ready") {
|
|
147
|
+
throw new Error(`WorkItem ${id} cannot start while ${item.state}`);
|
|
148
|
+
}
|
|
149
|
+
validateIdentifier(agentId, "agentId");
|
|
150
|
+
item.state = "running";
|
|
151
|
+
item.assignedAgentId = agentId;
|
|
152
|
+
item.generation = ++this.generation;
|
|
153
|
+
return structuredClone(item);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
complete(id: string, input: CompleteWorkItemInput): WorkItemRecord {
|
|
157
|
+
const item = this.require(id);
|
|
158
|
+
this.assertMutable(item);
|
|
159
|
+
if (item.state !== "running") {
|
|
160
|
+
throw new Error(`WorkItem ${id} cannot complete while ${item.state}`);
|
|
161
|
+
}
|
|
162
|
+
if (input.taskGeneration !== item.taskGeneration) {
|
|
163
|
+
throw new Error(`WorkItem ${id} rejected a stale task generation`);
|
|
164
|
+
}
|
|
165
|
+
item.acceptedExecutionPlanId = input.executionPlanId?.slice(0, 256);
|
|
166
|
+
item.artifactHistory.push(...item.artifacts.map((artifact) => structuredClone(artifact)));
|
|
167
|
+
item.artifacts = normalizeArtifacts(input.artifacts ?? [], id, this.generation + 1);
|
|
168
|
+
item.verificationAccepted = input.verificationAccepted === true;
|
|
169
|
+
item.state = "completed";
|
|
170
|
+
item.generation = ++this.generation;
|
|
171
|
+
if (item.verifierFor && item.verificationAccepted) {
|
|
172
|
+
const verified = this.require(item.verifierFor);
|
|
173
|
+
verified.verificationAccepted = true;
|
|
174
|
+
verified.generation = this.generation;
|
|
175
|
+
}
|
|
176
|
+
this.refreshReadyState();
|
|
177
|
+
return structuredClone(item);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
settle(
|
|
181
|
+
id: string,
|
|
182
|
+
state: "blocked" | "needs-input" | "failed" | "interrupted",
|
|
183
|
+
reason?: string,
|
|
184
|
+
): WorkItemRecord {
|
|
185
|
+
const item = this.require(id);
|
|
186
|
+
this.assertMutable(item);
|
|
187
|
+
if (item.state !== "running" && item.state !== "ready" && item.state !== "pending") {
|
|
188
|
+
throw new Error(`WorkItem ${id} cannot settle while ${item.state}`);
|
|
189
|
+
}
|
|
190
|
+
item.state = state;
|
|
191
|
+
item.outcomeReason = reason ? bounded(reason, MAX_TEXT_LENGTH) : undefined;
|
|
192
|
+
item.generation = ++this.generation;
|
|
193
|
+
this.refreshReadyState();
|
|
194
|
+
return structuredClone(item);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
acceptIntegration(
|
|
198
|
+
id: string,
|
|
199
|
+
expected: ManagedIntegrationExpectation,
|
|
200
|
+
candidate: ManagedIntegrationCandidate,
|
|
201
|
+
): WorkItemRecord {
|
|
202
|
+
const item = this.require(id);
|
|
203
|
+
if (!item.integrationOwner) {
|
|
204
|
+
throw new Error(`WorkItem ${id} is not the integration owner`);
|
|
205
|
+
}
|
|
206
|
+
if (item.state !== "running") {
|
|
207
|
+
throw new Error(`WorkItem ${id} cannot integrate while ${item.state}`);
|
|
208
|
+
}
|
|
209
|
+
if (expected.taskId !== id || expected.taskGeneration !== item.taskGeneration) {
|
|
210
|
+
throw new Error(`WorkItem ${id} integration expectation has a stale generation`);
|
|
211
|
+
}
|
|
212
|
+
verifyManagedIntegration(expected, candidate);
|
|
213
|
+
item.verificationAccepted = true;
|
|
214
|
+
item.generation = ++this.generation;
|
|
215
|
+
return structuredClone(item);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
rerun(id: string): WorkItemRecord {
|
|
219
|
+
const item = this.require(id);
|
|
220
|
+
if (
|
|
221
|
+
!["blocked", "needs-input", "failed", "interrupted", "stale", "invalidated"].includes(
|
|
222
|
+
item.state,
|
|
223
|
+
)
|
|
224
|
+
) {
|
|
225
|
+
throw new Error(`WorkItem ${id} cannot rerun while ${item.state}`);
|
|
226
|
+
}
|
|
227
|
+
item.state = "pending";
|
|
228
|
+
item.taskGeneration++;
|
|
229
|
+
item.assignedAgentId = undefined;
|
|
230
|
+
item.acceptedExecutionPlanId = undefined;
|
|
231
|
+
item.outcomeReason = undefined;
|
|
232
|
+
item.verificationAccepted = false;
|
|
233
|
+
item.generation = ++this.generation;
|
|
234
|
+
this.refreshReadyState();
|
|
235
|
+
return structuredClone(item);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
invalidate(id: string, reason: string): WorkItemRecord[] {
|
|
239
|
+
const root = this.require(id);
|
|
240
|
+
const normalizedReason = bounded(reason, MAX_TEXT_LENGTH);
|
|
241
|
+
if (!normalizedReason) throw new Error("WorkItem invalidation requires a reason");
|
|
242
|
+
const affected: WorkItemRecord[] = [];
|
|
243
|
+
const queue = [root.id];
|
|
244
|
+
const seen = new Set<string>();
|
|
245
|
+
while (queue.length > 0) {
|
|
246
|
+
const currentId = queue.shift();
|
|
247
|
+
if (!currentId || seen.has(currentId)) continue;
|
|
248
|
+
seen.add(currentId);
|
|
249
|
+
const item = this.require(currentId);
|
|
250
|
+
item.state = currentId === root.id ? "stale" : "invalidated";
|
|
251
|
+
item.taskGeneration++;
|
|
252
|
+
item.invalidationReasons.push(`${root.id}:${normalizedReason}`);
|
|
253
|
+
item.generation = ++this.generation;
|
|
254
|
+
affected.push(structuredClone(item));
|
|
255
|
+
queue.push(...item.dependents);
|
|
256
|
+
}
|
|
257
|
+
return affected;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
snapshot(): WorkItemLedgerSnapshot {
|
|
261
|
+
return {
|
|
262
|
+
version: WORK_ITEM_LEDGER_VERSION,
|
|
263
|
+
workflowId: this.workflowId,
|
|
264
|
+
generation: this.generation,
|
|
265
|
+
items: [...this.items.values()]
|
|
266
|
+
.sort((left, right) => left.id.localeCompare(right.id))
|
|
267
|
+
.map((item) => structuredClone(item)),
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
static restore(snapshot: WorkItemLedgerSnapshot): WorkItemLedger {
|
|
272
|
+
if (
|
|
273
|
+
!snapshot ||
|
|
274
|
+
snapshot.version !== WORK_ITEM_LEDGER_VERSION ||
|
|
275
|
+
!Number.isSafeInteger(snapshot.generation) ||
|
|
276
|
+
snapshot.generation < 0
|
|
277
|
+
) {
|
|
278
|
+
throw new Error("Unsupported or malformed WorkItem ledger snapshot");
|
|
279
|
+
}
|
|
280
|
+
if (
|
|
281
|
+
!Array.isArray(snapshot.items) ||
|
|
282
|
+
snapshot.items.length < 1 ||
|
|
283
|
+
snapshot.items.length > MAX_ITEMS
|
|
284
|
+
) {
|
|
285
|
+
throw new Error("Malformed WorkItem ledger items");
|
|
286
|
+
}
|
|
287
|
+
for (const item of snapshot.items) validateStoredRecord(item, snapshot.generation);
|
|
288
|
+
const ledger = WorkItemLedger.create({
|
|
289
|
+
workflowId: snapshot.workflowId,
|
|
290
|
+
items: snapshot.items.map((item) => ({
|
|
291
|
+
id: item.id,
|
|
292
|
+
objective: item.objective,
|
|
293
|
+
dependencies: item.dependencies,
|
|
294
|
+
inputArtifacts: item.inputArtifacts,
|
|
295
|
+
inputArtifactVersions: item.requiredArtifactVersions,
|
|
296
|
+
requiredCapabilities: item.requiredCapabilities,
|
|
297
|
+
requiredTools: item.requiredTools,
|
|
298
|
+
selectedAgentName: item.selectedAgentName,
|
|
299
|
+
sideEffectPolicy: item.sideEffectPolicy,
|
|
300
|
+
readPaths: item.readPaths,
|
|
301
|
+
writePaths: item.writePaths,
|
|
302
|
+
ownershipKeys: item.ownershipKeys,
|
|
303
|
+
acceptanceCriteria: item.acceptanceCriteria,
|
|
304
|
+
integrationOwner: item.integrationOwner,
|
|
305
|
+
verifierFor: item.verifierFor,
|
|
306
|
+
dependencyPolicy: item.dependencyPolicy,
|
|
307
|
+
})),
|
|
308
|
+
});
|
|
309
|
+
ledger.generation = snapshot.generation;
|
|
310
|
+
for (const stored of snapshot.items) {
|
|
311
|
+
const item = ledger.require(stored.id);
|
|
312
|
+
item.state = stored.state === "running" ? "interrupted" : stored.state;
|
|
313
|
+
item.generation = stored.generation;
|
|
314
|
+
item.taskGeneration = stored.taskGeneration ?? 1;
|
|
315
|
+
item.assignedAgentId = stored.assignedAgentId;
|
|
316
|
+
item.acceptedExecutionPlanId = stored.acceptedExecutionPlanId;
|
|
317
|
+
item.inputArtifactVersions = { ...stored.inputArtifactVersions };
|
|
318
|
+
item.artifacts = normalizeStoredArtifacts(stored.artifacts, stored.id, stored.generation);
|
|
319
|
+
item.artifactHistory = normalizeStoredArtifacts(
|
|
320
|
+
stored.artifactHistory ?? [],
|
|
321
|
+
stored.id,
|
|
322
|
+
stored.generation,
|
|
323
|
+
);
|
|
324
|
+
item.verificationAccepted = stored.verificationAccepted;
|
|
325
|
+
item.invalidationReasons = [...stored.invalidationReasons];
|
|
326
|
+
item.outcomeReason = stored.outcomeReason;
|
|
327
|
+
}
|
|
328
|
+
return ledger;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
private addDefinition(definition: WorkItemDefinition): void {
|
|
332
|
+
validateIdentifier(definition.id, "WorkItem id");
|
|
333
|
+
if (this.items.has(definition.id)) throw new Error(`Duplicate WorkItem id ${definition.id}`);
|
|
334
|
+
const objective = bounded(definition.objective, MAX_TEXT_LENGTH);
|
|
335
|
+
if (!objective) throw new Error(`WorkItem ${definition.id} requires an objective`);
|
|
336
|
+
this.items.set(definition.id, {
|
|
337
|
+
id: definition.id,
|
|
338
|
+
objective,
|
|
339
|
+
dependencies: uniqueBounded(definition.dependencies, "dependency"),
|
|
340
|
+
dependents: [],
|
|
341
|
+
state: "pending",
|
|
342
|
+
generation: 0,
|
|
343
|
+
taskGeneration: 1,
|
|
344
|
+
acceptedExecutionPlanId: undefined,
|
|
345
|
+
inputArtifacts: uniqueBounded(definition.inputArtifacts ?? [], "input artifact"),
|
|
346
|
+
inputArtifactVersions: {},
|
|
347
|
+
requiredArtifactVersions: normalizeVersionMap(definition.inputArtifactVersions),
|
|
348
|
+
requiredCapabilities: uniqueBounded(
|
|
349
|
+
definition.requiredCapabilities ?? [],
|
|
350
|
+
"required capability",
|
|
351
|
+
),
|
|
352
|
+
requiredTools: uniqueBounded(definition.requiredTools ?? [], "required tool"),
|
|
353
|
+
selectedAgentName: definition.selectedAgentName,
|
|
354
|
+
sideEffectPolicy: definition.sideEffectPolicy ?? "mutating",
|
|
355
|
+
artifacts: [],
|
|
356
|
+
artifactHistory: [],
|
|
357
|
+
readPaths: uniqueBounded(definition.readPaths ?? [], "read path"),
|
|
358
|
+
writePaths: uniqueBounded(definition.writePaths ?? [], "write path"),
|
|
359
|
+
ownershipKeys: uniqueBounded(definition.ownershipKeys ?? [], "ownership key"),
|
|
360
|
+
acceptanceCriteria: uniqueBounded(
|
|
361
|
+
definition.acceptanceCriteria ?? [],
|
|
362
|
+
"acceptance criterion",
|
|
363
|
+
),
|
|
364
|
+
integrationOwner: definition.integrationOwner === true,
|
|
365
|
+
verifierFor: definition.verifierFor,
|
|
366
|
+
dependencyPolicy: definition.dependencyPolicy ?? "completed",
|
|
367
|
+
verificationAccepted: false,
|
|
368
|
+
invalidationReasons: [],
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private linkAndValidate(): void {
|
|
373
|
+
const integrationOwners = [...this.items.values()].filter((item) => item.integrationOwner);
|
|
374
|
+
if (integrationOwners.length > 1)
|
|
375
|
+
throw new Error("Workflow can have only one integration owner");
|
|
376
|
+
for (const item of this.items.values()) {
|
|
377
|
+
for (const dependency of item.dependencies) {
|
|
378
|
+
if (dependency === item.id) throw new Error(`WorkItem ${item.id} has a self cycle`);
|
|
379
|
+
const parent = this.items.get(dependency);
|
|
380
|
+
if (!parent) throw new Error(`WorkItem ${item.id} has missing dependency ${dependency}`);
|
|
381
|
+
parent.dependents.push(item.id);
|
|
382
|
+
}
|
|
383
|
+
if (item.verifierFor && !this.items.has(item.verifierFor)) {
|
|
384
|
+
throw new Error(`WorkItem ${item.id} verifies missing WorkItem ${item.verifierFor}`);
|
|
385
|
+
}
|
|
386
|
+
if (item.verifierFor && !item.dependencies.includes(item.verifierFor)) {
|
|
387
|
+
throw new Error(
|
|
388
|
+
`WorkItem ${item.id} must depend on the WorkItem it verifies (${item.verifierFor})`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
const visiting = new Set<string>();
|
|
393
|
+
const visited = new Set<string>();
|
|
394
|
+
const visit = (id: string) => {
|
|
395
|
+
if (visiting.has(id)) throw new Error(`WorkItem dependency cycle includes ${id}`);
|
|
396
|
+
if (visited.has(id)) return;
|
|
397
|
+
visiting.add(id);
|
|
398
|
+
for (const dependency of this.require(id).dependencies) visit(dependency);
|
|
399
|
+
visiting.delete(id);
|
|
400
|
+
visited.add(id);
|
|
401
|
+
};
|
|
402
|
+
for (const id of this.items.keys()) visit(id);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
private refreshReadyState(): void {
|
|
406
|
+
for (const item of this.items.values()) {
|
|
407
|
+
if (item.state !== "pending") continue;
|
|
408
|
+
const dependencies = item.dependencies.map((id) => this.require(id));
|
|
409
|
+
const dependenciesReady =
|
|
410
|
+
item.dependencyPolicy === "settled"
|
|
411
|
+
? dependencies.every(
|
|
412
|
+
(dependency) => !["pending", "ready", "running"].includes(dependency.state),
|
|
413
|
+
)
|
|
414
|
+
: dependencies.every((dependency) => dependency.state === "completed");
|
|
415
|
+
if (!dependenciesReady) continue;
|
|
416
|
+
const available = new Map<string, WorkArtifactReference>();
|
|
417
|
+
for (const dependency of dependencies) {
|
|
418
|
+
for (const artifact of dependency.artifacts) available.set(artifact.id, artifact);
|
|
419
|
+
}
|
|
420
|
+
if (!item.inputArtifacts.every((artifact) => available.has(artifact))) continue;
|
|
421
|
+
if (
|
|
422
|
+
!Object.entries(item.requiredArtifactVersions).every(
|
|
423
|
+
([id, version]) => available.get(id)?.version === version,
|
|
424
|
+
)
|
|
425
|
+
) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
item.inputArtifactVersions = Object.fromEntries(
|
|
429
|
+
item.inputArtifacts.map((id) => [id, available.get(id)?.version ?? "unknown"]),
|
|
430
|
+
);
|
|
431
|
+
item.state = "ready";
|
|
432
|
+
item.generation = this.generation;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
private require(id: string): WorkItemRecord {
|
|
437
|
+
const item = this.items.get(id);
|
|
438
|
+
if (!item) throw new Error(`Unknown WorkItem ${id}`);
|
|
439
|
+
return item;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
private assertMutable(item: WorkItemRecord): void {
|
|
443
|
+
if (TERMINAL_STATES.has(item.state)) {
|
|
444
|
+
throw new Error(`WorkItem ${item.id} is terminal (${item.state})`);
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function validateStoredRecord(item: WorkItemRecord, ledgerGeneration: number): void {
|
|
450
|
+
const states: WorkItemState[] = [
|
|
451
|
+
"pending",
|
|
452
|
+
"ready",
|
|
453
|
+
"running",
|
|
454
|
+
"blocked",
|
|
455
|
+
"needs-input",
|
|
456
|
+
"completed",
|
|
457
|
+
"failed",
|
|
458
|
+
"interrupted",
|
|
459
|
+
"stale",
|
|
460
|
+
"invalidated",
|
|
461
|
+
];
|
|
462
|
+
if (
|
|
463
|
+
!item ||
|
|
464
|
+
typeof item !== "object" ||
|
|
465
|
+
typeof item.objective !== "string" ||
|
|
466
|
+
item.objective.length === 0 ||
|
|
467
|
+
item.objective.length > MAX_TEXT_LENGTH ||
|
|
468
|
+
item.objective.trim() !== item.objective ||
|
|
469
|
+
!states.includes(item.state) ||
|
|
470
|
+
!Number.isSafeInteger(item.generation) ||
|
|
471
|
+
item.generation < 0 ||
|
|
472
|
+
item.generation > ledgerGeneration ||
|
|
473
|
+
!Number.isSafeInteger(item.taskGeneration) ||
|
|
474
|
+
item.taskGeneration < 1 ||
|
|
475
|
+
!Array.isArray(item.artifacts) ||
|
|
476
|
+
!validStoredArtifacts(item.artifacts, item.id, item.generation) ||
|
|
477
|
+
(item.artifactHistory !== undefined && !Array.isArray(item.artifactHistory)) ||
|
|
478
|
+
(item.artifactHistory !== undefined &&
|
|
479
|
+
!validStoredArtifacts(item.artifactHistory, item.id, item.generation)) ||
|
|
480
|
+
!item.inputArtifactVersions ||
|
|
481
|
+
typeof item.inputArtifactVersions !== "object" ||
|
|
482
|
+
Array.isArray(item.inputArtifactVersions) ||
|
|
483
|
+
(item.assignedAgentId !== undefined && !isValidIdentifier(item.assignedAgentId)) ||
|
|
484
|
+
(item.selectedAgentName !== undefined &&
|
|
485
|
+
(typeof item.selectedAgentName !== "string" ||
|
|
486
|
+
item.selectedAgentName.length === 0 ||
|
|
487
|
+
item.selectedAgentName.length > MAX_IDENTIFIER_LENGTH ||
|
|
488
|
+
item.selectedAgentName.trim() !== item.selectedAgentName)) ||
|
|
489
|
+
typeof item.integrationOwner !== "boolean" ||
|
|
490
|
+
!(["completed", "settled"] as const).includes(item.dependencyPolicy) ||
|
|
491
|
+
!(["read-only", "idempotent", "mutating"] as const).includes(item.sideEffectPolicy) ||
|
|
492
|
+
(item.verifierFor !== undefined && !isValidIdentifier(item.verifierFor)) ||
|
|
493
|
+
![
|
|
494
|
+
item.dependencies,
|
|
495
|
+
item.dependents,
|
|
496
|
+
item.inputArtifacts,
|
|
497
|
+
item.requiredCapabilities,
|
|
498
|
+
item.requiredTools,
|
|
499
|
+
item.readPaths,
|
|
500
|
+
item.writePaths,
|
|
501
|
+
item.ownershipKeys,
|
|
502
|
+
item.acceptanceCriteria,
|
|
503
|
+
].every(
|
|
504
|
+
(values) =>
|
|
505
|
+
Array.isArray(values) &&
|
|
506
|
+
values.every(
|
|
507
|
+
(value) =>
|
|
508
|
+
typeof value === "string" &&
|
|
509
|
+
value.length > 0 &&
|
|
510
|
+
value.length <= MAX_TEXT_LENGTH &&
|
|
511
|
+
value.trim() === value,
|
|
512
|
+
),
|
|
513
|
+
) ||
|
|
514
|
+
!item.requiredArtifactVersions ||
|
|
515
|
+
typeof item.requiredArtifactVersions !== "object" ||
|
|
516
|
+
Array.isArray(item.requiredArtifactVersions) ||
|
|
517
|
+
!Object.entries(item.requiredArtifactVersions).every(
|
|
518
|
+
([id, version]) =>
|
|
519
|
+
isValidIdentifier(id) &&
|
|
520
|
+
typeof version === "string" &&
|
|
521
|
+
version.length > 0 &&
|
|
522
|
+
version.length <= MAX_IDENTIFIER_LENGTH &&
|
|
523
|
+
version.trim() === version,
|
|
524
|
+
) ||
|
|
525
|
+
(item.acceptedExecutionPlanId !== undefined &&
|
|
526
|
+
(typeof item.acceptedExecutionPlanId !== "string" ||
|
|
527
|
+
!/^[a-f0-9]{64}$/u.test(item.acceptedExecutionPlanId))) ||
|
|
528
|
+
!Object.entries(item.inputArtifactVersions).every(
|
|
529
|
+
([id, value]) =>
|
|
530
|
+
isValidIdentifier(id) &&
|
|
531
|
+
typeof value === "string" &&
|
|
532
|
+
value.length > 0 &&
|
|
533
|
+
value.length <= MAX_IDENTIFIER_LENGTH &&
|
|
534
|
+
value.trim() === value,
|
|
535
|
+
) ||
|
|
536
|
+
!Array.isArray(item.invalidationReasons) ||
|
|
537
|
+
!item.invalidationReasons.every(
|
|
538
|
+
(reason) =>
|
|
539
|
+
typeof reason === "string" &&
|
|
540
|
+
reason.length > 0 &&
|
|
541
|
+
reason.length <= MAX_TEXT_LENGTH &&
|
|
542
|
+
reason.trim() === reason,
|
|
543
|
+
) ||
|
|
544
|
+
(item.outcomeReason !== undefined &&
|
|
545
|
+
(typeof item.outcomeReason !== "string" ||
|
|
546
|
+
item.outcomeReason.length > MAX_TEXT_LENGTH ||
|
|
547
|
+
item.outcomeReason.trim() !== item.outcomeReason)) ||
|
|
548
|
+
typeof item.verificationAccepted !== "boolean"
|
|
549
|
+
) {
|
|
550
|
+
throw new Error(`Malformed stored WorkItem ${String(item?.id ?? "unknown")}`);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function validStoredArtifacts(
|
|
555
|
+
values: WorkArtifactReference[],
|
|
556
|
+
producerTaskId: string,
|
|
557
|
+
itemGeneration: number,
|
|
558
|
+
): boolean {
|
|
559
|
+
const seen = new Set<string>();
|
|
560
|
+
return values.every(
|
|
561
|
+
(artifact) =>
|
|
562
|
+
artifact !== null &&
|
|
563
|
+
typeof artifact === "object" &&
|
|
564
|
+
Object.keys(artifact).every((key) =>
|
|
565
|
+
["id", "kind", "version", "digest", "producerTaskId", "generation", "verified"].includes(
|
|
566
|
+
key,
|
|
567
|
+
),
|
|
568
|
+
) &&
|
|
569
|
+
isValidIdentifier(artifact.id) &&
|
|
570
|
+
claimUnique(artifact.id, seen) &&
|
|
571
|
+
typeof artifact.kind === "string" &&
|
|
572
|
+
artifact.kind.length > 0 &&
|
|
573
|
+
artifact.kind.length <= MAX_IDENTIFIER_LENGTH &&
|
|
574
|
+
artifact.kind.trim() === artifact.kind &&
|
|
575
|
+
typeof artifact.version === "string" &&
|
|
576
|
+
artifact.version.length > 0 &&
|
|
577
|
+
artifact.version.length <= MAX_IDENTIFIER_LENGTH &&
|
|
578
|
+
artifact.version.trim() === artifact.version &&
|
|
579
|
+
(artifact.digest === undefined ||
|
|
580
|
+
(typeof artifact.digest === "string" &&
|
|
581
|
+
artifact.digest.length > 0 &&
|
|
582
|
+
artifact.digest.length <= MAX_TEXT_LENGTH &&
|
|
583
|
+
artifact.digest.trim() === artifact.digest)) &&
|
|
584
|
+
artifact.producerTaskId === producerTaskId &&
|
|
585
|
+
Number.isSafeInteger(artifact.generation) &&
|
|
586
|
+
Number(artifact.generation) >= 0 &&
|
|
587
|
+
Number(artifact.generation) <= itemGeneration &&
|
|
588
|
+
typeof artifact.verified === "boolean",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
function claimUnique(value: string, seen: Set<string>): boolean {
|
|
593
|
+
if (seen.has(value)) return false;
|
|
594
|
+
seen.add(value);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function normalizeStoredArtifacts(
|
|
599
|
+
values: WorkArtifactReference[],
|
|
600
|
+
defaultProducerTaskId: string,
|
|
601
|
+
defaultGeneration: number,
|
|
602
|
+
): WorkArtifactReference[] {
|
|
603
|
+
if (!validStoredArtifacts(values, defaultProducerTaskId, defaultGeneration)) {
|
|
604
|
+
throw new Error(`Malformed stored artifacts for WorkItem ${defaultProducerTaskId}`);
|
|
605
|
+
}
|
|
606
|
+
return values.map((value) => structuredClone(value));
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
function normalizeArtifacts(
|
|
610
|
+
values: Array<Omit<WorkArtifactReference, "producerTaskId" | "generation">>,
|
|
611
|
+
producerTaskId: string,
|
|
612
|
+
generation: number,
|
|
613
|
+
): WorkArtifactReference[] {
|
|
614
|
+
if (values.length > MAX_LIST_ITEMS) throw new Error("Too many WorkItem artifacts");
|
|
615
|
+
const seen = new Set<string>();
|
|
616
|
+
return values.map((value) => {
|
|
617
|
+
validateIdentifier(value.id, "artifact id");
|
|
618
|
+
if (seen.has(value.id)) throw new Error(`Duplicate artifact id ${value.id}`);
|
|
619
|
+
seen.add(value.id);
|
|
620
|
+
const kind = bounded(value.kind, MAX_IDENTIFIER_LENGTH);
|
|
621
|
+
const version = bounded(value.version, MAX_IDENTIFIER_LENGTH);
|
|
622
|
+
if (!kind || !version) throw new Error(`Artifact ${value.id} requires kind and version`);
|
|
623
|
+
return {
|
|
624
|
+
id: value.id,
|
|
625
|
+
kind,
|
|
626
|
+
version,
|
|
627
|
+
...(value.digest ? { digest: bounded(value.digest, MAX_TEXT_LENGTH) } : {}),
|
|
628
|
+
producerTaskId,
|
|
629
|
+
generation,
|
|
630
|
+
verified: value.verified === true,
|
|
631
|
+
};
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function normalizeVersionMap(value: Record<string, string> | undefined): Record<string, string> {
|
|
636
|
+
if (value === undefined) return {};
|
|
637
|
+
const entries = Object.entries(value);
|
|
638
|
+
if (entries.length > MAX_LIST_ITEMS) throw new Error("Too many artifact version requirements");
|
|
639
|
+
return Object.fromEntries(
|
|
640
|
+
entries.map(([id, version]) => {
|
|
641
|
+
validateIdentifier(id, "artifact version id");
|
|
642
|
+
const normalized = bounded(version, MAX_IDENTIFIER_LENGTH);
|
|
643
|
+
if (!normalized) throw new Error(`Artifact ${id} requires a version`);
|
|
644
|
+
return [id, normalized];
|
|
645
|
+
}),
|
|
646
|
+
);
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function uniqueBounded(values: readonly string[], label: string): string[] {
|
|
650
|
+
if (!Array.isArray(values) || values.length > MAX_LIST_ITEMS) {
|
|
651
|
+
throw new Error(`Too many WorkItem ${label} values`);
|
|
652
|
+
}
|
|
653
|
+
const result: string[] = [];
|
|
654
|
+
const seen = new Set<string>();
|
|
655
|
+
for (const value of values) {
|
|
656
|
+
if (typeof value !== "string") throw new Error(`Invalid WorkItem ${label}`);
|
|
657
|
+
const normalized = bounded(value, MAX_TEXT_LENGTH);
|
|
658
|
+
if (!normalized) throw new Error(`Empty WorkItem ${label}`);
|
|
659
|
+
if (!seen.has(normalized)) result.push(normalized);
|
|
660
|
+
seen.add(normalized);
|
|
661
|
+
}
|
|
662
|
+
return result;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function isValidIdentifier(value: unknown): value is string {
|
|
666
|
+
return (
|
|
667
|
+
typeof value === "string" &&
|
|
668
|
+
value.length >= 1 &&
|
|
669
|
+
value.length <= MAX_IDENTIFIER_LENGTH &&
|
|
670
|
+
/^[A-Za-z0-9][A-Za-z0-9._:-]*$/u.test(value)
|
|
671
|
+
);
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
function validateIdentifier(value: string, label: string): void {
|
|
675
|
+
if (!isValidIdentifier(value)) {
|
|
676
|
+
throw new Error(`Invalid ${label}`);
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function bounded(value: string, maxLength: number): string {
|
|
681
|
+
return value.trim().slice(0, maxLength);
|
|
682
|
+
}
|