@narumitw/pi-subagents 1.0.2 → 2.0.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 (47) hide show
  1. package/README.md +198 -188
  2. package/package.json +2 -2
  3. package/src/agents/built-ins.ts +13 -66
  4. package/src/agents/catalog.ts +19 -2
  5. package/src/agents/discovery.ts +31 -15
  6. package/src/auto-transport.ts +7 -1
  7. package/src/child-peer-bridge.ts +124 -0
  8. package/src/child-peer-tools.ts +132 -0
  9. package/src/completion-delivery.ts +19 -5
  10. package/src/completion-render.ts +189 -0
  11. package/src/completion-routing.ts +24 -0
  12. package/src/config-ui.ts +11 -17
  13. package/src/consult-registration.ts +3 -2
  14. package/src/create-stateful-transport.ts +15 -2
  15. package/src/execution-ui.ts +0 -72
  16. package/src/in-process-transport.ts +39 -7
  17. package/src/inspect-tool.ts +3 -1
  18. package/src/peer-communication.ts +352 -0
  19. package/src/peer-transport.ts +49 -0
  20. package/src/persistence.ts +26 -1
  21. package/src/pi-args.ts +2 -0
  22. package/src/registry-types.ts +7 -0
  23. package/src/registry.ts +240 -41
  24. package/src/result-contract.ts +20 -5
  25. package/src/rpc-transport.ts +56 -26
  26. package/src/runner.ts +13 -1
  27. package/src/spawn-idempotency.ts +2 -0
  28. package/src/stateful-agent-view.ts +3 -1
  29. package/src/stateful-guidance.ts +11 -11
  30. package/src/stateful-safety.ts +0 -45
  31. package/src/stateful-tool-params.ts +11 -3
  32. package/src/stateful.ts +119 -47
  33. package/src/subagents.ts +6 -8
  34. package/src/subprocess-transport.ts +49 -28
  35. package/src/task-path.ts +65 -0
  36. package/src/transport-ui.ts +0 -6
  37. package/src/transport.ts +2 -1
  38. package/src/workflow-ui.ts +4 -4
  39. package/src/automation-contract.ts +0 -709
  40. package/src/automation-planner.ts +0 -65
  41. package/src/automation-registration.ts +0 -137
  42. package/src/automation-tool.ts +0 -40
  43. package/src/automation.ts +0 -435
  44. package/src/execution-profiles.ts +0 -95
  45. package/src/workflow-plan-compiler.ts +0 -618
  46. package/src/workflow-plan-patch.ts +0 -636
  47. package/src/workflow-planning-benchmark.ts +0 -95
