@narumitw/pi-subagents 0.30.1 → 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/README.md +31 -15
- package/package.json +7 -7
- package/src/agents.ts +20 -15
- package/src/config-ui.ts +368 -66
- package/src/context.ts +13 -16
- package/src/execution.ts +360 -337
- package/src/limits.ts +8 -2
- package/src/params.ts +26 -9
- package/src/render.ts +2 -4
- package/src/settings.ts +84 -2
- package/src/stateful-prompt.ts +2 -5
- package/src/stateful.ts +25 -47
- package/src/subagents.ts +30 -12
- package/src/workspace.ts +7 -6
package/src/limits.ts
CHANGED
|
@@ -31,7 +31,10 @@ export function truncateUtf8(text: string, maxBytes: number): BoundedText {
|
|
|
31
31
|
originalBytes,
|
|
32
32
|
};
|
|
33
33
|
}
|
|
34
|
-
const prefix = Buffer.from(text, "utf8")
|
|
34
|
+
const prefix = Buffer.from(text, "utf8")
|
|
35
|
+
.subarray(0, limit - marker.length)
|
|
36
|
+
.toString("utf8")
|
|
37
|
+
.replace(/�+$/g, "");
|
|
35
38
|
return { text: `${prefix}${TRUNCATION_MARKER}`, truncated: true, originalBytes };
|
|
36
39
|
}
|
|
37
40
|
|
|
@@ -45,7 +48,10 @@ export function truncateUtf8Tail(text: string, maxBytes: number): BoundedText {
|
|
|
45
48
|
const marker = Buffer.from(TAIL_TRUNCATION_MARKER, "utf8");
|
|
46
49
|
if (marker.length >= limit) {
|
|
47
50
|
return {
|
|
48
|
-
text: bytes
|
|
51
|
+
text: bytes
|
|
52
|
+
.subarray(originalBytes - limit)
|
|
53
|
+
.toString("utf8")
|
|
54
|
+
.replace(/^�+/g, ""),
|
|
49
55
|
truncated: true,
|
|
50
56
|
originalBytes,
|
|
51
57
|
};
|
package/src/params.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StringEnum } from "@earendil-works/pi-ai";
|
|
2
|
-
import {
|
|
2
|
+
import { type Static, Type } from "typebox";
|
|
3
3
|
import { THINKING_LEVELS } from "./agents.js";
|
|
4
4
|
|
|
5
5
|
const TimeoutMs = Type.Number({
|
|
@@ -31,9 +31,15 @@ const ChainItem = Type.Object({
|
|
|
31
31
|
|
|
32
32
|
const AggregatorItem = Type.Object(
|
|
33
33
|
{
|
|
34
|
-
agent: Type.String({
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
agent: Type.String({
|
|
35
|
+
description: "Name of the fan-in agent to invoke after parallel tasks complete",
|
|
36
|
+
}),
|
|
37
|
+
task: Type.String({
|
|
38
|
+
description: "Fan-in task. Use {previous} to include all parallel outputs.",
|
|
39
|
+
}),
|
|
40
|
+
cwd: Type.Optional(
|
|
41
|
+
Type.String({ description: "Working directory for the aggregator process" }),
|
|
42
|
+
),
|
|
37
43
|
timeoutMs: Type.Optional(TimeoutMs),
|
|
38
44
|
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
39
45
|
},
|
|
@@ -50,16 +56,27 @@ const AgentScopeSchema = StringEnum(["user", "project", "both"] as const, {
|
|
|
50
56
|
});
|
|
51
57
|
|
|
52
58
|
export const SubagentParams = Type.Object({
|
|
53
|
-
agent: Type.Optional(
|
|
59
|
+
agent: Type.Optional(
|
|
60
|
+
Type.String({ description: "Name of the agent to invoke (for single mode)" }),
|
|
61
|
+
),
|
|
54
62
|
task: Type.Optional(Type.String({ description: "Task to delegate (for single mode)" })),
|
|
55
|
-
tasks: Type.Optional(
|
|
56
|
-
|
|
63
|
+
tasks: Type.Optional(
|
|
64
|
+
Type.Array(TaskItem, { description: "Array of {agent, task} for parallel execution" }),
|
|
65
|
+
),
|
|
66
|
+
chain: Type.Optional(
|
|
67
|
+
Type.Array(ChainItem, { description: "Array of {agent, task} for sequential execution" }),
|
|
68
|
+
),
|
|
57
69
|
aggregator: Type.Optional(AggregatorItem),
|
|
58
70
|
agentScope: Type.Optional(AgentScopeSchema),
|
|
59
71
|
confirmProjectAgents: Type.Optional(
|
|
60
|
-
Type.Boolean({
|
|
72
|
+
Type.Boolean({
|
|
73
|
+
description: "Prompt before running project-local agents. Default: true.",
|
|
74
|
+
default: true,
|
|
75
|
+
}),
|
|
76
|
+
),
|
|
77
|
+
cwd: Type.Optional(
|
|
78
|
+
Type.String({ description: "Working directory for the agent process (single mode)" }),
|
|
61
79
|
),
|
|
62
|
-
cwd: Type.Optional(Type.String({ description: "Working directory for the agent process (single mode)" })),
|
|
63
80
|
timeoutMs: Type.Optional(TimeoutMs),
|
|
64
81
|
thinkingLevel: Type.Optional(ThinkingLevelSchema),
|
|
65
82
|
});
|
package/src/render.ts
CHANGED
|
@@ -440,8 +440,7 @@ export function renderSubagentResult(
|
|
|
440
440
|
const collapsed = getCollapsedDisplayItems(r);
|
|
441
441
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
442
442
|
text += `\n\n${theme.fg("muted", `─── Step ${r.step}: `)}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
443
|
-
if (rFailed && r.errorMessage)
|
|
444
|
-
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
443
|
+
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
445
444
|
if (collapsed.items.length > 0)
|
|
446
445
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
447
446
|
else if (currentIsRunning && r === currentResult)
|
|
@@ -605,8 +604,7 @@ export function renderSubagentResult(
|
|
|
605
604
|
const collapsed = getCollapsedDisplayItems(r);
|
|
606
605
|
const finalOutput = getResultFinalOutput(r).trim();
|
|
607
606
|
text += `\n\n${theme.fg("muted", "─── ")}${theme.fg("accent", r.agent)} ${rIcon}`;
|
|
608
|
-
if (rFailed && r.errorMessage)
|
|
609
|
-
text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
607
|
+
if (rFailed && r.errorMessage) text += `\n${theme.fg("error", `Error: ${r.errorMessage}`)}`;
|
|
610
608
|
if (collapsed.items.length > 0)
|
|
611
609
|
text += `\n${renderDisplayItems(collapsed.items, 5, collapsed.total)}`;
|
|
612
610
|
else if (rRunning) text += `\n${theme.fg("muted", "(running...)")}`;
|
package/src/settings.ts
CHANGED
|
@@ -80,6 +80,15 @@ export function normalizeSubagentSettings(value: unknown): SubagentSettings | un
|
|
|
80
80
|
}
|
|
81
81
|
if (Object.keys(agents).length > 0) settings.agents = agents;
|
|
82
82
|
}
|
|
83
|
+
if (hasOwn(value, "blocking")) {
|
|
84
|
+
if (!isPlainObject(value.blocking)) return undefined;
|
|
85
|
+
const blocking: NonNullable<SubagentSettings["blocking"]> = {};
|
|
86
|
+
if (hasOwn(value.blocking, "enabled")) {
|
|
87
|
+
if (typeof value.blocking.enabled !== "boolean") return undefined;
|
|
88
|
+
blocking.enabled = value.blocking.enabled;
|
|
89
|
+
}
|
|
90
|
+
settings.blocking = blocking;
|
|
91
|
+
}
|
|
83
92
|
if (hasOwn(value, "stateful")) {
|
|
84
93
|
if (!isPlainObject(value.stateful)) return undefined;
|
|
85
94
|
const runtime: NonNullable<SubagentSettings["stateful"]> = {};
|
|
@@ -241,6 +250,15 @@ export function saveSubagentConfig(settings: SubagentSettings): void {
|
|
|
241
250
|
writeSettingsObject(settings);
|
|
242
251
|
}
|
|
243
252
|
|
|
253
|
+
export type DelegationWorkflow = "all" | "async-only" | "blocking-only" | "disabled";
|
|
254
|
+
|
|
255
|
+
export interface DelegationWorkflowSettingsSnapshot {
|
|
256
|
+
path: string;
|
|
257
|
+
value: DelegationWorkflow;
|
|
258
|
+
source: "default" | "user settings";
|
|
259
|
+
error?: string;
|
|
260
|
+
}
|
|
261
|
+
|
|
244
262
|
export interface CompletionDeliverySettingsSnapshot {
|
|
245
263
|
path: string;
|
|
246
264
|
value: CompletionDelivery;
|
|
@@ -252,6 +270,46 @@ export function subagentSettingsFilePath(): string {
|
|
|
252
270
|
return path.join(getAgentDir(), SETTINGS_FILE);
|
|
253
271
|
}
|
|
254
272
|
|
|
273
|
+
export function resolveDelegationWorkflow(
|
|
274
|
+
blockingEnabled: boolean,
|
|
275
|
+
statefulEnabled: boolean,
|
|
276
|
+
): DelegationWorkflow {
|
|
277
|
+
if (blockingEnabled && statefulEnabled) return "all";
|
|
278
|
+
if (statefulEnabled) return "async-only";
|
|
279
|
+
if (blockingEnabled) return "blocking-only";
|
|
280
|
+
return "disabled";
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export function inspectDelegationWorkflowSettings(): DelegationWorkflowSettingsSnapshot {
|
|
284
|
+
const configPath = subagentSettingsFilePath();
|
|
285
|
+
if (!fs.existsSync(configPath)) {
|
|
286
|
+
return { path: configPath, value: "all", source: "default" };
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
290
|
+
const settings = normalizeSubagentSettings(raw);
|
|
291
|
+
if (!settings) throw new Error(`${SETTINGS_FILE} is not a valid settings object`);
|
|
292
|
+
const explicit =
|
|
293
|
+
(isPlainObject(raw.blocking) && hasOwn(raw.blocking, "enabled")) ||
|
|
294
|
+
(isPlainObject(raw.stateful) && hasOwn(raw.stateful, "enabled"));
|
|
295
|
+
return {
|
|
296
|
+
path: configPath,
|
|
297
|
+
value: resolveDelegationWorkflow(
|
|
298
|
+
settings.blocking?.enabled !== false,
|
|
299
|
+
settings.stateful?.enabled !== false,
|
|
300
|
+
),
|
|
301
|
+
source: explicit ? "user settings" : "default",
|
|
302
|
+
};
|
|
303
|
+
} catch (error) {
|
|
304
|
+
return {
|
|
305
|
+
path: configPath,
|
|
306
|
+
value: "all",
|
|
307
|
+
source: "default",
|
|
308
|
+
error: formatError(error),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
255
313
|
export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsSnapshot {
|
|
256
314
|
const configPath = subagentSettingsFilePath();
|
|
257
315
|
if (!fs.existsSync(configPath)) {
|
|
@@ -261,8 +319,7 @@ export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsS
|
|
|
261
319
|
const raw = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
262
320
|
const settings = normalizeSubagentSettings(raw);
|
|
263
321
|
if (!settings) throw new Error(`${SETTINGS_FILE} is not a valid settings object`);
|
|
264
|
-
const explicit =
|
|
265
|
-
isPlainObject(raw.stateful) && hasOwn(raw.stateful, "completionDelivery");
|
|
322
|
+
const explicit = isPlainObject(raw.stateful) && hasOwn(raw.stateful, "completionDelivery");
|
|
266
323
|
return {
|
|
267
324
|
path: configPath,
|
|
268
325
|
value: settings.stateful?.completionDelivery ?? DEFAULT_COMPLETION_DELIVERY,
|
|
@@ -278,6 +335,31 @@ export function inspectCompletionDeliverySettings(): CompletionDeliverySettingsS
|
|
|
278
335
|
}
|
|
279
336
|
}
|
|
280
337
|
|
|
338
|
+
export function updateDelegationWorkflowSetting(
|
|
339
|
+
value: Exclude<DelegationWorkflow, "disabled">,
|
|
340
|
+
): void {
|
|
341
|
+
const raw = readSettingsObjectForUpdate();
|
|
342
|
+
const blocking = raw.blocking;
|
|
343
|
+
if (blocking !== undefined && !isPlainObject(blocking)) {
|
|
344
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} blocking settings`);
|
|
345
|
+
}
|
|
346
|
+
const stateful = raw.stateful;
|
|
347
|
+
if (stateful !== undefined && !isPlainObject(stateful)) {
|
|
348
|
+
throw new Error(`Cannot update invalid ${SETTINGS_FILE} stateful settings`);
|
|
349
|
+
}
|
|
350
|
+
writeSettingsObject({
|
|
351
|
+
...raw,
|
|
352
|
+
blocking: {
|
|
353
|
+
...(blocking ?? {}),
|
|
354
|
+
enabled: value !== "async-only",
|
|
355
|
+
},
|
|
356
|
+
stateful: {
|
|
357
|
+
...(stateful ?? {}),
|
|
358
|
+
enabled: value !== "blocking-only",
|
|
359
|
+
},
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
281
363
|
export function updateCompletionDeliverySetting(value: CompletionDelivery): void {
|
|
282
364
|
const raw = readSettingsObjectForUpdate();
|
|
283
365
|
const stateful = raw.stateful;
|
package/src/stateful-prompt.ts
CHANGED
|
@@ -1,14 +1,11 @@
|
|
|
1
1
|
import type { AgentConfig } from "./agents.js";
|
|
2
|
+
import { redactPrivateText } from "./context.js";
|
|
2
3
|
import { resolveDefaultSubagentTimeoutMs } from "./execution.js";
|
|
3
4
|
import { DEFAULT_MAX_CONTEXT_BYTES, truncateUtf8 } from "./limits.js";
|
|
4
|
-
import { redactPrivateText } from "./context.js";
|
|
5
5
|
import type { ManagedAgent } from "./registry.js";
|
|
6
6
|
|
|
7
7
|
export function buildStatefulTurnPrompt(
|
|
8
|
-
record: Pick<
|
|
9
|
-
ManagedAgent,
|
|
10
|
-
"context" | "history" | "mailbox" | "currentMailboxMessageIds"
|
|
11
|
-
>,
|
|
8
|
+
record: Pick<ManagedAgent, "context" | "history" | "mailbox" | "currentMailboxMessageIds">,
|
|
12
9
|
task: string,
|
|
13
10
|
maxBytes = DEFAULT_MAX_CONTEXT_BYTES,
|
|
14
11
|
): { text: string; truncated: boolean } {
|
package/src/stateful.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
type CompletionDelivery,
|
|
13
13
|
discoverAgents,
|
|
14
14
|
isThinkingLevel,
|
|
15
|
+
type SubagentRuntimeSettings,
|
|
15
16
|
THINKING_LEVELS,
|
|
16
17
|
} from "./agents.js";
|
|
17
18
|
import { buildContextSnapshot, type ContextMode, redactPrivateText } from "./context.js";
|
|
@@ -52,11 +53,18 @@ const MAX_COMPLETION_ERROR_BYTES = 512;
|
|
|
52
53
|
const MAX_COMPLETIONS_PER_MESSAGE = 16;
|
|
53
54
|
const COMPLETION_BATCH_DELAY_MS = 10;
|
|
54
55
|
|
|
55
|
-
function createSpawnPromptGuidelines(
|
|
56
|
+
function createSpawnPromptGuidelines(
|
|
57
|
+
completionDelivery: CompletionDelivery,
|
|
58
|
+
blockingEnabled = true,
|
|
59
|
+
): string[] {
|
|
56
60
|
const deliveryGuidance =
|
|
57
61
|
completionDelivery === "auto-resume"
|
|
58
|
-
?
|
|
59
|
-
|
|
62
|
+
? blockingEnabled
|
|
63
|
+
? "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result; do not choose blocking parallel fan-out merely to keep delegation in the same turn."
|
|
64
|
+
: "With subagent_spawn completion delivery set to auto-resume, prefer one subagent_spawn for broad asynchronous research or review that covers related branches even when the final answer depends on its result."
|
|
65
|
+
: blockingEnabled
|
|
66
|
+
? "With subagent_spawn completion delivery set to next-turn (the default), prefer one subagent_spawn for broad asynchronous research or review only when the current response does not depend on its result; use the blocking subagent when the final answer depends on the detached result."
|
|
67
|
+
: "With subagent_spawn completion delivery set to next-turn (the default), use subagent_spawn only when the current response does not depend on its result; complete final-answer-dependent work directly because an idle root is not awakened.";
|
|
60
68
|
const noLocalWorkGuidance =
|
|
61
69
|
completionDelivery === "auto-resume"
|
|
62
70
|
? "After subagent_spawn returns, do useful non-overlapping local work immediately. If none remains, briefly tell the user what subagent_spawn launched and end the response; auto-resume will request a synthesis turn after completion."
|
|
@@ -66,8 +74,12 @@ function createSpawnPromptGuidelines(completionDelivery: CompletionDelivery): st
|
|
|
66
74
|
"Set subagent_spawn thinkingLevel to the lowest sufficient thinking level for the delegated task: use off or minimal for extraction, formatting, or mechanical work; low for straightforward bounded work; medium for ordinary multi-step research or implementation; high for complex debugging, design, review, or cross-file analysis; xhigh for highly ambiguous, cross-system, or high-risk analysis; and max only for the hardest tasks when quality clearly outweighs latency and cost. Omit subagent_spawn thinkingLevel only to preserve the agent or child default.",
|
|
67
75
|
deliveryGuidance,
|
|
68
76
|
"Use a single subagent_spawn only for a concrete bounded subtask that can run independently and has an isolation or specialization benefit such as independent review, bounded context/output, a distinct model/tool profile, or workspace isolation.",
|
|
69
|
-
|
|
70
|
-
|
|
77
|
+
...(blockingEnabled
|
|
78
|
+
? [
|
|
79
|
+
"Use the blocking subagent instead of subagent_spawn when synchronous output is required before the main agent can continue and waiting is intentional; queued steering cannot be processed until that blocking call returns.",
|
|
80
|
+
"When subagent_spawn fits the completion-delivery policy, do not choose a blocking parallel subagent merely to keep delegation in the same turn.",
|
|
81
|
+
]
|
|
82
|
+
: []),
|
|
71
83
|
"Add another subagent_spawn only for truly independent work with safe workspace concurrency.",
|
|
72
84
|
noLocalWorkGuidance,
|
|
73
85
|
'Consume and synthesize available subagent_spawn completion messages; use subagent_manage with action "interrupt" or "close" for agents that are no longer needed.',
|
|
@@ -76,8 +88,10 @@ function createSpawnPromptGuidelines(completionDelivery: CompletionDelivery): st
|
|
|
76
88
|
}
|
|
77
89
|
|
|
78
90
|
export interface StatefulSubagentDependencies {
|
|
91
|
+
blockingEnabled?: boolean;
|
|
79
92
|
createInProcessSession?: ChildSessionFactory;
|
|
80
93
|
workspaceManager?: WorkspaceManager;
|
|
94
|
+
settings?: SubagentRuntimeSettings;
|
|
81
95
|
}
|
|
82
96
|
|
|
83
97
|
export interface StatefulSubagentRuntimeStatus {
|
|
@@ -106,7 +120,10 @@ export function registerStatefulSubagents(
|
|
|
106
120
|
pi: ExtensionAPI,
|
|
107
121
|
dependencies: StatefulSubagentDependencies = {},
|
|
108
122
|
): StatefulSubagentController {
|
|
109
|
-
const settings =
|
|
123
|
+
const settings = Object.hasOwn(dependencies, "settings")
|
|
124
|
+
? (dependencies.settings ?? {})
|
|
125
|
+
: (readSubagentSettings()?.stateful ?? {});
|
|
126
|
+
const blockingEnabled = dependencies.blockingEnabled !== false;
|
|
110
127
|
const enabled = settings.enabled !== false;
|
|
111
128
|
const transportKind = resolveStatefulTransportKind(settings.transport);
|
|
112
129
|
let completionDelivery = resolveCompletionDelivery(settings.completionDelivery);
|
|
@@ -304,7 +321,7 @@ export function registerStatefulSubagents(
|
|
|
304
321
|
description:
|
|
305
322
|
"Start an addressable background subagent with an optional thinking level chosen for the task difficulty, return immediately with an agentId, and receive its completion asynchronously.",
|
|
306
323
|
promptSnippet: "Start a reusable detached subagent; completion is delivered asynchronously",
|
|
307
|
-
promptGuidelines: createSpawnPromptGuidelines(completionDelivery),
|
|
324
|
+
promptGuidelines: createSpawnPromptGuidelines(completionDelivery, blockingEnabled),
|
|
308
325
|
parameters: Type.Object({
|
|
309
326
|
agent: Type.String({ minLength: 1 }),
|
|
310
327
|
task: Type.String({ minLength: 1, maxLength: DEFAULT_MAX_CONTEXT_BYTES }),
|
|
@@ -382,7 +399,7 @@ export function registerStatefulSubagents(
|
|
|
382
399
|
},
|
|
383
400
|
});
|
|
384
401
|
refreshSpawnToolRegistration = () => {
|
|
385
|
-
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery);
|
|
402
|
+
spawnTool.promptGuidelines = createSpawnPromptGuidelines(completionDelivery, blockingEnabled);
|
|
386
403
|
pi.registerTool(spawnTool);
|
|
387
404
|
};
|
|
388
405
|
refreshSpawnToolRegistration();
|
|
@@ -531,39 +548,6 @@ export function registerStatefulSubagents(
|
|
|
531
548
|
},
|
|
532
549
|
});
|
|
533
550
|
|
|
534
|
-
pi.registerCommand("subagents:agents", {
|
|
535
|
-
description: "Inspect or clear current-session subagents",
|
|
536
|
-
getArgumentCompletions(prefix: string) {
|
|
537
|
-
return ["list", "clear"]
|
|
538
|
-
.filter((value) => value.startsWith(prefix))
|
|
539
|
-
.map((value) => ({ value, label: value }));
|
|
540
|
-
},
|
|
541
|
-
async handler(args, ctx) {
|
|
542
|
-
const subcommand = args.trim().toLowerCase() || "list";
|
|
543
|
-
if (subcommand === "clear") {
|
|
544
|
-
const count = await controller.clearAgents();
|
|
545
|
-
ctx.ui.notify(
|
|
546
|
-
count > 0
|
|
547
|
-
? `Cleared ${count} current-session subagent${count === 1 ? "" : "s"}.`
|
|
548
|
-
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
549
|
-
"info",
|
|
550
|
-
);
|
|
551
|
-
return;
|
|
552
|
-
}
|
|
553
|
-
if (subcommand !== "list") {
|
|
554
|
-
ctx.ui.notify(`Unknown /subagents:agents subcommand: ${subcommand}`, "warning");
|
|
555
|
-
return;
|
|
556
|
-
}
|
|
557
|
-
const agents = controller.listAgents(true);
|
|
558
|
-
ctx.ui.notify(
|
|
559
|
-
agents.length
|
|
560
|
-
? agents.map(formatLine).join("\n")
|
|
561
|
-
: statefulEmptyMessage(controller.getRuntimeStatus()),
|
|
562
|
-
"info",
|
|
563
|
-
);
|
|
564
|
-
},
|
|
565
|
-
});
|
|
566
|
-
|
|
567
551
|
return controller;
|
|
568
552
|
}
|
|
569
553
|
|
|
@@ -652,12 +636,6 @@ export function resolveSpawnContextMode(
|
|
|
652
636
|
return normalizeContextMode(value);
|
|
653
637
|
}
|
|
654
638
|
|
|
655
|
-
function statefulEmptyMessage(status: StatefulSubagentRuntimeStatus): string {
|
|
656
|
-
if (!status.enabled) return "Stateful subagents are disabled in user settings.";
|
|
657
|
-
if (!status.initialized) return "Stateful subagents are not initialized for this session.";
|
|
658
|
-
return "No current-session subagents.";
|
|
659
|
-
}
|
|
660
|
-
|
|
661
639
|
export function formatStatefulAgentLine(agent: ManagedAgent): string {
|
|
662
640
|
const elapsedSeconds = Math.max(0, Math.floor((Date.now() - agent.updatedAt) / 1000));
|
|
663
641
|
const actions =
|
package/src/subagents.ts
CHANGED
|
@@ -22,6 +22,33 @@ import { consumeSubagentSettingsNotice, readSubagentSettings } from "./settings.
|
|
|
22
22
|
import { registerStatefulSubagents } from "./stateful.js";
|
|
23
23
|
|
|
24
24
|
export default function (pi: ExtensionAPI) {
|
|
25
|
+
const settings = readSubagentSettings();
|
|
26
|
+
if (settings?.blocking?.enabled !== false) registerBlockingSubagent(pi);
|
|
27
|
+
|
|
28
|
+
pi.on("session_start", (_event, ctx) => {
|
|
29
|
+
// Preserve a one-shot migration notice from extension load while refreshing
|
|
30
|
+
// validation against settings that may have changed before this session.
|
|
31
|
+
const loadNotice = consumeSubagentSettingsNotice();
|
|
32
|
+
readSubagentSettings();
|
|
33
|
+
const refreshedNotice = consumeSubagentSettingsNotice();
|
|
34
|
+
const notice = [
|
|
35
|
+
...new Set([loadNotice, refreshedNotice].filter((value) => value !== undefined)),
|
|
36
|
+
].join("\n");
|
|
37
|
+
if (notice) ctx.ui.notify(notice, "warning");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const blockingEnabled = settings?.blocking?.enabled !== false;
|
|
41
|
+
const statefulRuntime = registerStatefulSubagents(pi, {
|
|
42
|
+
blockingEnabled,
|
|
43
|
+
settings: settings?.stateful,
|
|
44
|
+
});
|
|
45
|
+
registerSubagentConfigCommand(pi, {
|
|
46
|
+
...statefulRuntime,
|
|
47
|
+
getBlockingEnabled: () => blockingEnabled,
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function registerBlockingSubagent(pi: ExtensionAPI) {
|
|
25
52
|
pi.registerTool<typeof SubagentParams, SubagentDetails>({
|
|
26
53
|
name: "subagent",
|
|
27
54
|
label: "Blocking Subagent",
|
|
@@ -65,24 +92,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
65
92
|
if ((event.details as (SubagentDetails & { isError?: boolean }) | undefined)?.isError)
|
|
66
93
|
return { isError: true };
|
|
67
94
|
});
|
|
68
|
-
|
|
69
|
-
pi.on("session_start", (_event, ctx) => {
|
|
70
|
-
let notice = consumeSubagentSettingsNotice();
|
|
71
|
-
if (!notice) {
|
|
72
|
-
readSubagentSettings();
|
|
73
|
-
notice = consumeSubagentSettingsNotice();
|
|
74
|
-
}
|
|
75
|
-
if (notice) ctx.ui.notify(notice, "warning");
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const statefulRuntime = registerStatefulSubagents(pi);
|
|
79
|
-
registerSubagentConfigCommand(pi, statefulRuntime);
|
|
80
95
|
}
|
|
96
|
+
|
|
81
97
|
export { parsePositiveInteger } from "./execution.js";
|
|
82
98
|
export { formatTokens, formatUsageStats } from "./render.js";
|
|
83
99
|
export { buildPiArgs } from "./runner.js";
|
|
84
100
|
export {
|
|
85
101
|
inspectCompletionDeliverySettings,
|
|
102
|
+
inspectDelegationWorkflowSettings,
|
|
86
103
|
normalizeAgentSettings,
|
|
87
104
|
normalizeSubagentSettings,
|
|
88
105
|
readSubagentSettings,
|
|
@@ -93,4 +110,5 @@ export {
|
|
|
93
110
|
uniqueToolNames,
|
|
94
111
|
updateAgentToolsSetting,
|
|
95
112
|
updateCompletionDeliverySetting,
|
|
113
|
+
updateDelegationWorkflowSetting,
|
|
96
114
|
} from "./settings.js";
|
package/src/workspace.ts
CHANGED
|
@@ -27,10 +27,10 @@ export class WorkspaceManager {
|
|
|
27
27
|
if (relativeCwd.startsWith("..") || path.isAbsolute(relativeCwd)) {
|
|
28
28
|
throw new Error("Subagent cwd is outside the Git repository");
|
|
29
29
|
}
|
|
30
|
-
const status = (
|
|
31
|
-
|
|
32
|
-
)
|
|
33
|
-
|
|
30
|
+
const status = (await execFileAsync("git", ["-C", repositoryRoot, "status", "--porcelain"]))
|
|
31
|
+
.stdout;
|
|
32
|
+
if (status.trim())
|
|
33
|
+
throw new Error("Isolated subagent workspace requires a clean Git repository");
|
|
34
34
|
const rootPath = await fs.promises.mkdtemp(path.join(os.tmpdir(), WORKTREE_PREFIX));
|
|
35
35
|
let registered = false;
|
|
36
36
|
try {
|
|
@@ -86,8 +86,9 @@ export class WorkspaceManager {
|
|
|
86
86
|
workspace.rootPath,
|
|
87
87
|
]).catch(async () => {
|
|
88
88
|
await fs.promises.rm(workspace.rootPath, { recursive: true, force: true });
|
|
89
|
-
await execFileAsync("git", ["-C", workspace.repositoryRoot, "worktree", "prune"])
|
|
90
|
-
|
|
89
|
+
await execFileAsync("git", ["-C", workspace.repositoryRoot, "worktree", "prune"]).catch(
|
|
90
|
+
() => undefined,
|
|
91
|
+
);
|
|
91
92
|
});
|
|
92
93
|
await fs.promises.rm(`${workspace.rootPath}.owner`, { force: true });
|
|
93
94
|
}
|