@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,114 @@
1
+ import { discoverAgents, type SubagentSettings } from "./agents.js";
2
+ import type { ManagedAgent, TurnOutcome } from "./registry.js";
3
+ import { isWriteCapable } from "./stateful-safety.js";
4
+ import type { SubagentTransport } from "./transport.js";
5
+ import type {
6
+ EffectiveSubagentTransportKind,
7
+ TransportProgressCallback,
8
+ } from "./transport-types.js";
9
+
10
+ const BUILT_IN_TOOL_NAMES = new Set(["read", "bash", "edit", "write", "grep", "find", "ls"]);
11
+
12
+ export interface AutoTransportOptions {
13
+ subprocess: SubagentTransport;
14
+ inProcess: SubagentTransport;
15
+ rpc: SubagentTransport;
16
+ getSettings?: () => SubagentSettings | undefined;
17
+ }
18
+
19
+ export interface AutoTransportSelection {
20
+ kind: EffectiveSubagentTransportKind;
21
+ reason: string;
22
+ }
23
+
24
+ export class AutoTransport implements SubagentTransport {
25
+ readonly kind = "auto" as const;
26
+ private readonly selections = new Map<string, AutoTransportSelection>();
27
+
28
+ constructor(private readonly options: AutoTransportOptions) {}
29
+
30
+ async runTurn(
31
+ agent: ManagedAgent,
32
+ task: string,
33
+ signal: AbortSignal,
34
+ onProgress?: TransportProgressCallback,
35
+ ): Promise<TurnOutcome> {
36
+ const selection = this.selections.get(agent.id) ?? this.select(agent);
37
+ this.selections.set(agent.id, selection);
38
+ const transport = this.transport(selection.kind);
39
+ const outcome = await transport.runTurn(agent, task, signal, (progress) =>
40
+ onProgress?.({ ...progress, selectionReason: selection.reason }),
41
+ );
42
+ return {
43
+ ...outcome,
44
+ telemetry: outcome.telemetry
45
+ ? { ...outcome.telemetry, selectionReason: selection.reason }
46
+ : outcome.telemetry,
47
+ };
48
+ }
49
+
50
+ async release(agent: ManagedAgent): Promise<void> {
51
+ const selection = this.selections.get(agent.id);
52
+ this.selections.delete(agent.id);
53
+ if (selection) await this.transport(selection.kind).release?.(agent);
54
+ }
55
+
56
+ async shutdown(): Promise<void> {
57
+ this.selections.clear();
58
+ const transports = [this.options.subprocess, this.options.inProcess, this.options.rpc];
59
+ const results = await Promise.allSettled(transports.map((transport) => transport.shutdown?.()));
60
+ const failures = results.flatMap((result) =>
61
+ result.status === "rejected" ? [result.reason] : [],
62
+ );
63
+ if (failures.length > 0) {
64
+ throw new AggregateError(
65
+ failures,
66
+ `Failed to shut down ${failures.length} auto transport(s)`,
67
+ );
68
+ }
69
+ }
70
+
71
+ selectionFor(agentId: string): AutoTransportSelection | undefined {
72
+ const selection = this.selections.get(agentId);
73
+ return selection ? { ...selection } : undefined;
74
+ }
75
+
76
+ private select(agent: ManagedAgent): AutoTransportSelection {
77
+ const settings = this.options.getSettings?.();
78
+ const config = discoverAgents(agent.cwd, agent.agentScope ?? "user", settings).agents.find(
79
+ (candidate) => candidate.name === agent.agent,
80
+ );
81
+ if (!config) {
82
+ throw new Error(`Automatic transport cannot resolve subagent ${agent.agent}`);
83
+ }
84
+ const effectiveTools = agent.executionPlan?.effectiveTools ?? config.tools;
85
+ const unsupported = (effectiveTools ?? []).filter((tool) => !BUILT_IN_TOOL_NAMES.has(tool));
86
+ if (unsupported.length > 0) {
87
+ return {
88
+ kind: "subprocess",
89
+ reason: `extension/custom tools require subprocess: ${unsupported.join(", ")}`,
90
+ };
91
+ }
92
+ if (isWriteCapable(effectiveTools)) {
93
+ return {
94
+ kind: "rpc",
95
+ reason: "write-capable built-in tools use a persistent isolated process",
96
+ };
97
+ }
98
+ return {
99
+ kind: "in-process",
100
+ reason: "read-only built-in tools use the lowest-overhead public SDK session",
101
+ };
102
+ }
103
+
104
+ private transport(kind: EffectiveSubagentTransportKind): SubagentTransport {
105
+ switch (kind) {
106
+ case "subprocess":
107
+ return this.options.subprocess;
108
+ case "in-process":
109
+ return this.options.inProcess;
110
+ case "rpc":
111
+ return this.options.rpc;
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,63 @@
1
+ const STATUS_KEY = "subagents";
2
+ const activeStatuses = new Map<string, string>();
3
+
4
+ interface StatusContext {
5
+ ui: { setStatus: (key: string, value: string | undefined) => void };
6
+ }
7
+
8
+ export function startSubagentStatus(
9
+ ctx: StatusContext,
10
+ toolCallId: string,
11
+ status: string,
12
+ ): { update(status: string): void; clear(): void } {
13
+ let cleared = false;
14
+ const update = (nextStatus: string) => {
15
+ if (cleared) return;
16
+ activeStatuses.set(toolCallId, nextStatus);
17
+ publishSubagentStatus(ctx);
18
+ };
19
+ update(status);
20
+ return {
21
+ update,
22
+ clear() {
23
+ if (cleared) return;
24
+ cleared = true;
25
+ activeStatuses.delete(toolCallId);
26
+ publishSubagentStatus(ctx);
27
+ },
28
+ };
29
+ }
30
+
31
+ function publishSubagentStatus(ctx: StatusContext): void {
32
+ const statuses = [...activeStatuses.values()];
33
+ if (statuses.length === 0) {
34
+ ctx.ui.setStatus(STATUS_KEY, undefined);
35
+ return;
36
+ }
37
+ const suffix = statuses.length > 1 ? ` +${statuses.length - 1}` : "";
38
+ ctx.ui.setStatus(STATUS_KEY, `${statuses[0]}${suffix}`);
39
+ }
40
+
41
+ export function singleStatus(agent: string): string {
42
+ return `${agent}`;
43
+ }
44
+
45
+ export function chainStatus(step: number, total: number, agent?: string): string {
46
+ return `chain ${step}/${total}${agent ? ` ${agent}` : ""}`;
47
+ }
48
+
49
+ export function parallelStatus(done: number, total: number, running: number): string {
50
+ return `parallel ${done}/${total} done${running > 0 ? ` ${running} running` : ""}`;
51
+ }
52
+
53
+ export function fanInStatus(agent: string): string {
54
+ return `fan-in ${agent}`;
55
+ }
56
+
57
+ export function panelReviewStatus(done: number, total: number, running: number): string {
58
+ return `panel review ${done}/${total}${running > 0 ? ` ${running} running` : ""}`;
59
+ }
60
+
61
+ export function panelSynthesisStatus(agent: string): string {
62
+ return `panel synthesis ${agent}`;
63
+ }
@@ -0,0 +1,145 @@
1
+ import type { SubagentResultFormat } from "./result-contract.js";
2
+ import { SUBAGENT_RESULT_FORMATS } from "./result-contract.js";
3
+
4
+ export const CAPABILITY_MANIFEST_VERSION = "pi-subagents:capabilities:v1" as const;
5
+ export const CAPABILITY_MODALITIES = ["text", "image", "audio"] as const;
6
+ export const FILESYSTEM_AUTHORITY = ["none", "read", "write"] as const;
7
+ export const EXTERNAL_AUTHORITY = ["none", "required"] as const;
8
+ export const CAPABILITY_HINTS = ["low", "medium", "high"] as const;
9
+ export type CapabilityHint = (typeof CAPABILITY_HINTS)[number];
10
+
11
+ const MAX_ITEMS = 50;
12
+ const MAX_ITEM_LENGTH = 256;
13
+ const MAX_LIMITATION_LENGTH = 1024;
14
+
15
+ export interface AgentAuthorityManifest {
16
+ filesystem?: (typeof FILESYSTEM_AUTHORITY)[number];
17
+ network?: (typeof EXTERNAL_AUTHORITY)[number];
18
+ secrets?: (typeof EXTERNAL_AUTHORITY)[number];
19
+ }
20
+
21
+ export interface AgentCapabilityManifest {
22
+ version: typeof CAPABILITY_MANIFEST_VERSION;
23
+ capabilities: string[];
24
+ modalities: Array<(typeof CAPABILITY_MODALITIES)[number]>;
25
+ resultFormats: SubagentResultFormat[];
26
+ authority?: AgentAuthorityManifest;
27
+ verificationRoles: string[];
28
+ contextStrengths?: string[];
29
+ costHint?: CapabilityHint;
30
+ latencyHint?: CapabilityHint;
31
+ limitations: string[];
32
+ }
33
+
34
+ export function normalizeCapabilityManifest(value: unknown): AgentCapabilityManifest | undefined {
35
+ if (value === undefined) return undefined;
36
+ if (!isPlainObject(value) || value.version !== CAPABILITY_MANIFEST_VERSION) return undefined;
37
+ const capabilities = identifiers(value.capabilities, false);
38
+ const modalities = enumArray(value.modalities, CAPABILITY_MODALITIES);
39
+ const resultFormats = enumArray(value.resultFormats, SUBAGENT_RESULT_FORMATS);
40
+ const verificationRoles = identifiers(value.verificationRoles, false);
41
+ const contextStrengths =
42
+ value.contextStrengths === undefined ? undefined : identifiers(value.contextStrengths, false);
43
+ const costHint = optionalEnum(value.costHint, CAPABILITY_HINTS);
44
+ const latencyHint = optionalEnum(value.latencyHint, CAPABILITY_HINTS);
45
+ const limitations = strings(value.limitations, MAX_LIMITATION_LENGTH);
46
+ if (
47
+ !capabilities ||
48
+ !modalities ||
49
+ !resultFormats ||
50
+ !verificationRoles ||
51
+ (contextStrengths === undefined && value.contextStrengths !== undefined) ||
52
+ costHint === false ||
53
+ latencyHint === false ||
54
+ !limitations
55
+ ) {
56
+ return undefined;
57
+ }
58
+ const authority = normalizeAuthority(value.authority);
59
+ if (authority === false) return undefined;
60
+ return {
61
+ version: CAPABILITY_MANIFEST_VERSION,
62
+ capabilities,
63
+ modalities,
64
+ resultFormats,
65
+ ...(authority === undefined ? {} : { authority }),
66
+ verificationRoles,
67
+ ...(contextStrengths === undefined ? {} : { contextStrengths }),
68
+ ...(costHint === undefined ? {} : { costHint }),
69
+ ...(latencyHint === undefined ? {} : { latencyHint }),
70
+ limitations,
71
+ };
72
+ }
73
+
74
+ export function projectCapabilityManifest(
75
+ manifest: AgentCapabilityManifest | undefined,
76
+ ): AgentCapabilityManifest | undefined {
77
+ return manifest ? structuredClone(manifest) : undefined;
78
+ }
79
+
80
+ function normalizeAuthority(value: unknown): AgentAuthorityManifest | undefined | false {
81
+ if (value === undefined) return undefined;
82
+ if (!isPlainObject(value)) return false;
83
+ const filesystem = optionalEnum(value.filesystem, FILESYSTEM_AUTHORITY);
84
+ const network = optionalEnum(value.network, EXTERNAL_AUTHORITY);
85
+ const secrets = optionalEnum(value.secrets, EXTERNAL_AUTHORITY);
86
+ if (filesystem === false || network === false || secrets === false) return false;
87
+ return {
88
+ ...(filesystem === undefined ? {} : { filesystem }),
89
+ ...(network === undefined ? {} : { network }),
90
+ ...(secrets === undefined ? {} : { secrets }),
91
+ };
92
+ }
93
+
94
+ function identifiers(value: unknown, required: boolean): string[] | undefined {
95
+ if (value === undefined) return required ? undefined : [];
96
+ return strings(value, MAX_ITEM_LENGTH, /^[a-z0-9]+(?:-[a-z0-9]+)*$/u);
97
+ }
98
+
99
+ function strings(value: unknown, maxLength: number, pattern?: RegExp): string[] | undefined {
100
+ if (value === undefined) return [];
101
+ if (!Array.isArray(value) || value.length > MAX_ITEMS) return undefined;
102
+ const result: string[] = [];
103
+ const seen = new Set<string>();
104
+ for (const item of value) {
105
+ if (typeof item !== "string") return undefined;
106
+ const normalized = item.trim();
107
+ if (!normalized || normalized.length > maxLength || (pattern && !pattern.test(normalized))) {
108
+ return undefined;
109
+ }
110
+ if (seen.has(normalized)) continue;
111
+ seen.add(normalized);
112
+ result.push(normalized);
113
+ }
114
+ return result;
115
+ }
116
+
117
+ function enumArray<const T extends readonly string[]>(
118
+ value: unknown,
119
+ allowed: T,
120
+ ): Array<T[number]> | undefined {
121
+ if (value === undefined) return [];
122
+ if (!Array.isArray(value) || value.length > MAX_ITEMS) return undefined;
123
+ const result: Array<T[number]> = [];
124
+ const seen = new Set<string>();
125
+ for (const item of value) {
126
+ if (typeof item !== "string" || !allowed.includes(item)) return undefined;
127
+ if (seen.has(item)) continue;
128
+ seen.add(item);
129
+ result.push(item as T[number]);
130
+ }
131
+ return result;
132
+ }
133
+
134
+ function optionalEnum<const T extends readonly string[]>(
135
+ value: unknown,
136
+ allowed: T,
137
+ ): T[number] | undefined | false {
138
+ if (value === undefined) return undefined;
139
+ if (typeof value !== "string" || !allowed.includes(value)) return false;
140
+ return value as T[number];
141
+ }
142
+
143
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
144
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
145
+ }
@@ -0,0 +1,115 @@
1
+ import { createHash } from "node:crypto";
2
+ import { type ExecutionPlan, isExecutionPlan } from "./execution-plan.js";
3
+
4
+ export const CAPABILITY_GRANT_VERSION = "pi-subagents:capability-grant:v1" as const;
5
+
6
+ export interface CapabilityGrant {
7
+ version: typeof CAPABILITY_GRANT_VERSION;
8
+ id: string;
9
+ executionPlanId: string;
10
+ taskGeneration: number;
11
+ effectiveTools?: string[];
12
+ issuedAt: number;
13
+ expiresAt: number;
14
+ state: "active" | "revoked";
15
+ revokedAt?: number;
16
+ revocationReason?: string;
17
+ }
18
+
19
+ function grantId(projection: {
20
+ executionPlanId: string;
21
+ taskGeneration: number;
22
+ effectiveTools?: string[];
23
+ issuedAt: number;
24
+ expiresAt: number;
25
+ }): string {
26
+ return createHash("sha256").update(JSON.stringify(projection)).digest("hex");
27
+ }
28
+
29
+ export function issueCapabilityGrant(
30
+ plan: ExecutionPlan,
31
+ issuedAt: number,
32
+ lifetimeMs: number,
33
+ ): CapabilityGrant {
34
+ if (!Number.isFinite(issuedAt) || !Number.isFinite(lifetimeMs) || lifetimeMs <= 0) {
35
+ throw new Error("Capability grant requires finite issuance and lifetime bounds");
36
+ }
37
+ const expiresAt = issuedAt + lifetimeMs;
38
+ const projection = {
39
+ executionPlanId: plan.id,
40
+ taskGeneration: plan.taskGeneration,
41
+ effectiveTools: plan.effectiveTools,
42
+ issuedAt,
43
+ expiresAt,
44
+ };
45
+ return {
46
+ version: CAPABILITY_GRANT_VERSION,
47
+ id: grantId(projection),
48
+ ...projection,
49
+ state: "active",
50
+ };
51
+ }
52
+
53
+ export function revokeCapabilityGrant(
54
+ grant: CapabilityGrant,
55
+ reason: string,
56
+ revokedAt: number,
57
+ ): CapabilityGrant {
58
+ if (grant.state === "revoked") throw new Error("Capability grant is already revoked");
59
+ grant.state = "revoked";
60
+ grant.revokedAt = revokedAt;
61
+ grant.revocationReason = reason.slice(0, 256);
62
+ return structuredClone(grant);
63
+ }
64
+
65
+ export function isCapabilityGrant(value: unknown): value is CapabilityGrant {
66
+ if (!value || typeof value !== "object" || Array.isArray(value)) return false;
67
+ const grant = value as Partial<CapabilityGrant>;
68
+ const validShape =
69
+ grant.version === CAPABILITY_GRANT_VERSION &&
70
+ typeof grant.id === "string" &&
71
+ /^[a-f0-9]{64}$/u.test(grant.id) &&
72
+ typeof grant.executionPlanId === "string" &&
73
+ /^[a-f0-9]{64}$/u.test(grant.executionPlanId) &&
74
+ Number.isSafeInteger(grant.taskGeneration) &&
75
+ Number(grant.taskGeneration) >= 0 &&
76
+ Number.isFinite(grant.issuedAt) &&
77
+ Number.isFinite(grant.expiresAt) &&
78
+ Number(grant.expiresAt) >= Number(grant.issuedAt) &&
79
+ (grant.state === "active" || grant.state === "revoked") &&
80
+ (grant.state === "active"
81
+ ? grant.revokedAt === undefined && grant.revocationReason === undefined
82
+ : Number.isFinite(grant.revokedAt) &&
83
+ typeof grant.revocationReason === "string" &&
84
+ grant.revocationReason.length > 0) &&
85
+ (grant.effectiveTools === undefined ||
86
+ (Array.isArray(grant.effectiveTools) &&
87
+ grant.effectiveTools.every((tool) => typeof tool === "string")));
88
+ if (!validShape) return false;
89
+ const validated = grant as CapabilityGrant;
90
+ const expectedId = grantId({
91
+ executionPlanId: validated.executionPlanId,
92
+ taskGeneration: validated.taskGeneration,
93
+ effectiveTools: validated.effectiveTools,
94
+ issuedAt: validated.issuedAt,
95
+ expiresAt: validated.expiresAt,
96
+ });
97
+ return validated.id === expectedId;
98
+ }
99
+
100
+ export function isCapabilityGrantActive(
101
+ grant: CapabilityGrant,
102
+ plan: ExecutionPlan,
103
+ now: number,
104
+ ): boolean {
105
+ return (
106
+ isCapabilityGrant(grant) &&
107
+ isExecutionPlan(plan) &&
108
+ grant.state === "active" &&
109
+ grant.executionPlanId === plan.id &&
110
+ grant.taskGeneration === plan.taskGeneration &&
111
+ JSON.stringify(grant.effectiveTools ?? null) === JSON.stringify(plan.effectiveTools ?? null) &&
112
+ now >= grant.issuedAt &&
113
+ now <= grant.expiresAt
114
+ );
115
+ }
@@ -0,0 +1,107 @@
1
+ import { type AgentConfig, resolveAgentToolNames } from "./agents.js";
2
+ import type { CapabilityHint } from "./capabilities.js";
3
+
4
+ export interface CapabilityRouteRequest {
5
+ agent?: string;
6
+ requiredCapabilities?: string[];
7
+ requiredTools?: string[];
8
+ requiredVerificationRole?: string;
9
+ requiredSideEffectClass?: string;
10
+ preferredCostHint?: CapabilityHint;
11
+ preferredLatencyHint?: CapabilityHint;
12
+ }
13
+
14
+ export interface CapabilityRouteDecision {
15
+ agent: AgentConfig;
16
+ eligibleAgents: string[];
17
+ requiredCapabilities: string[];
18
+ }
19
+
20
+ const HINT_RANK: Record<CapabilityHint, number> = { low: 0, medium: 1, high: 2 };
21
+
22
+ export function routeByCapability(
23
+ agents: readonly AgentConfig[],
24
+ request: CapabilityRouteRequest,
25
+ ): CapabilityRouteDecision {
26
+ const requiredCapabilities = unique(request.requiredCapabilities ?? []);
27
+ const requiredTools = unique(request.requiredTools ?? []);
28
+ const eligible = agents.filter((agent) => {
29
+ const manifest = agent.capabilityManifest;
30
+ const effectiveTools = resolveAgentToolNames(agent.tools);
31
+ if (!manifest) {
32
+ return (
33
+ requiredCapabilities.length === 0 &&
34
+ requiredTools.every((tool) => effectiveTools.includes(tool)) &&
35
+ request.requiredVerificationRole === undefined &&
36
+ request.requiredSideEffectClass === undefined
37
+ );
38
+ }
39
+ const sideEffectAllowed =
40
+ request.requiredSideEffectClass !== "read-only" ||
41
+ manifest.authority?.filesystem === "read" ||
42
+ manifest.authority?.filesystem === "none";
43
+ return (
44
+ requiredCapabilities.every((capability) => manifest.capabilities.includes(capability)) &&
45
+ requiredTools.every((tool) => effectiveTools.includes(tool)) &&
46
+ (!request.requiredVerificationRole ||
47
+ manifest.verificationRoles.includes(request.requiredVerificationRole)) &&
48
+ sideEffectAllowed
49
+ );
50
+ });
51
+ if (request.agent) {
52
+ const named = agents.find((agent) => agent.name === request.agent);
53
+ if (!named) throw new Error(`Unknown subagent ${request.agent}`);
54
+ if (!eligible.includes(named)) {
55
+ throw new Error(
56
+ `Selected subagent ${request.agent} does not satisfy the required capability manifest`,
57
+ );
58
+ }
59
+ return {
60
+ agent: named,
61
+ eligibleAgents: eligible.map((agent) => agent.name).sort(),
62
+ requiredCapabilities,
63
+ };
64
+ }
65
+ if (eligible.length === 0) {
66
+ throw new Error(
67
+ `No capable agent satisfies: ${
68
+ [...requiredCapabilities, ...requiredTools.map((tool) => `tool:${tool}`)].join(", ") ||
69
+ "the requested side-effect class"
70
+ }`,
71
+ );
72
+ }
73
+ const ranked = [...eligible].sort((left, right) => {
74
+ const leftCost = hintDistance(
75
+ left.capabilityManifest?.costHint ?? "medium",
76
+ request.preferredCostHint,
77
+ );
78
+ const rightCost = hintDistance(
79
+ right.capabilityManifest?.costHint ?? "medium",
80
+ request.preferredCostHint,
81
+ );
82
+ const leftLatency = hintDistance(
83
+ left.capabilityManifest?.latencyHint ?? "medium",
84
+ request.preferredLatencyHint,
85
+ );
86
+ const rightLatency = hintDistance(
87
+ right.capabilityManifest?.latencyHint ?? "medium",
88
+ request.preferredLatencyHint,
89
+ );
90
+ return (
91
+ leftCost - rightCost || leftLatency - rightLatency || left.name.localeCompare(right.name)
92
+ );
93
+ });
94
+ return {
95
+ agent: ranked[0],
96
+ eligibleAgents: ranked.map((agent) => agent.name),
97
+ requiredCapabilities,
98
+ };
99
+ }
100
+
101
+ function hintDistance(actual: CapabilityHint, preferred: CapabilityHint | undefined): number {
102
+ return preferred ? Math.abs(HINT_RANK[actual] - HINT_RANK[preferred]) : HINT_RANK[actual];
103
+ }
104
+
105
+ function unique(values: readonly string[]): string[] {
106
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))].sort();
107
+ }