pi-subagents 0.35.1 → 0.37.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/CHANGELOG.md +71 -0
- package/README.md +224 -31
- package/agents/advisor.md +73 -0
- package/agents/delegate.md +2 -0
- package/agents/worker.md +2 -0
- package/package.json +11 -13
- package/skills/pi-subagents/SKILL.md +42 -13
- package/src/agents/agent-management.ts +7 -1
- package/src/agents/agents.ts +121 -17
- package/src/api/capability-ceiling.ts +17 -0
- package/src/api/delegation.ts +127 -0
- package/src/api/preflight.ts +399 -0
- package/src/extension/config.ts +7 -1
- package/src/extension/index.ts +52 -38
- package/src/extension/rpc.ts +31 -2
- package/src/extension/schemas.ts +29 -3
- package/src/extension/tool-description.ts +4 -2
- package/src/intercom/intercom-bridge.ts +1 -1
- package/src/intercom/native-supervisor-channel.ts +45 -6
- package/src/intercom/result-intercom.ts +7 -0
- package/src/runs/background/async-execution.ts +158 -17
- package/src/runs/background/async-job-tracker.ts +4 -0
- package/src/runs/background/async-resume.ts +47 -8
- package/src/runs/background/async-status.ts +94 -3
- package/src/runs/background/chain-append.ts +2 -0
- package/src/runs/background/completion-batcher.ts +6 -4
- package/src/runs/background/completion-dedupe.ts +2 -11
- package/src/runs/background/fleet-view.ts +9 -4
- package/src/runs/background/notify.ts +132 -120
- package/src/runs/background/process-terminal.ts +280 -0
- package/src/runs/background/result-watcher.ts +138 -78
- package/src/runs/background/run-status.ts +10 -2
- package/src/runs/background/scheduled-runs.ts +6 -1
- package/src/runs/background/stale-run-reconciler.ts +6 -0
- package/src/runs/background/subagent-runner.ts +406 -54
- package/src/runs/background/subagent-wait.ts +130 -4
- package/src/runs/background/wait-tool.ts +2 -2
- package/src/runs/foreground/chain-execution.ts +181 -111
- package/src/runs/foreground/execution.ts +147 -47
- package/src/runs/foreground/foreground-control.ts +90 -0
- package/src/runs/foreground/subagent-executor.ts +426 -165
- package/src/runs/shared/acceptance.ts +94 -41
- package/src/runs/shared/agent-contract.ts +38 -0
- package/src/runs/shared/capability-ceiling.ts +177 -0
- package/src/runs/shared/child-protocol.ts +1 -1
- package/src/runs/shared/completion-guard.ts +36 -5
- package/src/runs/shared/context-mode.ts +44 -0
- package/src/runs/shared/dynamic-fanout.ts +5 -5
- package/src/runs/shared/long-running-guard.ts +4 -0
- package/src/runs/shared/mcp-direct-tool-allowlist.ts +12 -6
- package/src/runs/shared/nested-events.ts +35 -3
- package/src/runs/shared/parallel-handoff.ts +154 -0
- package/src/runs/shared/parallel-utils.ts +11 -0
- package/src/runs/shared/pi-args.ts +148 -56
- package/src/runs/shared/run-history.ts +90 -5
- package/src/runs/shared/session-lease.ts +25 -5
- package/src/runs/shared/structured-output.ts +112 -7
- package/src/runs/shared/subagent-control.ts +4 -0
- package/src/runs/shared/subagent-prompt-runtime.ts +31 -19
- package/src/runs/shared/task-intent.ts +10 -5
- package/src/runs/shared/tool-availability.ts +21 -3
- package/src/runs/shared/tool-budget.ts +11 -5
- package/src/runs/shared/turn-budget.ts +2 -1
- package/src/runs/shared/worktree.ts +63 -14
- package/src/shared/accessible-dir.ts +25 -0
- package/src/shared/artifacts.ts +37 -7
- package/src/shared/atomic-json.ts +14 -42
- package/src/shared/child-transcript.ts +52 -0
- package/src/shared/file-system-retry.ts +47 -0
- package/src/shared/launch-contract.ts +123 -0
- package/src/shared/settings.ts +9 -1
- package/src/shared/types.ts +314 -28
- package/src/shared/utils.ts +17 -42
- package/src/slash/delegation-adapters.ts +153 -6
- package/src/slash/delegation-json.ts +108 -0
- package/src/slash/delegation-request.ts +182 -36
- package/src/slash/prompt-template-bridge.ts +222 -37
- package/src/slash/selector.ts +147 -0
- package/src/slash/slash-commands.ts +15 -6
- package/src/slash/slash-live-state.ts +2 -2
- package/src/slash/subagents-admin.ts +42 -42
- package/src/tui/fleet-status.ts +362 -0
- package/src/tui/fleet-transcript.ts +472 -0
- package/src/tui/fleet.ts +318 -59
- package/src/tui/render.ts +17 -15
- package/src/watchdog/change-signature.ts +105 -12
- package/src/watchdog/review.ts +7 -2
- package/src/watchdog/runtime.ts +5 -3
- package/src/slash/subagents-editor.ts +0 -86
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SUBAGENT_DELEGATION_CANCEL_EVENT,
|
|
3
3
|
SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
4
|
+
SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
4
5
|
SUBAGENT_DELEGATION_REQUEST_EVENT,
|
|
5
6
|
SUBAGENT_DELEGATION_RESPONSE_EVENT,
|
|
6
7
|
SUBAGENT_DELEGATION_STARTED_EVENT,
|
|
7
8
|
SUBAGENT_DELEGATION_UPDATE_EVENT,
|
|
8
9
|
type SubagentDelegationRequest,
|
|
9
10
|
type SubagentDelegationResponse,
|
|
11
|
+
type SubagentDelegationV2InvalidResponse,
|
|
12
|
+
type SubagentDelegationV2Request,
|
|
13
|
+
type SubagentDelegationV2Response,
|
|
10
14
|
} from "../api/delegation.ts";
|
|
11
15
|
import { parseSubagentDelegationRequest } from "./delegation-request.ts";
|
|
12
16
|
import {
|
|
@@ -17,6 +21,9 @@ import {
|
|
|
17
21
|
toSubagentDelegationExecutionParams,
|
|
18
22
|
toSubagentDelegationResponse,
|
|
19
23
|
toSubagentDelegationUpdate,
|
|
24
|
+
toSubagentDelegationV2ExecutionParams,
|
|
25
|
+
toSubagentDelegationV2Response,
|
|
26
|
+
toSubagentDelegationV2Update,
|
|
20
27
|
type DelegatedSubagentExecutionParams,
|
|
21
28
|
type PromptTemplateBridgeResult,
|
|
22
29
|
type PromptTemplateDelegationRequest,
|
|
@@ -44,6 +51,17 @@ interface PromptTemplateBridgeOptions<Ctx extends { cwd?: string }> {
|
|
|
44
51
|
ctx: Ctx,
|
|
45
52
|
onUpdate: (result: PromptTemplateBridgeResult) => void,
|
|
46
53
|
) => Promise<PromptTemplateBridgeResult>;
|
|
54
|
+
/**
|
|
55
|
+
* Concurrent-safe executor for strict versioned delegation requests.
|
|
56
|
+
* Non-versioned prompt-template requests retain the ordinary single-dispatch guard.
|
|
57
|
+
*/
|
|
58
|
+
executeVersioned?: (
|
|
59
|
+
requestId: string,
|
|
60
|
+
params: DelegatedSubagentExecutionParams,
|
|
61
|
+
signal: AbortSignal,
|
|
62
|
+
ctx: Ctx,
|
|
63
|
+
onUpdate: (result: PromptTemplateBridgeResult) => void,
|
|
64
|
+
) => Promise<PromptTemplateBridgeResult>;
|
|
47
65
|
}
|
|
48
66
|
|
|
49
67
|
export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: string }>(
|
|
@@ -52,10 +70,17 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
52
70
|
cancelAll: () => void;
|
|
53
71
|
dispose: () => void;
|
|
54
72
|
} {
|
|
73
|
+
// Legacy and V1 retain requestId-only correlation. V2 attempts are isolated by
|
|
74
|
+
// their complete wire identity, while logical-node ownership is tracked separately.
|
|
55
75
|
const controllers = new Map<string, AbortController>();
|
|
56
76
|
const pendingCancels = new Map<string, true>();
|
|
77
|
+
const v2Controllers = new Map<string, AbortController>();
|
|
78
|
+
const pendingV2Cancels = new Map<string, true>();
|
|
79
|
+
const activeV2Nodes = new Map<string, { attemptKey: string; controller: AbortController }>();
|
|
80
|
+
const settledV2Attempts = new Map<string, true>();
|
|
57
81
|
const subscriptions: Array<() => void> = [];
|
|
58
82
|
let disposed = false;
|
|
83
|
+
let v2IdentitySaturated = false;
|
|
59
84
|
|
|
60
85
|
const subscribe = (event: string, handler: (data: unknown) => void): void => {
|
|
61
86
|
const unsubscribe = options.events.on(event, handler);
|
|
@@ -63,25 +88,63 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
63
88
|
};
|
|
64
89
|
const ownsRequest = (requestId: string, controller: AbortController): boolean =>
|
|
65
90
|
!disposed && controllers.get(requestId) === controller;
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
const ownsV2Attempt = (attemptKey: string, controller: AbortController): boolean =>
|
|
92
|
+
!disposed && v2Controllers.get(attemptKey) === controller;
|
|
93
|
+
const boundedRemember = (map: Map<string, true>, key: string): void => {
|
|
94
|
+
map.delete(key);
|
|
95
|
+
map.set(key, true);
|
|
96
|
+
while (map.size > 256) {
|
|
97
|
+
const oldest = map.keys().next().value;
|
|
71
98
|
if (typeof oldest !== "string") break;
|
|
72
|
-
|
|
99
|
+
map.delete(oldest);
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const rememberV2Identity = (map: Map<string, true>, key: string): void => {
|
|
103
|
+
if (map.has(key) || v2IdentitySaturated) return;
|
|
104
|
+
if (map.size >= 8_192) {
|
|
105
|
+
v2IdentitySaturated = true;
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
map.set(key, true);
|
|
109
|
+
if (map.size === 8_192) {
|
|
110
|
+
// V2 identity facts are security state, not an LRU cache. Saturate as
|
|
111
|
+
// soon as the bounded history fills so no later untracked attempt can
|
|
112
|
+
// start, rather than evicting a cancellation or settled-attempt fact.
|
|
113
|
+
v2IdentitySaturated = true;
|
|
73
114
|
}
|
|
74
115
|
};
|
|
116
|
+
const v2NodeKey = (ownerRunId: string, nodeId: string): string => JSON.stringify([ownerRunId, nodeId]);
|
|
117
|
+
const v2AttemptKey = (requestId: string, ownerRunId: string, nodeId: string): string => JSON.stringify([requestId, ownerRunId, nodeId]);
|
|
118
|
+
const rememberPendingCancel = (requestId: string): void => {
|
|
119
|
+
boundedRemember(pendingCancels, requestId);
|
|
120
|
+
};
|
|
121
|
+
const emitV2Terminal = (attemptKey: string, payload: SubagentDelegationV2Response): void => {
|
|
122
|
+
if (disposed || settledV2Attempts.has(attemptKey)) return;
|
|
123
|
+
rememberV2Identity(settledV2Attempts, attemptKey);
|
|
124
|
+
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, payload);
|
|
125
|
+
};
|
|
75
126
|
|
|
76
127
|
subscribe(PROMPT_TEMPLATE_SUBAGENT_CANCEL_EVENT, (data) => {
|
|
77
128
|
if (!data || typeof data !== "object" || Array.isArray(data)) return;
|
|
78
|
-
|
|
79
|
-
|
|
129
|
+
const value = data as Record<string, unknown>;
|
|
130
|
+
const requestId = value.requestId;
|
|
131
|
+
if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256 || /[\r\n]/.test(requestId)) return;
|
|
132
|
+
if (Object.hasOwn(value, "version")) {
|
|
133
|
+
if (value.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
|
|
134
|
+
if (Object.keys(value).some((key) => key !== "version" && key !== "requestId" && key !== "ownerRunId" && key !== "nodeId")) return;
|
|
135
|
+
const ownerRunId = value.ownerRunId;
|
|
136
|
+
const nodeId = value.nodeId;
|
|
137
|
+
if (typeof ownerRunId !== "string" || !ownerRunId.trim() || ownerRunId.length > 256 || /[\r\n]/.test(ownerRunId)) return;
|
|
138
|
+
if (typeof nodeId !== "string" || !nodeId.trim() || nodeId.length > 256 || /[\r\n]/.test(nodeId)) return;
|
|
139
|
+
const attemptKey = v2AttemptKey(requestId, ownerRunId, nodeId);
|
|
140
|
+
const controller = v2Controllers.get(attemptKey);
|
|
141
|
+
if (controller) controller.abort();
|
|
142
|
+
else rememberV2Identity(pendingV2Cancels, attemptKey);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
80
145
|
if (value.version !== SUBAGENT_DELEGATION_PROTOCOL_VERSION) return;
|
|
81
146
|
if (Object.keys(value).some((key) => key !== "version" && key !== "requestId")) return;
|
|
82
147
|
}
|
|
83
|
-
const requestId = (data as { requestId?: unknown }).requestId;
|
|
84
|
-
if (typeof requestId !== "string" || !requestId.trim() || requestId.length > 256 || /[\r\n]/.test(requestId)) return;
|
|
85
148
|
const controller = controllers.get(requestId);
|
|
86
149
|
if (controller) {
|
|
87
150
|
controller.abort();
|
|
@@ -94,25 +157,50 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
94
157
|
const isVersioned = !!data && typeof data === "object" && Object.hasOwn(data, "version");
|
|
95
158
|
let requestId: string;
|
|
96
159
|
let params: DelegatedSubagentExecutionParams;
|
|
97
|
-
let versionedRequest: SubagentDelegationRequest | undefined;
|
|
160
|
+
let versionedRequest: SubagentDelegationRequest | SubagentDelegationV2Request | undefined;
|
|
161
|
+
let v2Request: SubagentDelegationV2Request | undefined;
|
|
162
|
+
let v2Key: string | undefined;
|
|
98
163
|
let legacyRequest: PromptTemplateDelegationRequest | undefined;
|
|
99
164
|
|
|
100
165
|
if (isVersioned) {
|
|
101
166
|
const parsed = parseSubagentDelegationRequest(data);
|
|
102
167
|
if (parsed.ok === false) {
|
|
103
|
-
if (!disposed && parsed.requestId
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
168
|
+
if (!disposed && parsed.requestId) {
|
|
169
|
+
if (parsed.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
|
|
170
|
+
const payload = {
|
|
171
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
172
|
+
requestId: parsed.requestId,
|
|
173
|
+
...(parsed.ownerRunId ? { ownerRunId: parsed.ownerRunId } : {}),
|
|
174
|
+
...(parsed.nodeId ? { nodeId: parsed.nodeId } : {}),
|
|
175
|
+
status: "invalid_request",
|
|
176
|
+
error: parsed.error,
|
|
177
|
+
} satisfies SubagentDelegationV2InvalidResponse;
|
|
178
|
+
if (parsed.ownerRunId && parsed.nodeId) {
|
|
179
|
+
const attemptKey = v2AttemptKey(parsed.requestId, parsed.ownerRunId, parsed.nodeId);
|
|
180
|
+
if (!v2Controllers.has(attemptKey)) emitV2Terminal(attemptKey, payload);
|
|
181
|
+
} else {
|
|
182
|
+
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, payload);
|
|
183
|
+
}
|
|
184
|
+
} else if (!controllers.has(parsed.requestId)) {
|
|
185
|
+
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
|
|
186
|
+
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
187
|
+
requestId: parsed.requestId,
|
|
188
|
+
status: "invalid_request",
|
|
189
|
+
error: parsed.error,
|
|
190
|
+
} satisfies SubagentDelegationResponse);
|
|
191
|
+
}
|
|
110
192
|
}
|
|
111
193
|
return;
|
|
112
194
|
}
|
|
113
195
|
versionedRequest = parsed.request;
|
|
114
196
|
requestId = parsed.request.requestId;
|
|
115
|
-
|
|
197
|
+
if (parsed.request.version === SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION) {
|
|
198
|
+
v2Request = parsed.request;
|
|
199
|
+
v2Key = v2AttemptKey(requestId, parsed.request.ownerRunId, parsed.request.nodeId);
|
|
200
|
+
params = toSubagentDelegationV2ExecutionParams(parsed.request);
|
|
201
|
+
} else {
|
|
202
|
+
params = toSubagentDelegationExecutionParams(parsed.request);
|
|
203
|
+
}
|
|
116
204
|
} else {
|
|
117
205
|
legacyRequest = parsePromptTemplateRequest(data);
|
|
118
206
|
if (!legacyRequest) return;
|
|
@@ -120,12 +208,56 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
120
208
|
params = toLegacyExecutionParams(legacyRequest);
|
|
121
209
|
}
|
|
122
210
|
|
|
123
|
-
//
|
|
124
|
-
//
|
|
125
|
-
if (controllers.has(requestId)) return;
|
|
211
|
+
// Legacy and V1 keep requestId-only ownership. V2 retransmission and terminal
|
|
212
|
+
// suppression use the full tuple, independently from logical-node ownership.
|
|
213
|
+
if (!v2Request && controllers.has(requestId)) return;
|
|
214
|
+
if (v2Request && v2Key) {
|
|
215
|
+
if (v2Controllers.has(v2Key) || settledV2Attempts.has(v2Key)) return;
|
|
216
|
+
if (pendingV2Cancels.delete(v2Key)) {
|
|
217
|
+
emitV2Terminal(v2Key, {
|
|
218
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
219
|
+
requestId,
|
|
220
|
+
ownerRunId: v2Request.ownerRunId,
|
|
221
|
+
nodeId: v2Request.nodeId,
|
|
222
|
+
status: "cancelled",
|
|
223
|
+
});
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (v2IdentitySaturated) {
|
|
227
|
+
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
|
|
228
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
229
|
+
requestId,
|
|
230
|
+
ownerRunId: v2Request.ownerRunId,
|
|
231
|
+
nodeId: v2Request.nodeId,
|
|
232
|
+
status: "unavailable_context",
|
|
233
|
+
error: "Delegation v2 identity capacity is exhausted for this extension context.",
|
|
234
|
+
} satisfies SubagentDelegationV2Response);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
const active = activeV2Nodes.get(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId));
|
|
238
|
+
if (active) {
|
|
239
|
+
emitV2Terminal(v2Key, {
|
|
240
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
241
|
+
requestId,
|
|
242
|
+
ownerRunId: v2Request.ownerRunId,
|
|
243
|
+
nodeId: v2Request.nodeId,
|
|
244
|
+
status: "duplicate_node",
|
|
245
|
+
});
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
126
249
|
const ctx = options.getContext();
|
|
127
250
|
if (!ctx) {
|
|
128
|
-
if (
|
|
251
|
+
if (v2Request && v2Key) {
|
|
252
|
+
emitV2Terminal(v2Key, {
|
|
253
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
254
|
+
requestId,
|
|
255
|
+
ownerRunId: v2Request.ownerRunId,
|
|
256
|
+
nodeId: v2Request.nodeId,
|
|
257
|
+
status: "unavailable_context",
|
|
258
|
+
error: "No active extension context for delegated subagent execution.",
|
|
259
|
+
});
|
|
260
|
+
} else if (versionedRequest) {
|
|
129
261
|
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
|
|
130
262
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
131
263
|
requestId,
|
|
@@ -144,10 +276,24 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
144
276
|
}
|
|
145
277
|
|
|
146
278
|
const controller = new AbortController();
|
|
147
|
-
|
|
148
|
-
|
|
279
|
+
if (v2Request && v2Key) {
|
|
280
|
+
v2Controllers.set(v2Key, controller);
|
|
281
|
+
activeV2Nodes.set(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId), { attemptKey: v2Key, controller });
|
|
282
|
+
} else {
|
|
283
|
+
controllers.set(requestId, controller);
|
|
284
|
+
if (pendingCancels.delete(requestId)) controller.abort();
|
|
285
|
+
}
|
|
149
286
|
if (controller.signal.aborted) {
|
|
150
|
-
if (
|
|
287
|
+
if (v2Request && v2Key) {
|
|
288
|
+
emitV2Terminal(v2Key, {
|
|
289
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
290
|
+
requestId,
|
|
291
|
+
ownerRunId: v2Request.ownerRunId,
|
|
292
|
+
nodeId: v2Request.nodeId,
|
|
293
|
+
status: "cancelled",
|
|
294
|
+
});
|
|
295
|
+
activeV2Nodes.delete(v2NodeKey(v2Request.ownerRunId, v2Request.nodeId));
|
|
296
|
+
} else if (versionedRequest) {
|
|
151
297
|
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
|
|
152
298
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
153
299
|
requestId,
|
|
@@ -161,25 +307,36 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
161
307
|
errorText: "Delegated prompt cancelled.",
|
|
162
308
|
} satisfies PromptTemplateDelegationResponse);
|
|
163
309
|
}
|
|
164
|
-
|
|
310
|
+
if (v2Key) v2Controllers.delete(v2Key);
|
|
311
|
+
else controllers.delete(requestId);
|
|
165
312
|
return;
|
|
166
313
|
}
|
|
167
314
|
|
|
168
315
|
options.events.emit(
|
|
169
316
|
versionedRequest ? SUBAGENT_DELEGATION_STARTED_EVENT : PROMPT_TEMPLATE_SUBAGENT_STARTED_EVENT,
|
|
170
|
-
|
|
171
|
-
? { version:
|
|
172
|
-
:
|
|
317
|
+
v2Request
|
|
318
|
+
? { version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION, requestId, ownerRunId: v2Request.ownerRunId, nodeId: v2Request.nodeId }
|
|
319
|
+
: versionedRequest
|
|
320
|
+
? { version: SUBAGENT_DELEGATION_PROTOCOL_VERSION, requestId }
|
|
321
|
+
: { requestId },
|
|
173
322
|
);
|
|
174
323
|
|
|
175
324
|
try {
|
|
176
|
-
const
|
|
325
|
+
const executeRequest = versionedRequest && options.executeVersioned
|
|
326
|
+
? options.executeVersioned
|
|
327
|
+
: options.execute;
|
|
328
|
+
const result = await executeRequest(
|
|
177
329
|
requestId,
|
|
178
330
|
params,
|
|
179
331
|
controller.signal,
|
|
180
332
|
ctx,
|
|
181
333
|
(update) => {
|
|
182
|
-
if (!ownsRequest(requestId, controller)) return;
|
|
334
|
+
if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
|
|
335
|
+
if (v2Request) {
|
|
336
|
+
const payload = toSubagentDelegationV2Update(v2Request, update);
|
|
337
|
+
if (payload) options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
183
340
|
if (versionedRequest) {
|
|
184
341
|
const payload = toSubagentDelegationUpdate(requestId, update);
|
|
185
342
|
if (payload) options.events.emit(SUBAGENT_DELEGATION_UPDATE_EVENT, payload);
|
|
@@ -189,8 +346,10 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
189
346
|
if (payload) options.events.emit(PROMPT_TEMPLATE_SUBAGENT_UPDATE_EVENT, payload);
|
|
190
347
|
},
|
|
191
348
|
);
|
|
192
|
-
if (!ownsRequest(requestId, controller)) return;
|
|
193
|
-
if (
|
|
349
|
+
if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
|
|
350
|
+
if (v2Request && v2Key) {
|
|
351
|
+
emitV2Terminal(v2Key, toSubagentDelegationV2Response(v2Request, result, controller.signal.aborted));
|
|
352
|
+
} else if (versionedRequest) {
|
|
194
353
|
options.events.emit(
|
|
195
354
|
SUBAGENT_DELEGATION_RESPONSE_EVENT,
|
|
196
355
|
toSubagentDelegationResponse(requestId, result, controller.signal.aborted),
|
|
@@ -204,8 +363,17 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
204
363
|
);
|
|
205
364
|
}
|
|
206
365
|
} catch (error) {
|
|
207
|
-
if (!ownsRequest(requestId, controller)) return;
|
|
208
|
-
if (
|
|
366
|
+
if (v2Key ? !ownsV2Attempt(v2Key, controller) : !ownsRequest(requestId, controller)) return;
|
|
367
|
+
if (v2Request && v2Key) {
|
|
368
|
+
emitV2Terminal(v2Key, {
|
|
369
|
+
version: SUBAGENT_DELEGATION_V2_PROTOCOL_VERSION,
|
|
370
|
+
requestId,
|
|
371
|
+
ownerRunId: v2Request.ownerRunId,
|
|
372
|
+
nodeId: v2Request.nodeId,
|
|
373
|
+
status: controller.signal.aborted ? "cancelled" : "failed",
|
|
374
|
+
...(controller.signal.aborted ? {} : { error: error instanceof Error ? error.message : String(error) }),
|
|
375
|
+
});
|
|
376
|
+
} else if (versionedRequest) {
|
|
209
377
|
options.events.emit(SUBAGENT_DELEGATION_RESPONSE_EVENT, {
|
|
210
378
|
version: SUBAGENT_DELEGATION_PROTOCOL_VERSION,
|
|
211
379
|
requestId,
|
|
@@ -221,23 +389,40 @@ export function registerPromptTemplateDelegationBridge<Ctx extends { cwd?: strin
|
|
|
221
389
|
} satisfies PromptTemplateDelegationResponse);
|
|
222
390
|
}
|
|
223
391
|
} finally {
|
|
224
|
-
if (
|
|
392
|
+
if (v2Key) {
|
|
393
|
+
if (v2Controllers.get(v2Key) === controller) v2Controllers.delete(v2Key);
|
|
394
|
+
} else if (controllers.get(requestId) === controller) controllers.delete(requestId);
|
|
395
|
+
if (v2Request) {
|
|
396
|
+
const key = v2NodeKey(v2Request.ownerRunId, v2Request.nodeId);
|
|
397
|
+
if (activeV2Nodes.get(key)?.controller === controller) activeV2Nodes.delete(key);
|
|
398
|
+
}
|
|
225
399
|
}
|
|
226
400
|
});
|
|
227
401
|
|
|
228
402
|
return {
|
|
229
403
|
cancelAll: () => {
|
|
230
404
|
for (const controller of controllers.values()) controller.abort();
|
|
405
|
+
for (const controller of v2Controllers.values()) controller.abort();
|
|
231
406
|
controllers.clear();
|
|
407
|
+
v2Controllers.clear();
|
|
232
408
|
pendingCancels.clear();
|
|
409
|
+
pendingV2Cancels.clear();
|
|
410
|
+
activeV2Nodes.clear();
|
|
411
|
+
settledV2Attempts.clear();
|
|
412
|
+
v2IdentitySaturated = false;
|
|
233
413
|
},
|
|
234
414
|
dispose: () => {
|
|
235
415
|
disposed = true;
|
|
236
416
|
for (const controller of controllers.values()) controller.abort();
|
|
417
|
+
for (const controller of v2Controllers.values()) controller.abort();
|
|
237
418
|
controllers.clear();
|
|
419
|
+
v2Controllers.clear();
|
|
238
420
|
for (const unsubscribe of subscriptions) unsubscribe();
|
|
239
421
|
subscriptions.length = 0;
|
|
240
422
|
pendingCancels.clear();
|
|
423
|
+
pendingV2Cancels.clear();
|
|
424
|
+
activeV2Nodes.clear();
|
|
425
|
+
settledV2Attempts.clear();
|
|
241
426
|
},
|
|
242
427
|
};
|
|
243
428
|
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { DynamicBorder, keyHint, rawKeyHint, type Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { Container, fuzzyFilter, Input, type KeybindingsManager, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
|
|
3
|
+
|
|
4
|
+
/** A single selectable row. `value` is returned on confirm; `label` is the primary text. */
|
|
5
|
+
export interface SelectorItem {
|
|
6
|
+
value: string;
|
|
7
|
+
label: string;
|
|
8
|
+
/** Optional dimmed badge shown after the label (e.g. a provider name). */
|
|
9
|
+
badge?: string;
|
|
10
|
+
/** Marks the current selection (rendered with a ✓ checkmark). */
|
|
11
|
+
current?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SelectorResult {
|
|
15
|
+
confirmed: boolean;
|
|
16
|
+
value?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface SelectorOptions {
|
|
20
|
+
title: string;
|
|
21
|
+
/** Optional dimmed hint line under the title (e.g. the current value). */
|
|
22
|
+
subtitle?: string;
|
|
23
|
+
items: SelectorItem[];
|
|
24
|
+
done: (result: SelectorResult) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const MAX_VISIBLE = 10;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* A single-select list with a search field and a bounded scroll window that keeps the
|
|
31
|
+
* highlighted row on screen with an `(n/total)` indicator, so the selection never scrolls
|
|
32
|
+
* out of view when the option list is long. Composed from pi's own TUI primitives so it
|
|
33
|
+
* matches the built-in `/model` picker.
|
|
34
|
+
*
|
|
35
|
+
* Colors come from the `theme` passed to the factory: the module-level `theme` singleton
|
|
36
|
+
* is undefined under the extension's jiti module cache.
|
|
37
|
+
*/
|
|
38
|
+
export class SelectorComponent extends Container {
|
|
39
|
+
private readonly tui: TUI;
|
|
40
|
+
private readonly theme: Theme;
|
|
41
|
+
private readonly keybindings: KeybindingsManager;
|
|
42
|
+
private readonly items: SelectorItem[];
|
|
43
|
+
private readonly done: (result: SelectorResult) => void;
|
|
44
|
+
|
|
45
|
+
private readonly searchInput: Input;
|
|
46
|
+
private readonly listContainer: Container;
|
|
47
|
+
private filtered: SelectorItem[];
|
|
48
|
+
private selectedIndex = 0;
|
|
49
|
+
|
|
50
|
+
constructor(tui: TUI, theme: Theme, keybindings: KeybindingsManager, options: SelectorOptions) {
|
|
51
|
+
super();
|
|
52
|
+
this.tui = tui;
|
|
53
|
+
this.theme = theme;
|
|
54
|
+
this.keybindings = keybindings;
|
|
55
|
+
this.items = options.items;
|
|
56
|
+
this.done = options.done;
|
|
57
|
+
this.filtered = [...this.items];
|
|
58
|
+
|
|
59
|
+
const border = () => new DynamicBorder((str) => theme.fg("border", str));
|
|
60
|
+
this.addChild(border());
|
|
61
|
+
this.addChild(new Spacer(1));
|
|
62
|
+
this.addChild(new Text(theme.fg("accent", theme.bold(options.title)), 0, 0));
|
|
63
|
+
if (options.subtitle) this.addChild(new Text(theme.fg("muted", options.subtitle), 0, 0));
|
|
64
|
+
this.addChild(new Spacer(1));
|
|
65
|
+
|
|
66
|
+
this.searchInput = new Input();
|
|
67
|
+
this.searchInput.focused = true;
|
|
68
|
+
this.searchInput.onSubmit = () => this.confirmSelection();
|
|
69
|
+
this.addChild(this.searchInput);
|
|
70
|
+
this.addChild(new Spacer(1));
|
|
71
|
+
|
|
72
|
+
this.listContainer = new Container();
|
|
73
|
+
this.addChild(this.listContainer);
|
|
74
|
+
this.addChild(new Spacer(1));
|
|
75
|
+
|
|
76
|
+
this.addChild(new Text(rawKeyHint("↑↓", "navigate") + " " + keyHint("tui.select.confirm", "select") + " " + keyHint("tui.select.cancel", "cancel"), 0, 0));
|
|
77
|
+
this.addChild(new Spacer(1));
|
|
78
|
+
this.addChild(border());
|
|
79
|
+
|
|
80
|
+
const currentIndex = this.filtered.findIndex((item) => item.current);
|
|
81
|
+
if (currentIndex >= 0) this.selectedIndex = currentIndex;
|
|
82
|
+
this.updateList();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
private applyFilter(query: string): void {
|
|
86
|
+
const previous = this.filtered[this.selectedIndex]?.value;
|
|
87
|
+
this.filtered = query
|
|
88
|
+
? fuzzyFilter(this.items, query, (item) => `${item.label} ${item.value} ${item.badge ?? ""}`)
|
|
89
|
+
: [...this.items];
|
|
90
|
+
const keepIndex = this.filtered.findIndex((item) => item.value === previous);
|
|
91
|
+
this.selectedIndex = keepIndex >= 0 ? keepIndex : Math.min(this.selectedIndex, Math.max(0, this.filtered.length - 1));
|
|
92
|
+
this.updateList();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
private updateList(): void {
|
|
96
|
+
const th = this.theme;
|
|
97
|
+
this.listContainer.clear();
|
|
98
|
+
const startIndex = Math.max(0, Math.min(this.selectedIndex - Math.floor(MAX_VISIBLE / 2), this.filtered.length - MAX_VISIBLE));
|
|
99
|
+
const endIndex = Math.min(startIndex + MAX_VISIBLE, this.filtered.length);
|
|
100
|
+
|
|
101
|
+
for (let i = startIndex; i < endIndex; i++) {
|
|
102
|
+
const item = this.filtered[i]!;
|
|
103
|
+
const isSelected = i === this.selectedIndex;
|
|
104
|
+
const badge = item.badge ? ` ${th.fg("muted", `[${item.badge}]`)}` : "";
|
|
105
|
+
const checkmark = item.current ? th.fg("success", " ✓") : "";
|
|
106
|
+
const line = isSelected
|
|
107
|
+
? `${th.fg("accent", "→ ")}${th.fg("accent", item.label)}${badge}${checkmark}`
|
|
108
|
+
: ` ${item.label}${badge}${checkmark}`;
|
|
109
|
+
this.listContainer.addChild(new Text(line, 0, 0));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (startIndex > 0 || endIndex < this.filtered.length) {
|
|
113
|
+
this.listContainer.addChild(new Text(th.fg("muted", ` (${this.selectedIndex + 1}/${this.filtered.length})`), 0, 0));
|
|
114
|
+
}
|
|
115
|
+
if (this.filtered.length === 0) {
|
|
116
|
+
this.listContainer.addChild(new Text(th.fg("muted", " No matching entries"), 0, 0));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private confirmSelection(): void {
|
|
121
|
+
const selected = this.filtered[this.selectedIndex];
|
|
122
|
+
this.done(selected ? { confirmed: true, value: selected.value } : { confirmed: false });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
handleInput(keyData: string): void {
|
|
126
|
+
const kb = this.keybindings;
|
|
127
|
+
if (kb.matches(keyData, "tui.select.up")) {
|
|
128
|
+
if (this.filtered.length === 0) return;
|
|
129
|
+
this.selectedIndex = this.selectedIndex === 0 ? this.filtered.length - 1 : this.selectedIndex - 1;
|
|
130
|
+
this.updateList();
|
|
131
|
+
this.tui.requestRender();
|
|
132
|
+
} else if (kb.matches(keyData, "tui.select.down")) {
|
|
133
|
+
if (this.filtered.length === 0) return;
|
|
134
|
+
this.selectedIndex = this.selectedIndex === this.filtered.length - 1 ? 0 : this.selectedIndex + 1;
|
|
135
|
+
this.updateList();
|
|
136
|
+
this.tui.requestRender();
|
|
137
|
+
} else if (kb.matches(keyData, "tui.select.confirm")) {
|
|
138
|
+
this.confirmSelection();
|
|
139
|
+
} else if (kb.matches(keyData, "tui.select.cancel")) {
|
|
140
|
+
this.done({ confirmed: false });
|
|
141
|
+
} else {
|
|
142
|
+
this.searchInput.handleInput(keyData);
|
|
143
|
+
this.applyFilter(this.searchInput.getValue());
|
|
144
|
+
this.tui.requestRender();
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -737,6 +737,13 @@ async function runSlashSubagent(
|
|
|
737
737
|
}
|
|
738
738
|
}
|
|
739
739
|
|
|
740
|
+
function launchSlashSubagent(
|
|
741
|
+
pi: ExtensionAPI,
|
|
742
|
+
ctx: ExtensionContext,
|
|
743
|
+
params: SubagentParamsLike,
|
|
744
|
+
): void {
|
|
745
|
+
void runSlashSubagent(pi, ctx, params);
|
|
746
|
+
}
|
|
740
747
|
|
|
741
748
|
export interface GroupConfig { concurrency?: number; failFast?: boolean; worktree?: boolean }
|
|
742
749
|
export interface ParsedStep { kind: "step"; name: string; config: InlineConfig; task?: string }
|
|
@@ -1001,7 +1008,7 @@ function validateInlineAcceptanceInput(value: string, agent: string): void {
|
|
|
1001
1008
|
const errors = validateAcceptanceInput(value, `acceptance for step '${agent}'`);
|
|
1002
1009
|
if (errors.length > 0) throw new SlashParseError(errors[0]!);
|
|
1003
1010
|
if (!INLINE_ACCEPTANCE_LEVELS.has(value)) {
|
|
1004
|
-
throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none or
|
|
1011
|
+
throw new SlashParseError(`Inline acceptance for step '${agent}' supports auto, attested, or checked. Use the subagent tool API or a saved .chain.json file for none, verified, or review requirements; reviewed is an achieved status, not an input level.`);
|
|
1005
1012
|
}
|
|
1006
1013
|
}
|
|
1007
1014
|
|
|
@@ -1189,7 +1196,7 @@ export function registerSlashCommands(
|
|
|
1189
1196
|
if (inline.model) params.model = inline.model;
|
|
1190
1197
|
if (bg) params.async = true;
|
|
1191
1198
|
if (fork) params.context = "fork";
|
|
1192
|
-
|
|
1199
|
+
launchSlashSubagent(pi, ctx, params);
|
|
1193
1200
|
},
|
|
1194
1201
|
});
|
|
1195
1202
|
|
|
@@ -1203,7 +1210,7 @@ export function registerSlashCommands(
|
|
|
1203
1210
|
const params: SubagentParamsLike = { chain: built.chain, task: built.task, clarify: false, agentScope: "both" };
|
|
1204
1211
|
if (bg) params.async = true;
|
|
1205
1212
|
if (fork) params.context = "fork";
|
|
1206
|
-
|
|
1213
|
+
launchSlashSubagent(pi, ctx, params);
|
|
1207
1214
|
},
|
|
1208
1215
|
});
|
|
1209
1216
|
|
|
@@ -1233,7 +1240,7 @@ export function registerSlashCommands(
|
|
|
1233
1240
|
const params: SubagentParamsLike = { chain: mapSavedChainSteps(chain), task, clarify: false, agentScope: "both" };
|
|
1234
1241
|
if (bg) params.async = true;
|
|
1235
1242
|
if (fork) params.context = "fork";
|
|
1236
|
-
|
|
1243
|
+
launchSlashSubagent(pi, ctx, params);
|
|
1237
1244
|
},
|
|
1238
1245
|
});
|
|
1239
1246
|
|
|
@@ -1257,7 +1264,7 @@ export function registerSlashCommands(
|
|
|
1257
1264
|
const params: SubagentParamsLike = { tasks, clarify: false, agentScope: "both" };
|
|
1258
1265
|
if (bg) params.async = true;
|
|
1259
1266
|
if (fork) params.context = "fork";
|
|
1260
|
-
|
|
1267
|
+
launchSlashSubagent(pi, ctx, params);
|
|
1261
1268
|
},
|
|
1262
1269
|
});
|
|
1263
1270
|
|
|
@@ -1331,7 +1338,9 @@ export function registerSlashCommands(
|
|
|
1331
1338
|
|
|
1332
1339
|
registerPromptWorkflowCommands({
|
|
1333
1340
|
pi,
|
|
1334
|
-
run: (params, ctx) =>
|
|
1341
|
+
run: async (params, ctx) => {
|
|
1342
|
+
launchSlashSubagent(pi, ctx, params);
|
|
1343
|
+
},
|
|
1335
1344
|
});
|
|
1336
1345
|
|
|
1337
1346
|
pi.registerCommand("subagents-models", {
|
|
@@ -193,8 +193,8 @@ function cloneResultsWithProgress(
|
|
|
193
193
|
progress: NonNullable<Details["progress"]> | undefined,
|
|
194
194
|
): SingleResult[] {
|
|
195
195
|
return results.map((result, index) => {
|
|
196
|
-
const nextProgress = progress?.find((entry) => entry.index === index
|
|
197
|
-
|
|
196
|
+
const nextProgress = progress?.find((entry) => entry.index === index
|
|
197
|
+
|| (entry.index === undefined && entry.agent === result.agent))
|
|
198
198
|
?? result.progress;
|
|
199
199
|
return nextProgress ? { ...result, progress: nextProgress } : result;
|
|
200
200
|
});
|