pi-subagents 0.32.0 → 0.33.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 +30 -3
- package/README.md +147 -58
- package/install.mjs +2 -1
- package/package.json +3 -2
- package/skills/pi-subagents/SKILL.md +70 -14
- package/src/agents/agent-management.ts +177 -5
- package/src/agents/agent-memory.ts +254 -0
- package/src/agents/agent-serializer.ts +11 -0
- package/src/agents/agents.ts +142 -12
- package/src/agents/chain-serializer.ts +27 -2
- package/src/extension/doctor.ts +1 -9
- package/src/extension/fanout-child.ts +2 -2
- package/src/extension/index.ts +65 -90
- package/src/extension/rpc.ts +369 -0
- package/src/extension/schemas.ts +52 -8
- package/src/extension/tool-description.ts +200 -0
- package/src/intercom/intercom-bridge.ts +21 -253
- package/src/intercom/native-supervisor-channel.ts +510 -0
- package/src/runs/background/async-execution.ts +51 -7
- package/src/runs/background/async-job-tracker.ts +12 -2
- package/src/runs/background/async-status.ts +27 -2
- package/src/runs/background/completion-batcher.ts +166 -0
- package/src/runs/background/control-channel.ts +106 -1
- package/src/runs/background/fleet-view.ts +515 -0
- package/src/runs/background/notify.ts +161 -44
- package/src/runs/background/result-watcher.ts +1 -2
- package/src/runs/background/run-id-resolver.ts +3 -2
- package/src/runs/background/run-status.ts +166 -6
- package/src/runs/background/scheduled-runs.ts +514 -0
- package/src/runs/background/subagent-runner.ts +409 -35
- package/src/runs/background/wait.ts +353 -0
- package/src/runs/foreground/chain-execution.ts +95 -21
- package/src/runs/foreground/execution.ts +150 -21
- package/src/runs/foreground/subagent-executor.ts +378 -64
- package/src/runs/shared/dynamic-fanout.ts +1 -1
- package/src/runs/shared/model-fallback.ts +167 -20
- package/src/runs/shared/model-scope.ts +128 -0
- package/src/runs/shared/nested-events.ts +31 -0
- package/src/runs/shared/parallel-utils.ts +1 -0
- package/src/runs/shared/pi-args.ts +30 -1
- package/src/runs/shared/subagent-prompt-runtime.ts +108 -2
- package/src/runs/shared/tool-budget.ts +74 -0
- package/src/runs/shared/turn-budget.ts +52 -0
- package/src/shared/artifacts.ts +1 -0
- package/src/shared/atomic-json.ts +15 -2
- package/src/shared/child-transcript.ts +212 -0
- package/src/shared/settings.ts +3 -1
- package/src/shared/types.ts +134 -19
- package/src/slash/prompt-workflows.ts +330 -0
- package/src/slash/slash-commands.ts +16 -2
- package/src/tui/render.ts +16 -8
- package/src/extension/companion-suggestions.ts +0 -359
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
3
|
+
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { Compile } from "typebox/compile";
|
|
5
|
+
import { resolveAsyncRunLocation } from "../runs/background/async-resume.ts";
|
|
6
|
+
import { deliverTimeoutRequest } from "../runs/background/control-channel.ts";
|
|
7
|
+
import { reconcileAsyncRun } from "../runs/background/stale-run-reconciler.ts";
|
|
8
|
+
import type { SubagentParamsLike } from "../runs/foreground/subagent-executor.ts";
|
|
9
|
+
import { type Details, ASYNC_DIR, RESULTS_DIR } from "../shared/types.ts";
|
|
10
|
+
import { readStatus } from "../shared/utils.ts";
|
|
11
|
+
import { SubagentParams } from "./schemas.ts";
|
|
12
|
+
|
|
13
|
+
export const SUBAGENT_RPC_PROTOCOL_VERSION = 1;
|
|
14
|
+
export const SUBAGENT_RPC_REQUEST_EVENT = "subagents:rpc:v1:request";
|
|
15
|
+
export const SUBAGENT_RPC_READY_EVENT = "subagents:rpc:v1:ready";
|
|
16
|
+
export const SUBAGENT_RPC_REPLY_EVENT_PREFIX = "subagents:rpc:v1:reply:";
|
|
17
|
+
|
|
18
|
+
export const SUBAGENT_RPC_METHODS = ["ping", "status", "spawn", "interrupt", "stop"] as const;
|
|
19
|
+
export type SubagentRpcMethod = typeof SUBAGENT_RPC_METHODS[number];
|
|
20
|
+
|
|
21
|
+
export interface SubagentRpcRequestEnvelope {
|
|
22
|
+
version: typeof SUBAGENT_RPC_PROTOCOL_VERSION;
|
|
23
|
+
requestId: string;
|
|
24
|
+
method: SubagentRpcMethod;
|
|
25
|
+
params?: unknown;
|
|
26
|
+
source?: {
|
|
27
|
+
extension?: string;
|
|
28
|
+
[key: string]: unknown;
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export type SubagentRpcReplyEnvelope<T = unknown> = {
|
|
33
|
+
version: typeof SUBAGENT_RPC_PROTOCOL_VERSION;
|
|
34
|
+
requestId: string;
|
|
35
|
+
method?: SubagentRpcMethod;
|
|
36
|
+
success: true;
|
|
37
|
+
data: T;
|
|
38
|
+
} | {
|
|
39
|
+
version: typeof SUBAGENT_RPC_PROTOCOL_VERSION;
|
|
40
|
+
requestId: string;
|
|
41
|
+
method?: SubagentRpcMethod;
|
|
42
|
+
success: false;
|
|
43
|
+
error: {
|
|
44
|
+
code: SubagentRpcErrorCode;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
type SubagentRpcErrorCode =
|
|
50
|
+
| "invalid_request"
|
|
51
|
+
| "invalid_params"
|
|
52
|
+
| "unsupported_version"
|
|
53
|
+
| "unsupported_method"
|
|
54
|
+
| "no_active_session"
|
|
55
|
+
| "execution_failed"
|
|
56
|
+
| "not_found"
|
|
57
|
+
| "invalid_state";
|
|
58
|
+
|
|
59
|
+
interface EventBus {
|
|
60
|
+
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
61
|
+
emit(event: string, data: unknown): void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface RegisterSubagentRpcBridgeOptions {
|
|
65
|
+
events: EventBus;
|
|
66
|
+
getContext: () => ExtensionContext | null;
|
|
67
|
+
execute: (
|
|
68
|
+
id: string,
|
|
69
|
+
params: SubagentParamsLike,
|
|
70
|
+
signal: AbortSignal,
|
|
71
|
+
onUpdate: ((result: AgentToolResult<Details>) => void) | undefined,
|
|
72
|
+
ctx: ExtensionContext,
|
|
73
|
+
) => Promise<AgentToolResult<Details>>;
|
|
74
|
+
asyncDirRoot?: string;
|
|
75
|
+
resultsDir?: string;
|
|
76
|
+
kill?: (pid: number, signal?: NodeJS.Signals | 0) => boolean;
|
|
77
|
+
now?: () => number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
class SubagentRpcError extends Error {
|
|
81
|
+
readonly code: SubagentRpcErrorCode;
|
|
82
|
+
|
|
83
|
+
constructor(code: SubagentRpcErrorCode, message: string) {
|
|
84
|
+
super(message);
|
|
85
|
+
this.name = "SubagentRpcError";
|
|
86
|
+
this.code = code;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const subagentParamsValidator = Compile(SubagentParams);
|
|
91
|
+
|
|
92
|
+
export function subagentRpcReplyEvent(requestId: string): string {
|
|
93
|
+
return `${SUBAGENT_RPC_REPLY_EVENT_PREFIX}${requestId}`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
97
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function assertRequestId(value: unknown): string {
|
|
101
|
+
if (typeof value !== "string" || value.trim().length === 0 || /[\r\n]/.test(value)) {
|
|
102
|
+
throw new SubagentRpcError("invalid_request", "RPC requestId must be a non-empty string without newlines.");
|
|
103
|
+
}
|
|
104
|
+
return value;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function assertRecordParams(params: unknown, method: SubagentRpcMethod): Record<string, unknown> {
|
|
108
|
+
if (params === undefined) return {};
|
|
109
|
+
if (!isRecord(params)) throw new SubagentRpcError("invalid_params", `RPC ${method} params must be an object.`);
|
|
110
|
+
return params;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function assertSubagentParams(params: SubagentParamsLike, label: string): void {
|
|
114
|
+
if (subagentParamsValidator.Check(params)) return;
|
|
115
|
+
const messages = [...subagentParamsValidator.Errors(params)]
|
|
116
|
+
.slice(0, 4)
|
|
117
|
+
.map((error) => error.message);
|
|
118
|
+
throw new SubagentRpcError("invalid_params", `${label}: ${messages.join("; ") || "invalid subagent parameters"}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function textFromToolResult(result: AgentToolResult<Details>): string {
|
|
122
|
+
return result.content
|
|
123
|
+
.filter((part): part is { type: "text"; text: string } => part.type === "text")
|
|
124
|
+
.map((part) => part.text)
|
|
125
|
+
.join("\n");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function dataFromToolResult(result: AgentToolResult<Details>): { text: string; details?: Details; isError?: boolean } {
|
|
129
|
+
return {
|
|
130
|
+
text: textFromToolResult(result),
|
|
131
|
+
...(result.details ? { details: result.details } : {}),
|
|
132
|
+
...(result.isError ? { isError: true } : {}),
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function failIfToolError(result: AgentToolResult<Details>): void {
|
|
137
|
+
if (!result.isError) return;
|
|
138
|
+
throw new SubagentRpcError("execution_failed", textFromToolResult(result) || "Subagent RPC execution failed.");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function normalizeTargetParams(params: unknown, method: SubagentRpcMethod): Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> {
|
|
142
|
+
const input = assertRecordParams(params, method);
|
|
143
|
+
const output: Pick<SubagentParamsLike, "id" | "runId" | "dir" | "index"> = {};
|
|
144
|
+
if (input.id !== undefined) output.id = input.id as string;
|
|
145
|
+
if (input.runId !== undefined) output.runId = input.runId as string;
|
|
146
|
+
if (input.dir !== undefined) output.dir = input.dir as string;
|
|
147
|
+
if (input.index !== undefined) output.index = input.index as number;
|
|
148
|
+
return output;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function sessionData(ctx: ExtensionContext | null): { cwd?: string; sessionId?: string; sessionFile?: string | null } {
|
|
152
|
+
if (!ctx) return {};
|
|
153
|
+
return {
|
|
154
|
+
cwd: ctx.cwd,
|
|
155
|
+
sessionId: ctx.sessionManager.getSessionId() ?? undefined,
|
|
156
|
+
sessionFile: ctx.sessionManager.getSessionFile() ?? null,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function pingData(ctx: ExtensionContext | null) {
|
|
161
|
+
return {
|
|
162
|
+
version: SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
163
|
+
methods: [...SUBAGENT_RPC_METHODS],
|
|
164
|
+
capabilities: {
|
|
165
|
+
status: true,
|
|
166
|
+
asyncSpawn: true,
|
|
167
|
+
interrupt: true,
|
|
168
|
+
stop: true,
|
|
169
|
+
},
|
|
170
|
+
events: {
|
|
171
|
+
ready: SUBAGENT_RPC_READY_EVENT,
|
|
172
|
+
request: SUBAGENT_RPC_REQUEST_EVENT,
|
|
173
|
+
replyPrefix: SUBAGENT_RPC_REPLY_EVENT_PREFIX,
|
|
174
|
+
},
|
|
175
|
+
session: sessionData(ctx),
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function executeChecked(
|
|
180
|
+
options: RegisterSubagentRpcBridgeOptions,
|
|
181
|
+
ctx: ExtensionContext,
|
|
182
|
+
requestId: string,
|
|
183
|
+
method: SubagentRpcMethod,
|
|
184
|
+
params: SubagentParamsLike,
|
|
185
|
+
): Promise<{ text: string; details?: Details; isError?: boolean }> {
|
|
186
|
+
assertSubagentParams(params, `RPC ${method} params`);
|
|
187
|
+
const controller = new AbortController();
|
|
188
|
+
const result = await options.execute(`rpc-${method}-${requestId}`, params, controller.signal, undefined, ctx);
|
|
189
|
+
failIfToolError(result);
|
|
190
|
+
return dataFromToolResult(result);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function spawnParams(params: unknown): SubagentParamsLike {
|
|
194
|
+
const input = assertRecordParams(params, "spawn");
|
|
195
|
+
if (input.action !== undefined) {
|
|
196
|
+
throw new SubagentRpcError("invalid_params", "RPC spawn does not accept management/control actions. Use status or interrupt RPC methods instead.");
|
|
197
|
+
}
|
|
198
|
+
if (input.async === false) {
|
|
199
|
+
throw new SubagentRpcError("invalid_params", "RPC spawn only supports detached async launches; omit async or set async: true.");
|
|
200
|
+
}
|
|
201
|
+
if (input.clarify === true) {
|
|
202
|
+
throw new SubagentRpcError("invalid_params", "RPC spawn cannot open the clarify UI; omit clarify or set clarify: false.");
|
|
203
|
+
}
|
|
204
|
+
return { ...(input as SubagentParamsLike), async: true, clarify: false };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function stopAsyncRun(
|
|
208
|
+
params: unknown,
|
|
209
|
+
options: RegisterSubagentRpcBridgeOptions,
|
|
210
|
+
ctx: ExtensionContext,
|
|
211
|
+
): { runId: string; asyncDir: string; previousState: string; state: "stopping"; message: string } {
|
|
212
|
+
const target = normalizeTargetParams(params, "stop");
|
|
213
|
+
assertSubagentParams({ action: "status", ...target }, "RPC stop target params");
|
|
214
|
+
const asyncDirRoot = options.asyncDirRoot ?? ASYNC_DIR;
|
|
215
|
+
const resultsDir = options.resultsDir ?? RESULTS_DIR;
|
|
216
|
+
let location;
|
|
217
|
+
try {
|
|
218
|
+
location = resolveAsyncRunLocation(target, asyncDirRoot, resultsDir);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
throw new SubagentRpcError("invalid_params", error instanceof Error ? error.message : String(error));
|
|
221
|
+
}
|
|
222
|
+
if (!location.asyncDir) {
|
|
223
|
+
throw new SubagentRpcError("not_found", "Async run not found or already completed; stop requires a live async run directory.");
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const currentSessionId = ctx.sessionManager.getSessionId();
|
|
227
|
+
const initialStatus = readStatus(location.asyncDir);
|
|
228
|
+
const initialRunId = initialStatus?.runId ?? location.resolvedId ?? path.basename(location.asyncDir);
|
|
229
|
+
if (!initialStatus) throw new SubagentRpcError("not_found", `Status file not found for async run '${initialRunId}'.`);
|
|
230
|
+
if (!currentSessionId || initialStatus.sessionId !== currentSessionId) {
|
|
231
|
+
throw new SubagentRpcError("not_found", `Async run '${initialRunId}' was not found in the active session.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
let status;
|
|
235
|
+
try {
|
|
236
|
+
status = reconcileAsyncRun(location.asyncDir, { resultsDir, kill: options.kill, now: options.now }).status;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
239
|
+
}
|
|
240
|
+
const runId = status?.runId ?? initialRunId;
|
|
241
|
+
if (!status) throw new SubagentRpcError("not_found", `Status file not found for async run '${runId}'.`);
|
|
242
|
+
if (status.sessionId !== currentSessionId) {
|
|
243
|
+
throw new SubagentRpcError("not_found", `Async run '${runId}' was not found in the active session.`);
|
|
244
|
+
}
|
|
245
|
+
if (status.state !== "running") {
|
|
246
|
+
throw new SubagentRpcError("invalid_state", `Async run ${runId} is ${status.state}; stop only supports running async runs.`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
deliverTimeoutRequest({
|
|
251
|
+
asyncDir: location.asyncDir,
|
|
252
|
+
pid: status.pid,
|
|
253
|
+
kill: options.kill,
|
|
254
|
+
now: options.now,
|
|
255
|
+
source: "rpc-stop",
|
|
256
|
+
});
|
|
257
|
+
} catch (error) {
|
|
258
|
+
throw new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
runId,
|
|
263
|
+
asyncDir: location.asyncDir,
|
|
264
|
+
previousState: status.state,
|
|
265
|
+
state: "stopping",
|
|
266
|
+
message: `Stop requested for async run ${runId}.`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function handleRequest(
|
|
271
|
+
request: SubagentRpcRequestEnvelope,
|
|
272
|
+
options: RegisterSubagentRpcBridgeOptions,
|
|
273
|
+
): Promise<unknown> {
|
|
274
|
+
const ctx = options.getContext();
|
|
275
|
+
if (request.method === "ping") return pingData(ctx);
|
|
276
|
+
if (!ctx) throw new SubagentRpcError("no_active_session", "No active extension context for subagent RPC.");
|
|
277
|
+
|
|
278
|
+
if (request.method === "spawn") {
|
|
279
|
+
return executeChecked(options, ctx, request.requestId, request.method, spawnParams(request.params));
|
|
280
|
+
}
|
|
281
|
+
if (request.method === "status") {
|
|
282
|
+
return executeChecked(options, ctx, request.requestId, request.method, { action: "status", ...normalizeTargetParams(request.params, "status") });
|
|
283
|
+
}
|
|
284
|
+
if (request.method === "interrupt") {
|
|
285
|
+
return executeChecked(options, ctx, request.requestId, request.method, { action: "interrupt", ...normalizeTargetParams(request.params, "interrupt") });
|
|
286
|
+
}
|
|
287
|
+
if (request.method === "stop") {
|
|
288
|
+
return stopAsyncRun(request.params, options, ctx);
|
|
289
|
+
}
|
|
290
|
+
throw new SubagentRpcError("unsupported_method", `Unsupported subagent RPC method: ${String(request.method)}`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function parseRequest(raw: unknown): SubagentRpcRequestEnvelope {
|
|
294
|
+
if (!isRecord(raw)) throw new SubagentRpcError("invalid_request", "Subagent RPC request must be an object.");
|
|
295
|
+
const requestId = assertRequestId(raw.requestId);
|
|
296
|
+
if (raw.version !== SUBAGENT_RPC_PROTOCOL_VERSION) {
|
|
297
|
+
throw new SubagentRpcError("unsupported_version", `Unsupported subagent RPC version: ${String(raw.version)}.`);
|
|
298
|
+
}
|
|
299
|
+
if (typeof raw.method !== "string" || !(SUBAGENT_RPC_METHODS as readonly string[]).includes(raw.method)) {
|
|
300
|
+
throw new SubagentRpcError("unsupported_method", `Unsupported subagent RPC method: ${String(raw.method)}.`);
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
version: SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
304
|
+
requestId,
|
|
305
|
+
method: raw.method as SubagentRpcMethod,
|
|
306
|
+
...(raw.params !== undefined ? { params: raw.params } : {}),
|
|
307
|
+
...(isRecord(raw.source) ? { source: raw.source as SubagentRpcRequestEnvelope["source"] } : {}),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function safeReplyRequestId(raw: unknown): string {
|
|
312
|
+
if (!isRecord(raw)) return "unknown";
|
|
313
|
+
const requestId = raw.requestId;
|
|
314
|
+
return typeof requestId === "string" && requestId.trim().length > 0 && !/[\r\n]/.test(requestId)
|
|
315
|
+
? requestId
|
|
316
|
+
: "unknown";
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function errorReply(raw: unknown, error: unknown): SubagentRpcReplyEnvelope {
|
|
320
|
+
const requestId = safeReplyRequestId(raw);
|
|
321
|
+
const method = isRecord(raw) && typeof raw.method === "string" && (SUBAGENT_RPC_METHODS as readonly string[]).includes(raw.method)
|
|
322
|
+
? raw.method as SubagentRpcMethod
|
|
323
|
+
: undefined;
|
|
324
|
+
const rpcError = error instanceof SubagentRpcError
|
|
325
|
+
? error
|
|
326
|
+
: new SubagentRpcError("execution_failed", error instanceof Error ? error.message : String(error));
|
|
327
|
+
return {
|
|
328
|
+
version: SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
329
|
+
requestId,
|
|
330
|
+
...(method ? { method } : {}),
|
|
331
|
+
success: false,
|
|
332
|
+
error: {
|
|
333
|
+
code: rpcError.code,
|
|
334
|
+
message: rpcError.message,
|
|
335
|
+
},
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function registerSubagentRpcBridge(options: RegisterSubagentRpcBridgeOptions): {
|
|
340
|
+
emitReady: (ctx?: ExtensionContext | null) => void;
|
|
341
|
+
dispose: () => void;
|
|
342
|
+
} {
|
|
343
|
+
const unsubscribe = options.events.on(SUBAGENT_RPC_REQUEST_EVENT, async (raw) => {
|
|
344
|
+
let request: SubagentRpcRequestEnvelope | undefined;
|
|
345
|
+
try {
|
|
346
|
+
request = parseRequest(raw);
|
|
347
|
+
const data = await handleRequest(request, options);
|
|
348
|
+
options.events.emit(subagentRpcReplyEvent(request.requestId), {
|
|
349
|
+
version: SUBAGENT_RPC_PROTOCOL_VERSION,
|
|
350
|
+
requestId: request.requestId,
|
|
351
|
+
method: request.method,
|
|
352
|
+
success: true,
|
|
353
|
+
data,
|
|
354
|
+
} satisfies SubagentRpcReplyEnvelope);
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const reply = errorReply(request ?? raw, error);
|
|
357
|
+
options.events.emit(subagentRpcReplyEvent(reply.requestId), reply);
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
return {
|
|
362
|
+
emitReady: (ctx) => {
|
|
363
|
+
options.events.emit(SUBAGENT_RPC_READY_EVENT, pingData(ctx ?? options.getContext()));
|
|
364
|
+
},
|
|
365
|
+
dispose: () => {
|
|
366
|
+
if (typeof unsubscribe === "function") unsubscribe();
|
|
367
|
+
},
|
|
368
|
+
};
|
|
369
|
+
}
|
package/src/extension/schemas.ts
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { Type } from "typebox";
|
|
6
|
-
import { SUBAGENT_ACTIONS } from "../shared/types.ts";
|
|
7
6
|
|
|
8
7
|
function keepTopLevelParameterDescriptions<T>(schema: T): T {
|
|
9
8
|
return pruneNestedDescriptions(schema, []) as T;
|
|
@@ -75,6 +74,24 @@ const AcceptanceOverride = Type.Unsafe({
|
|
|
75
74
|
description: "Optional acceptance policy. Omitted means auto-inferred; verified requires configured runtime commands.",
|
|
76
75
|
});
|
|
77
76
|
|
|
77
|
+
const TurnBudgetOverride = Type.Object({
|
|
78
|
+
maxTurns: Type.Integer({ minimum: 1 }),
|
|
79
|
+
graceTurns: Type.Optional(Type.Integer({ minimum: 0 })),
|
|
80
|
+
}, { additionalProperties: false, description: "Optional assistant-turn budget. At maxTurns the child is asked to wrap up; after graceTurns additional assistant turns it is aborted and partial output is returned." });
|
|
81
|
+
|
|
82
|
+
const ToolBudgetBlock = Type.Unsafe({
|
|
83
|
+
anyOf: [
|
|
84
|
+
{ type: "array", minItems: 1, items: { type: "string", minLength: 1 } },
|
|
85
|
+
{ type: "string", enum: ["*"] },
|
|
86
|
+
],
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
const ToolBudgetOverride = Type.Object({
|
|
90
|
+
soft: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
91
|
+
hard: Type.Integer({ minimum: 1 }),
|
|
92
|
+
block: Type.Optional(ToolBudgetBlock),
|
|
93
|
+
}, { additionalProperties: false, description: "Optional child tool-call budget. soft nudges the child; after hard, block tools (default read/grep/find/ls, or '*' for all tools) are blocked so the child can finalize." });
|
|
94
|
+
|
|
78
95
|
const TaskItem = Type.Object({
|
|
79
96
|
agent: Type.String(),
|
|
80
97
|
task: Type.String(),
|
|
@@ -86,6 +103,7 @@ const TaskItem = Type.Object({
|
|
|
86
103
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking for this task" })),
|
|
87
104
|
model: Type.Optional(Type.String({ description: "Override model for this task (e.g. 'google/gemini-3-pro')" })),
|
|
88
105
|
skill: Type.Optional(SkillOverride),
|
|
106
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
89
107
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
90
108
|
});
|
|
91
109
|
|
|
@@ -105,6 +123,7 @@ const ParallelTaskSchema = Type.Object({
|
|
|
105
123
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
106
124
|
skill: Type.Optional(SkillOverride),
|
|
107
125
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
126
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
108
127
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
109
128
|
});
|
|
110
129
|
|
|
@@ -132,6 +151,7 @@ const DynamicParallelTemplateSchema = Type.Object({
|
|
|
132
151
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
133
152
|
skill: Type.Optional(SkillOverride),
|
|
134
153
|
model: Type.Optional(Type.String({ description: "Override model for this task" })),
|
|
154
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
135
155
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
136
156
|
}, { additionalProperties: false });
|
|
137
157
|
|
|
@@ -157,6 +177,7 @@ const ChainItem = Type.Object({
|
|
|
157
177
|
progress: Type.Optional(Type.Boolean({ description: "Enable progress.md tracking in {chain_dir}" })),
|
|
158
178
|
skill: Type.Optional(SkillOverride),
|
|
159
179
|
model: Type.Optional(Type.String({ description: "Override model for this step" })),
|
|
180
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
160
181
|
acceptance: Type.Optional(AcceptanceOverride),
|
|
161
182
|
parallel: Type.Optional(Type.Unsafe({
|
|
162
183
|
anyOf: [
|
|
@@ -197,20 +218,26 @@ const SubagentParamsSchema = Type.Object({
|
|
|
197
218
|
task: Type.Optional(Type.String({ description: "Task (SINGLE mode, optional for self-contained agents)" })),
|
|
198
219
|
// Management action (when present, tool operates in management mode)
|
|
199
220
|
action: Type.Optional(Type.String({
|
|
200
|
-
|
|
201
|
-
description: "Management/control action. Omit for execution mode."
|
|
221
|
+
description: "Management/control action only. Must be omitted for execution mode (single, parallel, or chain)."
|
|
202
222
|
})),
|
|
203
223
|
id: Type.Optional(Type.String({
|
|
204
|
-
description: "Run id or prefix for action='status', action='interrupt', action='resume', or action='append-step'."
|
|
224
|
+
description: "Run id or prefix for action='status', action='interrupt', action='resume', action='steer', or action='append-step'."
|
|
205
225
|
})),
|
|
206
226
|
runId: Type.Optional(Type.String({
|
|
207
|
-
description: "Target run ID for action='interrupt', action='resume', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
227
|
+
description: "Target run ID for action='interrupt', action='resume', action='steer', or action='append-step'. Defaults to the most recently active controllable run for interrupt. Prefer id for new calls."
|
|
208
228
|
})),
|
|
209
229
|
dir: Type.Optional(Type.String({
|
|
210
|
-
description: "Async run directory for action='status' or action='
|
|
230
|
+
description: "Async run directory for action='status', action='resume', or action='steer'."
|
|
231
|
+
})),
|
|
232
|
+
index: Type.Optional(Type.Integer({ minimum: 0, description: "Zero-based child index for actions that target a specific child or transcript." })),
|
|
233
|
+
view: Type.Optional(Type.String({
|
|
234
|
+
enum: ["fleet", "transcript"],
|
|
235
|
+
description: "Optional status view. Use view='fleet' for a read-only active foreground/async fleet surface, or view='transcript' with id/dir (and optional index) to tail a run transcript.",
|
|
211
236
|
})),
|
|
212
|
-
|
|
213
|
-
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume'. Use index to choose a child from multi-child runs." })),
|
|
237
|
+
lines: Type.Optional(Type.Integer({ minimum: 1, maximum: 500, description: "Maximum transcript lines for action='status', view='transcript'. Defaults to 80." })),
|
|
238
|
+
message: Type.Optional(Type.String({ description: "Follow-up message for action='resume' or non-terminal guidance for action='steer'. Use index to choose a child from multi-child runs." })),
|
|
239
|
+
schedule: Type.Optional(Type.String({ description: "Explicit one-shot schedule for action='schedule'. Only honored when scheduledRuns.enabled is true. Use '+10m' or a future ISO timestamp with timezone; scheduled runs always launch async with fresh context." })),
|
|
240
|
+
scheduleName: Type.Optional(Type.String({ description: "Optional display name for action='schedule'." })),
|
|
214
241
|
// Chain identifier for management (can't reuse 'chain' — that's the execution array)
|
|
215
242
|
chainName: Type.Optional(Type.String({
|
|
216
243
|
description: "Chain name for get/update/delete management actions"
|
|
@@ -237,6 +264,8 @@ const SubagentParamsSchema = Type.Object({
|
|
|
237
264
|
async: Type.Optional(Type.Boolean({ description: "Run in background (default: false, or per config)" })),
|
|
238
265
|
timeoutMs: Type.Optional(Type.Integer({ minimum: 1, description: "Optional run-level timeout in ms for foreground and async/background runs. Alias of maxRuntimeMs." })),
|
|
239
266
|
maxRuntimeMs: Type.Optional(Type.Integer({ minimum: 1, description: "Alias of timeoutMs for optional run-level timeout in foreground and async/background runs." })),
|
|
267
|
+
turnBudget: Type.Optional(TurnBudgetOverride),
|
|
268
|
+
toolBudget: Type.Optional(ToolBudgetOverride),
|
|
240
269
|
agentScope: Type.Optional(Type.String({ description: "Agent discovery scope: 'user', 'project', or 'both' (default: 'both'; project wins on name collisions)" })),
|
|
241
270
|
cwd: Type.Optional(Type.String()),
|
|
242
271
|
artifacts: Type.Optional(Type.Boolean({ description: "Write debug artifacts (default: true)" })),
|
|
@@ -263,3 +292,18 @@ const SubagentParamsSchema = Type.Object({
|
|
|
263
292
|
});
|
|
264
293
|
|
|
265
294
|
export const SubagentParams = keepTopLevelParameterDescriptions(SubagentParamsSchema);
|
|
295
|
+
|
|
296
|
+
const WaitParamsSchema = Type.Object({
|
|
297
|
+
id: Type.Optional(Type.String({
|
|
298
|
+
description: "Run id or prefix to wait for one specific run. Omit to wait across every active async run started in this session.",
|
|
299
|
+
})),
|
|
300
|
+
all: Type.Optional(Type.Boolean({
|
|
301
|
+
description: "Wait for ALL active runs to finish. Default false: return as soon as the first run finishes, so a fleet manager can spawn a replacement and wait again. Ignored when id targets a single run.",
|
|
302
|
+
})),
|
|
303
|
+
timeoutMs: Type.Optional(Type.Integer({
|
|
304
|
+
minimum: 1,
|
|
305
|
+
description: "Give up waiting after this many milliseconds (the runs keep going regardless). Defaults to 1800000 (30 minutes).",
|
|
306
|
+
})),
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
export const WaitParams = keepTopLevelParameterDescriptions(WaitParamsSchema);
|