@@ -1,95 +0,0 @@
1
- import type { SubagentThinkingLevel } from "./agents/types.js";
2
- import { readSubagentSettings, updateAgentSettingsPatch } from "./settings.js";
3
-
4
- export const EXECUTION_PROFILES = ["fast", "balanced", "deep"] as const;
5
- export type ExecutionProfile = (typeof EXECUTION_PROFILES)[number];
6
-
7
- const PROFILE_AGENT_ORDER = [
8
- "scout",
9
- "planner",
10
- "reviewer",
11
- "worker",
12
- "general",
13
- "general-purpose",
14
- ] as const;
15
-
16
- type ProfileAgent = (typeof PROFILE_AGENT_ORDER)[number];
17
-
18
- export const EXECUTION_PROFILE_THINKING: Record<
19
- ExecutionProfile,
20
- Record<ProfileAgent, SubagentThinkingLevel>
21
- > = {
22
- fast: {
23
- scout: "low",
24
- planner: "low",
25
- reviewer: "medium",
26
- worker: "low",
27
- general: "low",
28
- "general-purpose": "low",
29
- },
30
- balanced: {
31
- scout: "low",
32
- planner: "medium",
33
- reviewer: "medium",
34
- worker: "medium",
35
- general: "medium",
36
- "general-purpose": "medium",
37
- },
38
- deep: {
39
- scout: "medium",
40
- planner: "high",
41
- reviewer: "high",
42
- worker: "high",
43
- general: "high",
44
- "general-purpose": "high",
45
- },
46
- };
47
-
48
- export function executionProfileLabel(profile: ExecutionProfile): string {
49
- switch (profile) {
50
- case "fast":
51
- return "Fast";
52
- case "balanced":
53
- return "Balanced";
54
- case "deep":
55
- return "Deep";
56
- }
57
- }
58
-
59
- export function executionProfileDescription(profile: ExecutionProfile): string {
60
- switch (profile) {
61
- case "fast":
62
- return "Prefer low thinking for bounded work and medium for review.";
63
- case "balanced":
64
- return "Use low for scouting and medium for planning, review, and implementation.";
65
- case "deep":
66
- return "Use medium scouting and high thinking for planning, review, and implementation.";
67
- }
68
- }
69
-
70
- export function executionProfilePreview(profile: ExecutionProfile): string[] {
71
- const values = EXECUTION_PROFILE_THINKING[profile];
72
- return PROFILE_AGENT_ORDER.map((agent) => `${agent}: ${values[agent]}`);
73
- }
74
-
75
- export function inspectExecutionProfile(): ExecutionProfile | "custom" {
76
- const configured = readSubagentSettings()?.agents ?? {};
77
- for (const profile of EXECUTION_PROFILES) {
78
- const expected = EXECUTION_PROFILE_THINKING[profile];
79
- if (
80
- PROFILE_AGENT_ORDER.every((agent) => configured[agent]?.thinkingLevel === expected[agent])
81
- ) {
82
- return profile;
83
- }
84
- }
85
- return "custom";
86
- }
87
-
88
- export function applyExecutionProfile(profile: ExecutionProfile): void {
89
- const values = EXECUTION_PROFILE_THINKING[profile];
90
- updateAgentSettingsPatch(
91
- Object.fromEntries(
92
- PROFILE_AGENT_ORDER.map((agent) => [agent, { thinkingLevel: values[agent] }]),
93
- ),
94
- );
95
- }
@@ -1,618 +0,0 @@
1
- import {
2
- type DelegationAdmissionDecision,
3
- evaluateDelegationAdmission,
4
- } from "./admission-policy.js";
5
- import type { AgentConfig } from "./agents/types.js";
6
- import type { AutomationRequest, WorkflowPlan, WorkflowPlanTask } from "./automation-contract.js";
7
- import { MAX_AUTOMATION_ITEMS, workflowPlanIdentity } from "./automation-contract.js";
8
- import { routeByCapability } from "./capability-router.js";
9
- import type { TargetPolicyAudit } from "./cwd-policy.js";
10
- import {
11
- acknowledgeExecutionPlan,
12
- createExecutionPlan,
13
- type ExecutionPlan,
14
- resolveContractTools,
15
- } from "./execution-plan.js";
16
- import type { SubagentParams } from "./params.js";
17
- import { validateWorkflowVerificationGraph } from "./verification-policy.js";
18
-
19
- export interface WorkflowPlanCompilerInput {
20
- request: AutomationRequest;
21
- proposal: WorkflowPlan;
22
- agents: readonly AgentConfig[];
23
- target: TargetPolicyAudit;
24
- depth: number;
25
- }
26
-
27
- interface NonLaunchResult {
28
- status: "parent-owned" | "needs-input" | "rejected";
29
- childCount: 0;
30
- reasonCodes: string[];
31
- admission?: DelegationAdmissionDecision;
32
- missingInputs?: string[];
33
- }
34
-
35
- export interface CompiledWorkflowPlan {
36
- status: "compiled";
37
- childCount: number;
38
- planId: string;
39
- workflowGeneration: number;
40
- revision: number;
41
- request: AutomationRequest;
42
- plan: WorkflowPlan;
43
- workflow: NonNullable<SubagentParams["workflow"]>;
44
- executionPlans: ExecutionPlan[];
45
- admission: DelegationAdmissionDecision;
46
- maxConcurrentMutating: number;
47
- aggregateBudget: {
48
- timeoutMs: number;
49
- maxTurns: number;
50
- maxToolCalls: number;
51
- };
52
- }
53
-
54
- export type WorkflowPlanCompilerResult = NonLaunchResult | CompiledWorkflowPlan;
55
-
56
- export function compileWorkflowPlan(input: WorkflowPlanCompilerInput): WorkflowPlanCompilerResult {
57
- if (input.depth > 0) return rejected("workflow-recursion-disabled");
58
- if (!input.target.trust.projectTrusted) return rejected("target-not-trusted");
59
- if (input.request.constraints.workspaceMode !== "shared") {
60
- return rejected("workspace-mode-unsupported");
61
- }
62
- if (input.proposal.missingInputs.length > 0) {
63
- return {
64
- status: "needs-input",
65
- childCount: 0,
66
- reasonCodes: ["planner-reported-missing-inputs"],
67
- missingInputs: [...input.proposal.missingInputs],
68
- };
69
- }
70
- if (
71
- input.proposal.tasks.length > input.request.aggregateBudget.maxTasks ||
72
- input.proposal.tasks.length > 8
73
- ) {
74
- return rejected("task-budget-exceeded");
75
- }
76
- const budget = aggregateBudget(input.proposal.tasks);
77
- const budgetAllowsChildren = withinBudget(budget, input.request);
78
- const authorityError = validateAuthority(input.proposal.tasks, input.request);
79
- if (authorityError) return rejected(authorityError);
80
- const primaryTasks = input.proposal.tasks.filter((task) => !task.verifierFor);
81
- const roots = primaryTasks.filter((task) =>
82
- task.dependsOn.every(
83
- (dependency) => !primaryTasks.some((candidate) => candidate.id === dependency),
84
- ),
85
- );
86
- const mutatingTasks = primaryTasks.filter((task) => task.sideEffectPolicy === "mutating");
87
- const verificationRequired =
88
- input.request.constraints.requireVerification || mutatingTasks.length > 0;
89
- const eligibleAgents = filterAllowedAgents(input.agents, input.request);
90
- const verificationAvailable = verificationRequired
91
- ? eligibleAgents.some(
92
- (agent) =>
93
- agent.capabilityManifest?.verificationRoles.includes("independent-review") &&
94
- agent.capabilityManifest.resultFormats.includes("structured-v2"),
95
- )
96
- : true;
97
- const admission = evaluateDelegationAdmission({
98
- contextPressure: input.request.constraints.contextPressure,
99
- independentWorkItems: Math.max(1, roots.length),
100
- coupling: roots.length >= 2 ? "sparse" : "dense",
101
- verificationRequired,
102
- verificationAvailable,
103
- capabilitiesSupported: proposalCapabilitiesSupported(
104
- input.proposal.tasks,
105
- eligibleAgents,
106
- input.request,
107
- ),
108
- budgetAllowsChildren,
109
- generationCurrent: true,
110
- requirementsComplete:
111
- input.request.acceptanceCriteria.length > 0 && input.proposal.missingInputs.length === 0,
112
- });
113
- if (admission.recommendation === "parent-owned-direct") {
114
- return {
115
- status: "parent-owned",
116
- childCount: 0,
117
- reasonCodes: [...admission.reasonCodes],
118
- admission,
119
- };
120
- }
121
- if (admission.recommendation === "abstain-insufficient-evidence") {
122
- return {
123
- status: "rejected",
124
- childCount: 0,
125
- reasonCodes: [...admission.reasonCodes],
126
- admission,
127
- };
128
- }
129
- const integrationError = validateIntegrationPath(primaryTasks, mutatingTasks);
130
- if (integrationError) return rejected(integrationError, admission);
131
- const maxConcurrentMutating = calculateMaxConcurrentMutating(primaryTasks);
132
- if (
133
- maxConcurrentMutating > 2 ||
134
- maxConcurrentMutating > input.request.constraints.maxMutatingWidth
135
- ) {
136
- return rejected("mutating-width-exceeded", admission);
137
- }
138
-
139
- let normalizedTasks = input.proposal.tasks.map((task) => structuredClone(task));
140
- if (mutatingTasks.length === 1 && !mutatingTasks[0]?.integrationOwner) {
141
- normalizedTasks = normalizedTasks.map((task) =>
142
- task.id === mutatingTasks[0]?.id ? { ...task, integrationOwner: true } : task,
143
- );
144
- }
145
- if (
146
- verificationRequired &&
147
- normalizedTasks.filter((task) => !task.verifierFor).length === 1 &&
148
- !normalizedTasks.some((task) => task.integrationOwner)
149
- ) {
150
- normalizedTasks = normalizedTasks.map((task) =>
151
- task.verifierFor ? task : { ...task, integrationOwner: true },
152
- );
153
- }
154
- if (verificationRequired) {
155
- const target = verificationTarget(normalizedTasks);
156
- if (!target) return rejected("integration-owner-required", admission);
157
- const existing = normalizedTasks.filter((task) => task.verifierFor === target.id);
158
- if (existing.length > 1) return rejected("multiple-verifiers", admission);
159
- if (existing.length === 0) {
160
- const synthesized = synthesizeVerifier(target, input.request, normalizedTasks);
161
- if (!synthesized) return rejected("verification-budget-insufficient", admission);
162
- normalizedTasks.push(synthesized);
163
- }
164
- }
165
- const requirementsApplied = applyRequestRequirements(normalizedTasks, input.request);
166
- if (!requirementsApplied) {
167
- return rejected("request-requirements-exceed-task-limit", admission);
168
- }
169
- normalizedTasks = requirementsApplied;
170
- if (normalizedTasks.length > input.request.aggregateBudget.maxTasks) {
171
- return rejected("task-budget-exceeded-after-verification", admission);
172
- }
173
- const normalizedAuthorityError = validateAuthority(normalizedTasks, input.request);
174
- if (normalizedAuthorityError) return rejected(normalizedAuthorityError, admission);
175
- const normalizedPlan: WorkflowPlan = { ...input.proposal, tasks: normalizedTasks };
176
- const finalBudget = aggregateBudget(normalizedTasks);
177
- if (!withinBudget(finalBudget, input.request)) {
178
- return rejected("aggregate-budget-exceeded", admission);
179
- }
180
-
181
- let compiled: ReturnType<typeof compileTasks>;
182
- try {
183
- validateVerifierTasks(normalizedTasks);
184
- compiled = compileTasks(normalizedTasks, input.request, eligibleAgents, input.target);
185
- const requiredTarget = verificationRequired ? verificationTarget(normalizedTasks) : undefined;
186
- validateWorkflowVerificationGraph(
187
- compiled.workflowTasks.map((task) => ({
188
- id: task.id,
189
- agent: task.agent as string,
190
- dependsOn: task.dependsOn,
191
- verifierFor: task.verifierFor,
192
- resultFormat: task.resultFormat,
193
- })),
194
- new Set(requiredTarget ? [requiredTarget.id] : []),
195
- );
196
- } catch (error) {
197
- return rejected(reasonFromError(error), admission);
198
- }
199
- const planId = workflowPlanIdentity(normalizedPlan, 0, 0);
200
- return {
201
- status: "compiled",
202
- childCount: compiled.workflowTasks.length,
203
- planId,
204
- workflowGeneration: 0,
205
- revision: 0,
206
- request: structuredClone(input.request),
207
- plan: normalizedPlan,
208
- workflow: {
209
- id: `auto-${planId.slice(0, 24)}`,
210
- honorAdmission: false,
211
- tasks: compiled.workflowTasks,
212
- },
213
- executionPlans: compiled.executionPlans,
214
- admission,
215
- maxConcurrentMutating,
216
- aggregateBudget: finalBudget,
217
- };
218
- }
219
-
220
- function compileTasks(
221
- tasks: readonly WorkflowPlanTask[],
222
- request: AutomationRequest,
223
- agents: readonly AgentConfig[],
224
- target: TargetPolicyAudit,
225
- ): {
226
- workflowTasks: NonNullable<SubagentParams["workflow"]>["tasks"];
227
- executionPlans: ExecutionPlan[];
228
- } {
229
- const selected = new Map<string, AgentConfig>();
230
- const routingOrder = [...tasks].sort(
231
- (left, right) => Number(Boolean(left.verifierFor)) - Number(Boolean(right.verifierFor)),
232
- );
233
- for (const task of routingOrder) {
234
- const verifierTarget = task.verifierFor;
235
- const candidates = verifierTarget
236
- ? agents.filter((agent) => agent.name !== selected.get(verifierTarget)?.name)
237
- : agents;
238
- const route = routeByCapability(candidates, {
239
- requiredCapabilities: task.requiredCapabilities,
240
- requiredTools: task.requiredTools,
241
- requiredVerificationRole: task.verifierFor
242
- ? (task.requiredVerificationRole ?? "independent-review")
243
- : task.requiredVerificationRole,
244
- requiredSideEffectClass: task.sideEffectPolicy === "read-only" ? "read-only" : undefined,
245
- preferredCostHint: task.preferredCostHint,
246
- preferredLatencyHint: task.preferredLatencyHint,
247
- });
248
- selected.set(task.id, route.agent);
249
- }
250
- const workflowTasks: NonNullable<SubagentParams["workflow"]>["tasks"] = [];
251
- const executionPlans: ExecutionPlan[] = [];
252
- for (const task of tasks) {
253
- const agent = selected.get(task.id);
254
- if (!agent) throw new Error(`No capable agent for ${task.id}`);
255
- if (task.verifierFor && selected.get(task.verifierFor)?.name === agent.name) {
256
- throw new Error(`Verification agent for ${task.id} is not distinct`);
257
- }
258
- const dependencies = task.dependsOn.map((taskId) => ({ taskId }));
259
- const contract = {
260
- version: "pi-subagents:delegation:v2" as const,
261
- level: "full" as const,
262
- taskId: task.id,
263
- objective: task.objective,
264
- nonGoals: [...request.nonGoals],
265
- dependencies,
266
- requiredInputs: [...task.inputArtifacts],
267
- requestedAuthority: {
268
- capabilities: [...task.requiredCapabilities],
269
- tools: [...task.requiredTools],
270
- network: "unspecified" as const,
271
- secrets: "unspecified" as const,
272
- },
273
- acceptanceCriteria: [...task.acceptanceCriteria],
274
- requiredEvidence: [...task.requiredEvidence],
275
- budget: { ...task.budget },
276
- admission: {
277
- contextPressure: request.constraints.contextPressure,
278
- independentWorkItems: Math.min(
279
- 2,
280
- Math.max(1, tasks.filter((item) => !item.verifierFor).length),
281
- ),
282
- coupling: tasks.some((item) => item.dependsOn.length > 0)
283
- ? ("dense" as const)
284
- : ("sparse" as const),
285
- verificationRequired: task.integrationOwner && task.sideEffectPolicy === "mutating",
286
- verificationAvailable: tasks.some((item) => item.verifierFor === task.id),
287
- budgetAllowsChildren: true,
288
- requirementsComplete: true,
289
- },
290
- sideEffectPolicy: task.sideEffectPolicy,
291
- enforcement: "enforce" as const,
292
- };
293
- const effectiveTools = resolveContractTools(agent.tools, contract);
294
- const resultFormat = "structured-v2" as const;
295
- const executionPlan = createExecutionPlan({
296
- contract,
297
- agent,
298
- effectiveTools,
299
- target,
300
- workspaceMode: request.constraints.workspaceMode,
301
- transport: "subprocess",
302
- resultFormat,
303
- model: agent.model,
304
- thinkingLevel: agent.thinkingLevel,
305
- timeoutMs: task.budget.timeoutMs,
306
- taskGeneration: 1,
307
- });
308
- const acknowledgement = acknowledgeExecutionPlan(executionPlan);
309
- if (acknowledgement.status !== "accepted") {
310
- throw new Error(`Execution plan rejected: ${acknowledgement.reasonCodes.join(",")}`);
311
- }
312
- executionPlans.push(executionPlan);
313
- workflowTasks.push({
314
- id: task.id,
315
- agent: agent.name,
316
- requiredCapabilities: [...task.requiredCapabilities],
317
- requiredTools: [...task.requiredTools],
318
- ...(task.requiredVerificationRole
319
- ? { requiredVerificationRole: task.requiredVerificationRole }
320
- : task.verifierFor
321
- ? { requiredVerificationRole: "independent-review" }
322
- : {}),
323
- task: taskPrompt(task),
324
- dependsOn: [...task.dependsOn],
325
- inputArtifacts: [...task.inputArtifacts],
326
- inputArtifactVersions: Object.fromEntries(
327
- task.inputArtifacts.flatMap((artifactId) => {
328
- const artifact = tasks
329
- .flatMap((candidate) => candidate.producesArtifacts)
330
- .find((candidate) => candidate.id === artifactId);
331
- return artifact ? [[artifact.id, artifact.version]] : [];
332
- }),
333
- ),
334
- readPaths: [...task.readPaths],
335
- writePaths: [...task.writePaths],
336
- ownershipKeys: [...task.ownershipKeys],
337
- acceptanceCriteria: [...task.acceptanceCriteria],
338
- integrationOwner: task.integrationOwner,
339
- ...(task.verifierFor ? { verifierFor: task.verifierFor } : {}),
340
- timeoutMs: task.budget.timeoutMs,
341
- maxTurns: task.budget.maxTurns,
342
- maxToolCalls: task.budget.maxToolCalls,
343
- contract,
344
- resultFormat,
345
- });
346
- }
347
- return { workflowTasks, executionPlans };
348
- }
349
-
350
- function applyRequestRequirements(
351
- tasks: readonly WorkflowPlanTask[],
352
- request: AutomationRequest,
353
- ): WorkflowPlanTask[] | undefined {
354
- const dependencyIds = new Set(tasks.flatMap((task) => task.dependsOn));
355
- const result: WorkflowPlanTask[] = [];
356
- for (const task of tasks) {
357
- const authoritative =
358
- task.integrationOwner || Boolean(task.verifierFor) || !dependencyIds.has(task.id);
359
- if (!authoritative) {
360
- result.push(task);
361
- continue;
362
- }
363
- const acceptanceCriteria = unique([...task.acceptanceCriteria, ...request.acceptanceCriteria]);
364
- const requiredEvidence = unique([...task.requiredEvidence, ...request.requiredEvidence]);
365
- if (
366
- acceptanceCriteria.length > MAX_AUTOMATION_ITEMS ||
367
- requiredEvidence.length > MAX_AUTOMATION_ITEMS
368
- ) {
369
- return undefined;
370
- }
371
- result.push({ ...task, acceptanceCriteria, requiredEvidence });
372
- }
373
- return result;
374
- }
375
-
376
- function validateVerifierTasks(tasks: readonly WorkflowPlanTask[]): void {
377
- const byId = new Map(tasks.map((task) => [task.id, task]));
378
- for (const task of tasks) {
379
- if (!task.verifierFor) continue;
380
- const target = byId.get(task.verifierFor);
381
- if (!target || target.verifierFor) {
382
- throw new Error(`Workflow verifier ${task.id} has an invalid verification target`);
383
- }
384
- if (
385
- task.sideEffectPolicy !== "read-only" ||
386
- task.writePaths.length > 0 ||
387
- task.dependsOn.length !== 1 ||
388
- task.dependsOn[0] !== target.id
389
- ) {
390
- throw new Error(`Workflow verifier ${task.id} must be one direct read-only verifier`);
391
- }
392
- }
393
- }
394
-
395
- function validateAuthority(
396
- tasks: readonly WorkflowPlanTask[],
397
- request: AutomationRequest,
398
- ): string | undefined {
399
- const ceiling = request.authorityCeiling;
400
- const sideEffectRank = { "read-only": 0, idempotent: 1, mutating: 2 } as const;
401
- for (const task of tasks) {
402
- if (
403
- task.sideEffectPolicy === "read-only" &&
404
- task.requiredTools.some((tool) => ["bash", "edit", "write"].includes(tool))
405
- ) {
406
- return "read-only-tool-authority-conflict";
407
- }
408
- if (sideEffectRank[task.sideEffectPolicy] > sideEffectRank[ceiling.sideEffectPolicy]) {
409
- return "side-effect-authority-exceeded";
410
- }
411
- if (task.requiredCapabilities.some((item) => !ceiling.capabilities.includes(item))) {
412
- return "capability-authority-exceeded";
413
- }
414
- if (task.requiredTools.some((item) => !ceiling.tools.includes(item))) {
415
- return "tool-authority-exceeded";
416
- }
417
- if (task.readPaths.some((item) => !withinAnyScope(item, ceiling.readPaths))) {
418
- return "read-scope-exceeded";
419
- }
420
- if (task.writePaths.some((item) => !withinAnyScope(item, ceiling.writePaths))) {
421
- return "write-scope-exceeded";
422
- }
423
- }
424
- return undefined;
425
- }
426
-
427
- function validateIntegrationPath(
428
- primaryTasks: readonly WorkflowPlanTask[],
429
- mutatingTasks: readonly WorkflowPlanTask[],
430
- ): string | undefined {
431
- if (mutatingTasks.length <= 1) return undefined;
432
- const owners = primaryTasks.filter((task) => task.integrationOwner);
433
- if (owners.length !== 1) return "integration-owner-required";
434
- const owner = owners[0];
435
- const byId = new Map(primaryTasks.map((task) => [task.id, task]));
436
- const dependsOn = (task: WorkflowPlanTask, target: string, seen = new Set<string>()): boolean => {
437
- if (seen.has(task.id)) return false;
438
- seen.add(task.id);
439
- return task.dependsOn.some(
440
- (dependency) =>
441
- dependency === target ||
442
- (Boolean(byId.get(dependency)) &&
443
- dependsOn(byId.get(dependency) as WorkflowPlanTask, target, seen)),
444
- );
445
- };
446
- if (mutatingTasks.some((task) => task.id !== owner.id && !dependsOn(owner, task.id))) {
447
- return "integration-path-incomplete";
448
- }
449
- return undefined;
450
- }
451
-
452
- function synthesizeVerifier(
453
- target: WorkflowPlanTask,
454
- request: AutomationRequest,
455
- tasks: readonly WorkflowPlanTask[],
456
- ): WorkflowPlanTask | undefined {
457
- if (
458
- !request.authorityCeiling.capabilities.includes("code-review") ||
459
- !request.authorityCeiling.tools.includes("read")
460
- ) {
461
- return undefined;
462
- }
463
- const current = aggregateBudget(tasks);
464
- const remaining = {
465
- timeoutMs: request.aggregateBudget.timeoutMs - current.timeoutMs,
466
- maxTurns: request.aggregateBudget.maxTurns - current.maxTurns,
467
- maxToolCalls: request.aggregateBudget.maxToolCalls - current.maxToolCalls,
468
- };
469
- if (remaining.timeoutMs < 1 || remaining.maxTurns < 1 || remaining.maxToolCalls < 1)
470
- return undefined;
471
- let id = `verify-${target.id}`;
472
- for (let suffix = 2; tasks.some((task) => task.id === id); suffix++)
473
- id = `verify-${target.id}-${suffix}`;
474
- return {
475
- id,
476
- objective: `Independently verify ${target.id} against its acceptance criteria and required evidence`,
477
- dependsOn: [target.id],
478
- inputArtifacts: [],
479
- producesArtifacts: [],
480
- sideEffectPolicy: "read-only",
481
- readPaths: [...target.readPaths, ...target.writePaths].filter(
482
- (value, index, values) => values.indexOf(value) === index,
483
- ),
484
- writePaths: [],
485
- ownershipKeys: [],
486
- requiredCapabilities: ["code-review"],
487
- requiredTools: ["read"],
488
- requiredVerificationRole: "independent-review",
489
- acceptanceCriteria: [...target.acceptanceCriteria],
490
- requiredEvidence: [...target.requiredEvidence],
491
- integrationOwner: false,
492
- verifierFor: target.id,
493
- budget: {
494
- timeoutMs: Math.min(60_000, remaining.timeoutMs),
495
- maxTurns: Math.min(8, remaining.maxTurns),
496
- maxToolCalls: Math.min(16, remaining.maxToolCalls),
497
- },
498
- };
499
- }
500
-
501
- function verificationTarget(tasks: readonly WorkflowPlanTask[]): WorkflowPlanTask | undefined {
502
- const primary = tasks.filter((task) => !task.verifierFor);
503
- return (
504
- primary.find((task) => task.integrationOwner) ??
505
- (primary.filter((task) => task.sideEffectPolicy === "mutating").length === 1
506
- ? primary.find((task) => task.sideEffectPolicy === "mutating")
507
- : undefined)
508
- );
509
- }
510
-
511
- function proposalCapabilitiesSupported(
512
- tasks: readonly WorkflowPlanTask[],
513
- agents: readonly AgentConfig[],
514
- request: AutomationRequest,
515
- ): boolean {
516
- if (validateAuthority(tasks, request)) return false;
517
- return tasks.every((task) => {
518
- try {
519
- routeByCapability(agents, {
520
- requiredCapabilities: task.requiredCapabilities,
521
- requiredTools: task.requiredTools,
522
- requiredVerificationRole: task.requiredVerificationRole,
523
- requiredSideEffectClass: task.sideEffectPolicy === "read-only" ? "read-only" : undefined,
524
- });
525
- return true;
526
- } catch {
527
- return false;
528
- }
529
- });
530
- }
531
-
532
- function filterAllowedAgents(
533
- agents: readonly AgentConfig[],
534
- request: AutomationRequest,
535
- ): AgentConfig[] {
536
- const allowed = request.constraints.allowedAgents;
537
- return allowed ? agents.filter((agent) => allowed.includes(agent.name)) : [...agents];
538
- }
539
-
540
- function aggregateBudget(tasks: readonly WorkflowPlanTask[]) {
541
- return tasks.reduce(
542
- (total, task) => ({
543
- timeoutMs: total.timeoutMs + task.budget.timeoutMs,
544
- maxTurns: total.maxTurns + task.budget.maxTurns,
545
- maxToolCalls: total.maxToolCalls + task.budget.maxToolCalls,
546
- }),
547
- { timeoutMs: 0, maxTurns: 0, maxToolCalls: 0 },
548
- );
549
- }
550
-
551
- function withinBudget(
552
- budget: ReturnType<typeof aggregateBudget>,
553
- request: AutomationRequest,
554
- ): boolean {
555
- return (
556
- budget.timeoutMs <= request.aggregateBudget.timeoutMs &&
557
- budget.maxTurns <= request.aggregateBudget.maxTurns &&
558
- budget.maxToolCalls <= request.aggregateBudget.maxToolCalls
559
- );
560
- }
561
-
562
- function calculateMaxConcurrentMutating(tasks: readonly WorkflowPlanTask[]): number {
563
- const levels = new Map<string, number>();
564
- const byId = new Map(tasks.map((task) => [task.id, task]));
565
- const level = (task: WorkflowPlanTask): number => {
566
- const cached = levels.get(task.id);
567
- if (cached !== undefined) return cached;
568
- const value = task.dependsOn.length
569
- ? 1 + Math.max(...task.dependsOn.map((id) => level(byId.get(id) as WorkflowPlanTask)))
570
- : 0;
571
- levels.set(task.id, value);
572
- return value;
573
- };
574
- const counts = new Map<number, number>();
575
- for (const task of tasks) {
576
- if (task.sideEffectPolicy !== "mutating") continue;
577
- const taskLevel = level(task);
578
- counts.set(taskLevel, (counts.get(taskLevel) ?? 0) + 1);
579
- }
580
- return Math.max(0, ...counts.values());
581
- }
582
-
583
- function unique(values: readonly string[]): string[] {
584
- return [...new Set(values)];
585
- }
586
-
587
- function withinAnyScope(candidate: string, scopes: readonly string[]): boolean {
588
- return scopes.some(
589
- (scope) =>
590
- scope === "." || candidate === scope || candidate.startsWith(`${scope.replace(/\/$/u, "")}/`),
591
- );
592
- }
593
-
594
- function taskPrompt(task: WorkflowPlanTask): string {
595
- return [
596
- task.objective,
597
- `Acceptance criteria: ${JSON.stringify(task.acceptanceCriteria)}.`,
598
- `Required evidence: ${JSON.stringify(task.requiredEvidence)}.`,
599
- "Stay within the executor-declared authority and report missing inputs instead of guessing.",
600
- ].join("\n");
601
- }
602
-
603
- function rejected(reasonCode: string, admission?: DelegationAdmissionDecision): NonLaunchResult {
604
- return {
605
- status: "rejected",
606
- childCount: 0,
607
- reasonCodes: [reasonCode],
608
- ...(admission ? { admission } : {}),
609
- };
610
- }
611
-
612
- function reasonFromError(error: unknown): string {
613
- const message = error instanceof Error ? error.message : String(error);
614
- if (/verification/i.test(message)) return "verification-agent-unavailable";
615
- if (/capab|tool/i.test(message)) return "capability-unsupported";
616
- if (/execution plan/i.test(message)) return "execution-plan-rejected";
617
- return "workflow-compilation-failed";
618
- }