@clanker-code/pi-subagents 0.11.0 → 0.11.1
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 +12 -0
- package/dist/agent-manager.d.ts +3 -0
- package/dist/agent-manager.js +1 -0
- package/dist/agent-runner.d.ts +11 -0
- package/dist/agent-runner.js +38 -1
- package/dist/agent-tool-description.d.ts +7 -1
- package/dist/agent-tool-description.js +3 -3
- package/dist/cross-extension-rpc.d.ts +4 -0
- package/dist/cross-extension-rpc.js +11 -1
- package/dist/dashboard-ui.js +48 -23
- package/dist/index.js +8 -2
- package/dist/schedule.d.ts +9 -1
- package/dist/schedule.js +7 -1
- package/package.json +1 -1
- package/src/agent-manager.ts +4 -0
- package/src/agent-runner.ts +45 -0
- package/src/agent-tool-description.ts +10 -4
- package/src/cross-extension-rpc.ts +14 -1
- package/src/dashboard-ui.ts +45 -24
- package/src/index.ts +8 -2
- package/src/schedule.ts +20 -1
- package/bugs.txt +0 -57
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,18 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.1] - 2026-06-28
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- **Depth 2+ subagents now appear in the TUI widget** — each child session's `DefaultResourceLoader` previously created its own isolated event bus, so lifecycle events (`subagents:created`, `subagents:started`, `subagents:completed`, `subagents:failed`) from depth 2+ agents never reached the parent's widget listener. A forwarding event bus now wraps the parent bus: the child gets its own isolated local bus, but lifecycle events are forwarded to the parent so the widget renders the full recursive agent tree.
|
|
14
|
+
- **Agent tool description shows next spawn depth instead of agent's own depth** — the `{{currentDepth}}` placeholder and recursive guideline in the Agent tool description now show `extensionDepth + 1` (the depth the *next* spawned agent would be at) instead of `extensionDepth` (the agent's own depth), eliminating the off-by-one confusion where a depth-1 agent displayed "1/4" but spawned agents at depth 2.
|
|
15
|
+
- **Event bus propagation covers all spawn paths** — RPC-spawned agents (`cross-extension-rpc.ts`), scheduled agents (`schedule.ts`), and `spawnAndWait` foreground agents now correctly receive the parent's event bus and recursive depth metadata, so lifecycle events from agents spawned via any path are visible in the parent widget.
|
|
16
|
+
- **Dashboard UI action handlers** — steer, abort, and view-result row actions in the subagents management modal now route to the correct handler functions instead of silently no-opping.
|
|
17
|
+
- **Dashboard UI duplicate module push guard** — the `ui_management` probe no longer pushes duplicate module entries when called multiple times in the same session.
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
- **Dashboard UI model column** — the subagents management modal now shows the model used by each agent (e.g. "opus", "sonnet") in a dedicated column.
|
|
21
|
+
|
|
10
22
|
## [0.11.0] - 2026-06-28
|
|
11
23
|
|
|
12
24
|
### Added
|
package/dist/agent-manager.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
import type { Model } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { type ToolActivity } from "./agent-runner.js";
|
|
11
|
+
import type { EventBus } from "./cross-extension-rpc.js";
|
|
11
12
|
import { type AgentInvocation, type AgentRecord, type IsolationMode, type SubagentType, type ThinkingLevel } from "./types.js";
|
|
12
13
|
export type OnAgentComplete = (record: AgentRecord) => void;
|
|
13
14
|
export type OnAgentStart = (record: AgentRecord) => void;
|
|
@@ -69,6 +70,8 @@ interface SpawnOptions {
|
|
|
69
70
|
}) => void;
|
|
70
71
|
/** Called when the session successfully compacts. */
|
|
71
72
|
onCompaction?: (info: CompactionInfo) => void;
|
|
73
|
+
/** Parent's event bus — shared with child sessions so lifecycle events propagate to the parent widget. */
|
|
74
|
+
eventBus?: EventBus;
|
|
72
75
|
}
|
|
73
76
|
interface ResumeOptions {
|
|
74
77
|
signal?: AbortSignal;
|
package/dist/agent-manager.js
CHANGED
|
@@ -218,6 +218,7 @@ export class AgentManager {
|
|
|
218
218
|
},
|
|
219
219
|
depth: record.depth,
|
|
220
220
|
parentAgentId: record.parentAgentId,
|
|
221
|
+
eventBus: options.eventBus,
|
|
221
222
|
onSessionCreated: (session) => {
|
|
222
223
|
record.session = session;
|
|
223
224
|
// Flush any steers that arrived before the session was ready
|
package/dist/agent-runner.d.ts
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import type { Model } from "@earendil-works/pi-ai";
|
|
5
5
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
6
6
|
import { type AgentSession, type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import type { EventBus } from "./cross-extension-rpc.js";
|
|
7
8
|
import { type SubagentType, type ThinkingLevel } from "./types.js";
|
|
8
9
|
/**
|
|
9
10
|
* Tool names registered by THIS extension. Single source of truth so the
|
|
@@ -19,6 +20,13 @@ export declare const SUBAGENT_TOOL_NAMES: {
|
|
|
19
20
|
readonly CLEAR_SUBAGENTS: "clear_subagents";
|
|
20
21
|
readonly LIST_MODELS: "list_models";
|
|
21
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Create a forwarding event bus for a child session.
|
|
25
|
+
* The child gets its own local bus for emit/on, but lifecycle events
|
|
26
|
+
* (subagents:*) are also forwarded to the parent bus so the parent widget
|
|
27
|
+
* can display depth 2+ agents.
|
|
28
|
+
*/
|
|
29
|
+
export declare function createForwardingEventBus(parentBus: EventBus): EventBus;
|
|
22
30
|
export declare function getCurrentExtensionDepth(): number;
|
|
23
31
|
export declare function getCurrentExtensionAgentId(): string | undefined;
|
|
24
32
|
export declare function getCurrentExtensionParentAgentId(): string | undefined;
|
|
@@ -131,6 +139,9 @@ export interface RunOptions {
|
|
|
131
139
|
depth?: number;
|
|
132
140
|
/** Parent subagent id when spawned recursively from another subagent. */
|
|
133
141
|
parentAgentId?: string;
|
|
142
|
+
/** Parent's event bus — shared with the child session so lifecycle events
|
|
143
|
+
* (subagents:created, subagents:started, etc.) propagate to the parent widget. */
|
|
144
|
+
eventBus?: EventBus;
|
|
134
145
|
}
|
|
135
146
|
export interface RunResult {
|
|
136
147
|
responseText: string;
|
package/dist/agent-runner.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import { existsSync, readFileSync } from "node:fs";
|
|
5
5
|
import { homedir } from "node:os";
|
|
6
6
|
import { basename, dirname, isAbsolute, join, resolve } from "node:path";
|
|
7
|
-
import { createAgentSession, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import { createAgentSession, createEventBus, DefaultResourceLoader, getAgentDir, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
|
|
8
8
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
9
9
|
import { buildParentContext, extractText } from "./context.js";
|
|
10
10
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
@@ -40,6 +40,38 @@ const RECURSIVE_TOOL_NAMES = [
|
|
|
40
40
|
SUBAGENT_TOOL_NAMES.STEER,
|
|
41
41
|
];
|
|
42
42
|
const EXTENSION_DEPTH_KEY = Symbol.for("pi-subagents:extension-depth");
|
|
43
|
+
/** Lifecycle event names that should propagate from child to parent sessions. */
|
|
44
|
+
const FORWARDABLE_EVENTS = new Set([
|
|
45
|
+
"subagents:created",
|
|
46
|
+
"subagents:started",
|
|
47
|
+
"subagents:completed",
|
|
48
|
+
"subagents:failed",
|
|
49
|
+
"subagents:compacted",
|
|
50
|
+
]);
|
|
51
|
+
/**
|
|
52
|
+
* Create a forwarding event bus for a child session.
|
|
53
|
+
* The child gets its own local bus for emit/on, but lifecycle events
|
|
54
|
+
* (subagents:*) are also forwarded to the parent bus so the parent widget
|
|
55
|
+
* can display depth 2+ agents.
|
|
56
|
+
*/
|
|
57
|
+
export function createForwardingEventBus(parentBus) {
|
|
58
|
+
// Use the parent's EventBus factory to create a properly isolated local bus
|
|
59
|
+
const localBus = createEventBus();
|
|
60
|
+
return {
|
|
61
|
+
on(event, handler) {
|
|
62
|
+
// Subscribe to local bus only — child doesn't see parent/sibling events
|
|
63
|
+
return localBus.on(event, handler);
|
|
64
|
+
},
|
|
65
|
+
emit(event, data) {
|
|
66
|
+
// Always emit on local bus for child's own listeners
|
|
67
|
+
localBus.emit(event, data);
|
|
68
|
+
// Forward lifecycle events to parent bus for parent widget visibility
|
|
69
|
+
if (FORWARDABLE_EVENTS.has(event)) {
|
|
70
|
+
parentBus.emit(event, data);
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
43
75
|
const AUTO_EXPOSE_EXTENSION_NAMES = new Set(["pi-c2c"]);
|
|
44
76
|
let extensionDepthLoadChain = Promise.resolve();
|
|
45
77
|
const packageNameCache = new Map();
|
|
@@ -438,6 +470,10 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
438
470
|
}),
|
|
439
471
|
};
|
|
440
472
|
};
|
|
473
|
+
// Create a forwarding event bus so the child session's lifecycle events
|
|
474
|
+
// (subagents:created, subagents:started, etc.) propagate to the parent's
|
|
475
|
+
// event bus — making depth 2+ agents visible in the parent widget.
|
|
476
|
+
const childEventBus = options.eventBus ? createForwardingEventBus(options.eventBus) : undefined;
|
|
441
477
|
const loader = new DefaultResourceLoader({
|
|
442
478
|
cwd: configCwd,
|
|
443
479
|
agentDir,
|
|
@@ -450,6 +486,7 @@ export async function runAgent(ctx, type, prompt, options) {
|
|
|
450
486
|
noContextFiles: true,
|
|
451
487
|
systemPromptOverride: () => systemPrompt,
|
|
452
488
|
appendSystemPromptOverride: () => [],
|
|
489
|
+
eventBus: childEventBus,
|
|
453
490
|
});
|
|
454
491
|
await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
|
|
455
492
|
// Plain entries in `tools:` are expected to be built-in names (extension tools
|
|
@@ -2,7 +2,13 @@ import type { ToolDescriptionMode } from "./settings.js";
|
|
|
2
2
|
export declare function getModelLabelFromConfig(model: string): string;
|
|
3
3
|
export interface AgentToolDescriptionOptions {
|
|
4
4
|
mode: ToolDescriptionMode;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Depth at which the NEXT spawned subagent will run.
|
|
7
|
+
* This is `extensionDepth + 1` — the agent's own depth plus one.
|
|
8
|
+
* Displayed as "Current recursive depth" in the tool description so the
|
|
9
|
+
* LLM sees the depth of the agent it is about to create, not its own depth.
|
|
10
|
+
*/
|
|
11
|
+
nextSubagentDepth: number;
|
|
6
12
|
schedulingEnabled: boolean;
|
|
7
13
|
}
|
|
8
14
|
export declare function buildScheduleGuideline(schedulingEnabled: boolean): string;
|
|
@@ -39,7 +39,7 @@ export function buildScheduleGuideline(schedulingEnabled) {
|
|
|
39
39
|
}
|
|
40
40
|
export function buildAgentToolDescription(options) {
|
|
41
41
|
const scheduleGuideline = buildScheduleGuideline(options.schedulingEnabled);
|
|
42
|
-
const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.
|
|
42
|
+
const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}.`;
|
|
43
43
|
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
44
44
|
${buildCompactTypeListText()}
|
|
45
45
|
|
|
@@ -49,7 +49,7 @@ Notes:
|
|
|
49
49
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
50
50
|
- Parallel work: one message, multiple Agent calls; they all run in the background. You are notified when agents finish — never poll or sleep.
|
|
51
51
|
- Background by default: when you have useful independent work, launch it and continue. Doing nothing while an agent runs is worse than letting background work proceed.
|
|
52
|
-
- Recursive agents: current depth ${options.
|
|
52
|
+
- Recursive agents: current depth ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
|
|
53
53
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
54
54
|
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
55
55
|
- list_models enumerates the model registry the \`model:\` param accepts — call it before picking a model explicitly.
|
|
@@ -105,7 +105,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
105
105
|
compactTypeList: buildCompactTypeListText,
|
|
106
106
|
agentDir: getAgentDir,
|
|
107
107
|
scheduleGuideline: () => scheduleGuideline,
|
|
108
|
-
currentDepth: () => String(options.
|
|
108
|
+
currentDepth: () => String(options.nextSubagentDepth),
|
|
109
109
|
maxDepth: () => String(MAX_RECURSIVE_DEPTH),
|
|
110
110
|
recursiveGuideline: () => recursiveGuideline,
|
|
111
111
|
};
|
|
@@ -33,6 +33,10 @@ export interface RpcDeps {
|
|
|
33
33
|
pi: unknown;
|
|
34
34
|
getCtx: () => unknown | undefined;
|
|
35
35
|
manager: SpawnCapable;
|
|
36
|
+
/** Default recursive depth for RPC-spawned subagents in this session. */
|
|
37
|
+
depth?: number;
|
|
38
|
+
/** Parent subagent id for RPC-spawned subagents in this session. */
|
|
39
|
+
parentAgentId?: string;
|
|
36
40
|
}
|
|
37
41
|
export interface RpcHandle {
|
|
38
42
|
unsubPing: () => void;
|
|
@@ -66,7 +66,17 @@ export function registerRpcHandlers(deps) {
|
|
|
66
66
|
}
|
|
67
67
|
normalizedOptions = { ...normalizedOptions, model: resolved };
|
|
68
68
|
}
|
|
69
|
-
|
|
69
|
+
const spawnOptions = {
|
|
70
|
+
...normalizedOptions,
|
|
71
|
+
eventBus: events,
|
|
72
|
+
depth: normalizedOptions.depth ?? deps.depth,
|
|
73
|
+
parentAgentId: normalizedOptions.parentAgentId ?? deps.parentAgentId,
|
|
74
|
+
};
|
|
75
|
+
if (spawnOptions.depth === undefined)
|
|
76
|
+
delete spawnOptions.depth;
|
|
77
|
+
if (spawnOptions.parentAgentId === undefined)
|
|
78
|
+
delete spawnOptions.parentAgentId;
|
|
79
|
+
return { id: manager.spawn(pi, ctx, type, prompt, spawnOptions) };
|
|
70
80
|
});
|
|
71
81
|
const unsubStop = handleRpc(events, "subagents:rpc:stop", ({ agentId }) => {
|
|
72
82
|
if (!manager.abort(agentId))
|
package/dist/dashboard-ui.js
CHANGED
|
@@ -24,6 +24,7 @@ function buildAgentRow(record) {
|
|
|
24
24
|
id: record.id,
|
|
25
25
|
type: getDisplayName(record.type),
|
|
26
26
|
description: record.description ?? "",
|
|
27
|
+
model: record.invocation?.modelName ?? "—",
|
|
27
28
|
status: record.status,
|
|
28
29
|
toolUses: record.toolUses ?? 0,
|
|
29
30
|
tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
|
|
@@ -56,7 +57,14 @@ export function registerDashboardModules(pi, manager) {
|
|
|
56
57
|
}, INVALIDATE_DEBOUNCE_MS);
|
|
57
58
|
}
|
|
58
59
|
// ── 1. Module Discovery (ui:list-modules) ──────────────────────────
|
|
60
|
+
// Guard against duplicate pushes: the bridge may call refreshUiModules
|
|
61
|
+
// multiple times per probe cycle when multiple sessions each register
|
|
62
|
+
// their own ui:invalidate listener. Check if our modules are already
|
|
63
|
+
// present before pushing.
|
|
59
64
|
pi.events.on("ui:list-modules", ((probe) => {
|
|
65
|
+
const alreadyContributed = probe.modules.some((m) => m.kind === "management-modal" && m.id === MODULE_ID);
|
|
66
|
+
if (alreadyContributed)
|
|
67
|
+
return;
|
|
60
68
|
const agents = manager.listAgents();
|
|
61
69
|
const running = agents.filter(a => a.status === "running").length;
|
|
62
70
|
const completed = agents.filter(a => a.status === "completed").length;
|
|
@@ -96,6 +104,7 @@ export function registerDashboardModules(pi, manager) {
|
|
|
96
104
|
{ key: "id", label: "ID", kind: "text", width: 120 },
|
|
97
105
|
{ key: "type", label: "Type", kind: "text", width: 100 },
|
|
98
106
|
{ key: "description", label: "Description", kind: "text" },
|
|
107
|
+
{ key: "model", label: "Model", kind: "text", width: 80 },
|
|
99
108
|
{ key: "status", label: "Status", kind: "text", width: 90 },
|
|
100
109
|
{ key: "toolUses", label: "Tools", kind: "number", width: 60 },
|
|
101
110
|
{ key: "tokens", label: "Tokens", kind: "text", width: 80 },
|
|
@@ -148,20 +157,25 @@ export function registerDashboardModules(pi, manager) {
|
|
|
148
157
|
pi.events.on("subagents:ui:refresh", (() => {
|
|
149
158
|
scheduleInvalidate();
|
|
150
159
|
}));
|
|
151
|
-
// View Result:
|
|
160
|
+
// View Result: return the agent's result as table rows so the modal
|
|
161
|
+
// displays it. The bridge's synchronous fast path calls `_reply(items)`
|
|
162
|
+
// when `data.items` is populated by the handler — do NOT call
|
|
163
|
+
// `scheduleInvalidate()` here as the subsequent re-probe would
|
|
164
|
+
// overwrite the returned rows with the original table data.
|
|
152
165
|
pi.events.on("subagents:ui:view-result", ((data) => {
|
|
153
|
-
|
|
166
|
+
// Bridge spreads msg.params into data; row identity is at data.row.id.
|
|
167
|
+
const agentId = data.row?.id ?? data.id;
|
|
168
|
+
if (!agentId)
|
|
169
|
+
return;
|
|
154
170
|
const record = manager.getRecord(agentId);
|
|
155
|
-
if (!record)
|
|
156
|
-
pi.events.emit("ui:invalidate", { id: MODULE_ID });
|
|
171
|
+
if (!record)
|
|
157
172
|
return;
|
|
158
|
-
}
|
|
159
173
|
const resultText = record.result?.trim() || "No output yet.";
|
|
160
174
|
const preview = resultText.length > 2000
|
|
161
175
|
? resultText.slice(0, 2000) + "\n…(truncated)"
|
|
162
176
|
: resultText;
|
|
163
|
-
|
|
164
|
-
//
|
|
177
|
+
// Populate data.items — the bridge's synchronous fast path forwards
|
|
178
|
+
// this as a `ui_data_list` message back to the dashboard.
|
|
165
179
|
data.items = [{
|
|
166
180
|
id: record.id,
|
|
167
181
|
type: getDisplayName(record.type),
|
|
@@ -171,25 +185,36 @@ export function registerDashboardModules(pi, manager) {
|
|
|
171
185
|
outputFile: record.outputFile ?? "",
|
|
172
186
|
}];
|
|
173
187
|
}));
|
|
174
|
-
// Abort:
|
|
188
|
+
// Abort: stop the running agent via the manager's abort() method
|
|
189
|
+
// which properly cancels the AbortController and cleans up state.
|
|
175
190
|
pi.events.on("subagents:ui:abort", ((data) => {
|
|
176
|
-
const agentId = data.
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
record.session.dispose?.();
|
|
182
|
-
}
|
|
183
|
-
catch {
|
|
184
|
-
// Ignore disposal errors
|
|
185
|
-
}
|
|
186
|
-
}
|
|
191
|
+
const agentId = data.row?.id ?? data.id;
|
|
192
|
+
if (!agentId)
|
|
193
|
+
return;
|
|
194
|
+
manager.abort(agentId);
|
|
187
195
|
scheduleInvalidate();
|
|
188
196
|
}));
|
|
189
|
-
// Steer:
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
197
|
+
// Steer: send a steering message to a running agent's session.
|
|
198
|
+
// The management-modal row action carries the row identity; we steer
|
|
199
|
+
// with a default "Continue" nudge. A future form view could accept
|
|
200
|
+
// custom text.
|
|
201
|
+
pi.events.on("subagents:ui:steer", ((data) => {
|
|
202
|
+
const agentId = data.row?.id ?? data.id;
|
|
203
|
+
if (!agentId)
|
|
204
|
+
return;
|
|
205
|
+
const record = manager.getRecord(agentId);
|
|
206
|
+
if (!record)
|
|
207
|
+
return;
|
|
208
|
+
if (record.status === "running" && record.session) {
|
|
209
|
+
// Session is live — steer immediately
|
|
210
|
+
record.session.steer("Continue").catch(() => { });
|
|
211
|
+
}
|
|
212
|
+
else if (record.status === "queued") {
|
|
213
|
+
// Session not yet created — queue the steer for flush on start
|
|
214
|
+
if (!record.pendingSteers)
|
|
215
|
+
record.pendingSteers = [];
|
|
216
|
+
record.pendingSteers.push("Continue");
|
|
217
|
+
}
|
|
193
218
|
scheduleInvalidate();
|
|
194
219
|
}));
|
|
195
220
|
// ── 4. Invalidate on agent lifecycle events ────────────────────────
|
package/dist/index.js
CHANGED
|
@@ -379,7 +379,7 @@ export default function (pi) {
|
|
|
379
379
|
return; // sessionId not yet available — try again on next event
|
|
380
380
|
const path = resolveStorePath(ctx.cwd, sessionId);
|
|
381
381
|
const store = new ScheduleStore(path);
|
|
382
|
-
scheduler.start(pi, ctx, manager, store);
|
|
382
|
+
scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
|
|
383
383
|
pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
|
|
384
384
|
}
|
|
385
385
|
catch (err) {
|
|
@@ -412,6 +412,8 @@ export default function (pi) {
|
|
|
412
412
|
pi,
|
|
413
413
|
getCtx: () => currentCtx,
|
|
414
414
|
manager,
|
|
415
|
+
depth: nextSubagentDepth,
|
|
416
|
+
parentAgentId: extensionAgentId,
|
|
415
417
|
});
|
|
416
418
|
// Broadcast readiness so extensions loaded after us can discover us
|
|
417
419
|
pi.events.emit("subagents:ready", {});
|
|
@@ -594,7 +596,7 @@ export default function (pi) {
|
|
|
594
596
|
const scheduleParam = isSchedulingEnabled() ? scheduleParamShape : {};
|
|
595
597
|
const agentToolDescription = buildAgentToolDescription({
|
|
596
598
|
mode: getToolDescriptionMode(),
|
|
597
|
-
|
|
599
|
+
nextSubagentDepth,
|
|
598
600
|
schedulingEnabled: isSchedulingEnabled(),
|
|
599
601
|
});
|
|
600
602
|
pi.registerTool(defineTool({
|
|
@@ -872,6 +874,7 @@ export default function (pi) {
|
|
|
872
874
|
invocation: agentInvocation,
|
|
873
875
|
depth: nextSubagentDepth,
|
|
874
876
|
parentAgentId: extensionAgentId,
|
|
877
|
+
eventBus: pi.events,
|
|
875
878
|
outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
|
|
876
879
|
onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt, ctx.cwd),
|
|
877
880
|
...bgCallbacks,
|
|
@@ -1632,6 +1635,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1632
1635
|
const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
|
|
1633
1636
|
description: `Generate ${name} agent`,
|
|
1634
1637
|
maxTurns: 5,
|
|
1638
|
+
eventBus: pi.events,
|
|
1639
|
+
depth: nextSubagentDepth,
|
|
1640
|
+
parentAgentId: extensionAgentId,
|
|
1635
1641
|
});
|
|
1636
1642
|
if (record.status === "error") {
|
|
1637
1643
|
ctx.ui.notify(`Generation failed: ${record.error}`, "warning");
|
package/dist/schedule.d.ts
CHANGED
|
@@ -51,6 +51,12 @@ export interface NewJobInput {
|
|
|
51
51
|
isolated?: boolean;
|
|
52
52
|
isolation?: IsolationMode;
|
|
53
53
|
}
|
|
54
|
+
interface SchedulerSpawnDefaults {
|
|
55
|
+
/** Recursive depth for scheduled subagents fired from this session. */
|
|
56
|
+
depth?: number;
|
|
57
|
+
/** Parent subagent id for scheduled subagents fired from this session. */
|
|
58
|
+
parentAgentId?: string;
|
|
59
|
+
}
|
|
54
60
|
export declare class SubagentScheduler {
|
|
55
61
|
private jobs;
|
|
56
62
|
private intervals;
|
|
@@ -58,8 +64,9 @@ export declare class SubagentScheduler {
|
|
|
58
64
|
private pi;
|
|
59
65
|
private ctx;
|
|
60
66
|
private manager;
|
|
67
|
+
private spawnDefaults;
|
|
61
68
|
/** Start the scheduler: bind to a session's store and arm enabled jobs. */
|
|
62
|
-
start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore): void;
|
|
69
|
+
start(pi: ExtensionAPI, ctx: ExtensionContext, manager: AgentManager, store: ScheduleStore, spawnDefaults?: SchedulerSpawnDefaults): void;
|
|
63
70
|
/** Stop all timers; drop refs. Safe to call repeatedly. */
|
|
64
71
|
stop(): void;
|
|
65
72
|
/** True if start() has bound a store and the scheduler is active. */
|
|
@@ -107,3 +114,4 @@ export declare class SubagentScheduler {
|
|
|
107
114
|
/** "10s"/"5m"/"1h"/"2d" → milliseconds. */
|
|
108
115
|
static parseInterval(s: string): number | null;
|
|
109
116
|
}
|
|
117
|
+
export {};
|
package/dist/schedule.js
CHANGED
|
@@ -24,12 +24,14 @@ export class SubagentScheduler {
|
|
|
24
24
|
pi;
|
|
25
25
|
ctx;
|
|
26
26
|
manager;
|
|
27
|
+
spawnDefaults = {};
|
|
27
28
|
/** Start the scheduler: bind to a session's store and arm enabled jobs. */
|
|
28
|
-
start(pi, ctx, manager, store) {
|
|
29
|
+
start(pi, ctx, manager, store, spawnDefaults = {}) {
|
|
29
30
|
this.pi = pi;
|
|
30
31
|
this.ctx = ctx;
|
|
31
32
|
this.manager = manager;
|
|
32
33
|
this.store = store;
|
|
34
|
+
this.spawnDefaults = spawnDefaults;
|
|
33
35
|
for (const job of store.list()) {
|
|
34
36
|
if (job.enabled)
|
|
35
37
|
this.scheduleJob(job);
|
|
@@ -47,6 +49,7 @@ export class SubagentScheduler {
|
|
|
47
49
|
this.pi = undefined;
|
|
48
50
|
this.ctx = undefined;
|
|
49
51
|
this.manager = undefined;
|
|
52
|
+
this.spawnDefaults = {};
|
|
50
53
|
}
|
|
51
54
|
/** True if start() has bound a store and the scheduler is active. */
|
|
52
55
|
isActive() {
|
|
@@ -222,6 +225,9 @@ export class SubagentScheduler {
|
|
|
222
225
|
isolated: job.isolated,
|
|
223
226
|
thinkingLevel: job.thinking,
|
|
224
227
|
isolation: job.isolation,
|
|
228
|
+
eventBus: pi.events,
|
|
229
|
+
depth: this.spawnDefaults.depth,
|
|
230
|
+
parentAgentId: this.spawnDefaults.parentAgentId,
|
|
225
231
|
});
|
|
226
232
|
}
|
|
227
233
|
catch (err) {
|
package/package.json
CHANGED
package/src/agent-manager.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { isAbsolute } from "node:path";
|
|
|
12
12
|
import type { Model } from "@earendil-works/pi-ai";
|
|
13
13
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
14
14
|
import { resumeAgent, runAgent, type ToolActivity } from "./agent-runner.js";
|
|
15
|
+
import type { EventBus } from "./cross-extension-rpc.js";
|
|
15
16
|
import { type AgentInvocation, type AgentRecord, type IsolationMode, MAX_RECURSIVE_DEPTH, type SubagentType, type ThinkingLevel } from "./types.js";
|
|
16
17
|
import { addUsage } from "./usage.js";
|
|
17
18
|
import { cleanupWorktree, createWorktree, pruneWorktrees, } from "./worktree.js";
|
|
@@ -102,6 +103,8 @@ interface SpawnOptions {
|
|
|
102
103
|
onAssistantUsage?: (usage: { input: number; output: number; cacheWrite: number }) => void;
|
|
103
104
|
/** Called when the session successfully compacts. */
|
|
104
105
|
onCompaction?: (info: CompactionInfo) => void;
|
|
106
|
+
/** Parent's event bus — shared with child sessions so lifecycle events propagate to the parent widget. */
|
|
107
|
+
eventBus?: EventBus;
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
interface ResumeOptions {
|
|
@@ -310,6 +313,7 @@ export class AgentManager {
|
|
|
310
313
|
},
|
|
311
314
|
depth: record.depth,
|
|
312
315
|
parentAgentId: record.parentAgentId,
|
|
316
|
+
eventBus: options.eventBus,
|
|
313
317
|
onSessionCreated: (session) => {
|
|
314
318
|
record.session = session;
|
|
315
319
|
// Flush any steers that arrived before the session was ready
|
package/src/agent-runner.ts
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
type AgentSession,
|
|
12
12
|
type AgentSessionEvent,
|
|
13
13
|
createAgentSession,
|
|
14
|
+
createEventBus,
|
|
14
15
|
DefaultResourceLoader,
|
|
15
16
|
type ExtensionAPI,
|
|
16
17
|
getAgentDir,
|
|
@@ -19,6 +20,7 @@ import {
|
|
|
19
20
|
} from "@earendil-works/pi-coding-agent";
|
|
20
21
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getConfig, getMemoryToolNames, getReadOnlyMemoryToolNames, getToolNamesForType } from "./agent-types.js";
|
|
21
22
|
import { buildParentContext, extractText } from "./context.js";
|
|
23
|
+
import type { EventBus } from "./cross-extension-rpc.js";
|
|
22
24
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
23
25
|
import { detectEnv } from "./env.js";
|
|
24
26
|
import { buildMemoryBlock, buildReadOnlyMemoryBlock } from "./memory.js";
|
|
@@ -55,6 +57,40 @@ const RECURSIVE_TOOL_NAMES: string[] = [
|
|
|
55
57
|
];
|
|
56
58
|
|
|
57
59
|
const EXTENSION_DEPTH_KEY = Symbol.for("pi-subagents:extension-depth");
|
|
60
|
+
|
|
61
|
+
/** Lifecycle event names that should propagate from child to parent sessions. */
|
|
62
|
+
const FORWARDABLE_EVENTS = new Set([
|
|
63
|
+
"subagents:created",
|
|
64
|
+
"subagents:started",
|
|
65
|
+
"subagents:completed",
|
|
66
|
+
"subagents:failed",
|
|
67
|
+
"subagents:compacted",
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Create a forwarding event bus for a child session.
|
|
72
|
+
* The child gets its own local bus for emit/on, but lifecycle events
|
|
73
|
+
* (subagents:*) are also forwarded to the parent bus so the parent widget
|
|
74
|
+
* can display depth 2+ agents.
|
|
75
|
+
*/
|
|
76
|
+
export function createForwardingEventBus(parentBus: EventBus): EventBus {
|
|
77
|
+
// Use the parent's EventBus factory to create a properly isolated local bus
|
|
78
|
+
const localBus = createEventBus();
|
|
79
|
+
return {
|
|
80
|
+
on(event, handler) {
|
|
81
|
+
// Subscribe to local bus only — child doesn't see parent/sibling events
|
|
82
|
+
return localBus.on(event, handler);
|
|
83
|
+
},
|
|
84
|
+
emit(event, data) {
|
|
85
|
+
// Always emit on local bus for child's own listeners
|
|
86
|
+
localBus.emit(event, data);
|
|
87
|
+
// Forward lifecycle events to parent bus for parent widget visibility
|
|
88
|
+
if (FORWARDABLE_EVENTS.has(event)) {
|
|
89
|
+
parentBus.emit(event, data);
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
58
94
|
const AUTO_EXPOSE_EXTENSION_NAMES = new Set(["pi-c2c"]);
|
|
59
95
|
let extensionDepthLoadChain: Promise<void> = Promise.resolve();
|
|
60
96
|
const packageNameCache = new Map<string, string[]>();
|
|
@@ -365,6 +401,9 @@ export interface RunOptions {
|
|
|
365
401
|
depth?: number;
|
|
366
402
|
/** Parent subagent id when spawned recursively from another subagent. */
|
|
367
403
|
parentAgentId?: string;
|
|
404
|
+
/** Parent's event bus — shared with the child session so lifecycle events
|
|
405
|
+
* (subagents:created, subagents:started, etc.) propagate to the parent widget. */
|
|
406
|
+
eventBus?: EventBus;
|
|
368
407
|
}
|
|
369
408
|
|
|
370
409
|
export interface RunResult {
|
|
@@ -555,6 +594,11 @@ export async function runAgent(
|
|
|
555
594
|
};
|
|
556
595
|
};
|
|
557
596
|
|
|
597
|
+
// Create a forwarding event bus so the child session's lifecycle events
|
|
598
|
+
// (subagents:created, subagents:started, etc.) propagate to the parent's
|
|
599
|
+
// event bus — making depth 2+ agents visible in the parent widget.
|
|
600
|
+
const childEventBus = options.eventBus ? createForwardingEventBus(options.eventBus) : undefined;
|
|
601
|
+
|
|
558
602
|
const loader = new DefaultResourceLoader({
|
|
559
603
|
cwd: configCwd,
|
|
560
604
|
agentDir,
|
|
@@ -567,6 +611,7 @@ export async function runAgent(
|
|
|
567
611
|
noContextFiles: true,
|
|
568
612
|
systemPromptOverride: () => systemPrompt,
|
|
569
613
|
appendSystemPromptOverride: () => [],
|
|
614
|
+
eventBus: childEventBus,
|
|
570
615
|
});
|
|
571
616
|
await withLoadingExtensionDepth(depth, options.agentId, options.parentAgentId, () => loader.reload());
|
|
572
617
|
|
|
@@ -44,7 +44,13 @@ const buildCompactTypeListText = () =>
|
|
|
44
44
|
|
|
45
45
|
export interface AgentToolDescriptionOptions {
|
|
46
46
|
mode: ToolDescriptionMode;
|
|
47
|
-
|
|
47
|
+
/**
|
|
48
|
+
* Depth at which the NEXT spawned subagent will run.
|
|
49
|
+
* This is `extensionDepth + 1` — the agent's own depth plus one.
|
|
50
|
+
* Displayed as "Current recursive depth" in the tool description so the
|
|
51
|
+
* LLM sees the depth of the agent it is about to create, not its own depth.
|
|
52
|
+
*/
|
|
53
|
+
nextSubagentDepth: number;
|
|
48
54
|
schedulingEnabled: boolean;
|
|
49
55
|
}
|
|
50
56
|
|
|
@@ -56,7 +62,7 @@ export function buildScheduleGuideline(schedulingEnabled: boolean): string {
|
|
|
56
62
|
|
|
57
63
|
export function buildAgentToolDescription(options: AgentToolDescriptionOptions): string {
|
|
58
64
|
const scheduleGuideline = buildScheduleGuideline(options.schedulingEnabled);
|
|
59
|
-
const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.
|
|
65
|
+
const recursiveGuideline = `Recursive agents are allowed through depth ${MAX_RECURSIVE_DEPTH}. Current recursive depth: ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}.`;
|
|
60
66
|
|
|
61
67
|
const compactAgentToolDescription = `Launch an autonomous agent for complex, multi-step tasks. Agent types:
|
|
62
68
|
${buildCompactTypeListText()}
|
|
@@ -67,7 +73,7 @@ Notes:
|
|
|
67
73
|
- description: 3-5 words (shown in UI). Prompts must be self-contained — the agent has not seen this conversation.
|
|
68
74
|
- Parallel work: one message, multiple Agent calls; they all run in the background. You are notified when agents finish — never poll or sleep.
|
|
69
75
|
- Background by default: when you have useful independent work, launch it and continue. Doing nothing while an agent runs is worse than letting background work proceed.
|
|
70
|
-
- Recursive agents: current depth ${options.
|
|
76
|
+
- Recursive agents: current depth ${options.nextSubagentDepth}/${MAX_RECURSIVE_DEPTH}; you may spawn subagents until depth ${MAX_RECURSIVE_DEPTH}.
|
|
71
77
|
- The result is not shown to the user — summarize it for them. Verify an agent's claimed code changes before reporting work done.
|
|
72
78
|
- resume continues a previous agent by ID; steer_subagent messages a running one.
|
|
73
79
|
- list_models enumerates the model registry the \`model:\` param accepts — call it before picking a model explicitly.
|
|
@@ -125,7 +131,7 @@ Terse command-style prompts produce shallow, generic work.
|
|
|
125
131
|
compactTypeList: buildCompactTypeListText,
|
|
126
132
|
agentDir: getAgentDir,
|
|
127
133
|
scheduleGuideline: () => scheduleGuideline,
|
|
128
|
-
currentDepth: () => String(options.
|
|
134
|
+
currentDepth: () => String(options.nextSubagentDepth),
|
|
129
135
|
maxDepth: () => String(MAX_RECURSIVE_DEPTH),
|
|
130
136
|
recursiveGuideline: () => recursiveGuideline,
|
|
131
137
|
};
|
|
@@ -36,6 +36,10 @@ export interface RpcDeps {
|
|
|
36
36
|
pi: unknown; // passed through to manager.spawn
|
|
37
37
|
getCtx: () => unknown | undefined; // returns current ExtensionContext
|
|
38
38
|
manager: SpawnCapable;
|
|
39
|
+
/** Default recursive depth for RPC-spawned subagents in this session. */
|
|
40
|
+
depth?: number;
|
|
41
|
+
/** Parent subagent id for RPC-spawned subagents in this session. */
|
|
42
|
+
parentAgentId?: string;
|
|
39
43
|
}
|
|
40
44
|
|
|
41
45
|
export interface RpcHandle {
|
|
@@ -108,7 +112,16 @@ export function registerRpcHandlers(deps: RpcDeps): RpcHandle {
|
|
|
108
112
|
normalizedOptions = { ...normalizedOptions, model: resolved };
|
|
109
113
|
}
|
|
110
114
|
|
|
111
|
-
|
|
115
|
+
const spawnOptions = {
|
|
116
|
+
...normalizedOptions,
|
|
117
|
+
eventBus: events,
|
|
118
|
+
depth: normalizedOptions.depth ?? deps.depth,
|
|
119
|
+
parentAgentId: normalizedOptions.parentAgentId ?? deps.parentAgentId,
|
|
120
|
+
};
|
|
121
|
+
if (spawnOptions.depth === undefined) delete spawnOptions.depth;
|
|
122
|
+
if (spawnOptions.parentAgentId === undefined) delete spawnOptions.parentAgentId;
|
|
123
|
+
|
|
124
|
+
return { id: manager.spawn(pi, ctx, type, prompt, spawnOptions) };
|
|
112
125
|
},
|
|
113
126
|
);
|
|
114
127
|
|
package/src/dashboard-ui.ts
CHANGED
|
@@ -77,6 +77,7 @@ function buildAgentRow(record: any) {
|
|
|
77
77
|
id: record.id,
|
|
78
78
|
type: getDisplayName(record.type),
|
|
79
79
|
description: record.description ?? "",
|
|
80
|
+
model: record.invocation?.modelName ?? "—",
|
|
80
81
|
status: record.status,
|
|
81
82
|
toolUses: record.toolUses ?? 0,
|
|
82
83
|
tokens: totalTokens > 0 ? formatTokenCount(totalTokens) : "—",
|
|
@@ -110,7 +111,16 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
// ── 1. Module Discovery (ui:list-modules) ──────────────────────────
|
|
114
|
+
// Guard against duplicate pushes: the bridge may call refreshUiModules
|
|
115
|
+
// multiple times per probe cycle when multiple sessions each register
|
|
116
|
+
// their own ui:invalidate listener. Check if our modules are already
|
|
117
|
+
// present before pushing.
|
|
113
118
|
pi.events.on("ui:list-modules", ((probe: ModuleProbe) => {
|
|
119
|
+
const alreadyContributed = probe.modules.some(
|
|
120
|
+
(m: any) => m.kind === "management-modal" && m.id === MODULE_ID,
|
|
121
|
+
);
|
|
122
|
+
if (alreadyContributed) return;
|
|
123
|
+
|
|
114
124
|
const agents = manager.listAgents();
|
|
115
125
|
const running = agents.filter(a => a.status === "running").length;
|
|
116
126
|
const completed = agents.filter(a => a.status === "completed").length;
|
|
@@ -150,6 +160,7 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
|
|
|
150
160
|
{ key: "id", label: "ID", kind: "text", width: 120 },
|
|
151
161
|
{ key: "type", label: "Type", kind: "text", width: 100 },
|
|
152
162
|
{ key: "description", label: "Description", kind: "text" },
|
|
163
|
+
{ key: "model", label: "Model", kind: "text", width: 80 },
|
|
153
164
|
{ key: "status", label: "Status", kind: "text", width: 90 },
|
|
154
165
|
{ key: "toolUses", label: "Tools", kind: "number", width: 60 },
|
|
155
166
|
{ key: "tokens", label: "Tokens", kind: "text", width: 80 },
|
|
@@ -206,23 +217,25 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
|
|
|
206
217
|
scheduleInvalidate();
|
|
207
218
|
}) as any);
|
|
208
219
|
|
|
209
|
-
// View Result:
|
|
220
|
+
// View Result: return the agent's result as table rows so the modal
|
|
221
|
+
// displays it. The bridge's synchronous fast path calls `_reply(items)`
|
|
222
|
+
// when `data.items` is populated by the handler — do NOT call
|
|
223
|
+
// `scheduleInvalidate()` here as the subsequent re-probe would
|
|
224
|
+
// overwrite the returned rows with the original table data.
|
|
210
225
|
pi.events.on("subagents:ui:view-result", ((data: any) => {
|
|
211
|
-
|
|
226
|
+
// Bridge spreads msg.params into data; row identity is at data.row.id.
|
|
227
|
+
const agentId = data.row?.id ?? data.id;
|
|
228
|
+
if (!agentId) return;
|
|
212
229
|
const record = manager.getRecord(agentId);
|
|
213
|
-
if (!record)
|
|
214
|
-
pi.events.emit("ui:invalidate", { id: MODULE_ID });
|
|
215
|
-
return;
|
|
216
|
-
}
|
|
230
|
+
if (!record) return;
|
|
217
231
|
|
|
218
232
|
const resultText = record.result?.trim() || "No output yet.";
|
|
219
233
|
const preview = resultText.length > 2000
|
|
220
234
|
? resultText.slice(0, 2000) + "\n…(truncated)"
|
|
221
235
|
: resultText;
|
|
222
236
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
// Return result as items so it shows in a detail view
|
|
237
|
+
// Populate data.items — the bridge's synchronous fast path forwards
|
|
238
|
+
// this as a `ui_data_list` message back to the dashboard.
|
|
226
239
|
data.items = [{
|
|
227
240
|
id: record.id,
|
|
228
241
|
type: getDisplayName(record.type),
|
|
@@ -233,25 +246,33 @@ export function registerDashboardModules(pi: ExtensionAPI, manager: AgentManager
|
|
|
233
246
|
}];
|
|
234
247
|
}) as any);
|
|
235
248
|
|
|
236
|
-
// Abort:
|
|
249
|
+
// Abort: stop the running agent via the manager's abort() method
|
|
250
|
+
// which properly cancels the AbortController and cleans up state.
|
|
237
251
|
pi.events.on("subagents:ui:abort", ((data: any) => {
|
|
238
|
-
const agentId = data.
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
// Use the session's abort mechanism
|
|
242
|
-
try {
|
|
243
|
-
record.session.dispose?.();
|
|
244
|
-
} catch {
|
|
245
|
-
// Ignore disposal errors
|
|
246
|
-
}
|
|
247
|
-
}
|
|
252
|
+
const agentId = data.row?.id ?? data.id;
|
|
253
|
+
if (!agentId) return;
|
|
254
|
+
manager.abort(agentId);
|
|
248
255
|
scheduleInvalidate();
|
|
249
256
|
}) as any);
|
|
250
257
|
|
|
251
|
-
// Steer:
|
|
252
|
-
//
|
|
253
|
-
|
|
254
|
-
|
|
258
|
+
// Steer: send a steering message to a running agent's session.
|
|
259
|
+
// The management-modal row action carries the row identity; we steer
|
|
260
|
+
// with a default "Continue" nudge. A future form view could accept
|
|
261
|
+
// custom text.
|
|
262
|
+
pi.events.on("subagents:ui:steer", ((data: any) => {
|
|
263
|
+
const agentId = data.row?.id ?? data.id;
|
|
264
|
+
if (!agentId) return;
|
|
265
|
+
const record = manager.getRecord(agentId);
|
|
266
|
+
if (!record) return;
|
|
267
|
+
|
|
268
|
+
if (record.status === "running" && record.session) {
|
|
269
|
+
// Session is live — steer immediately
|
|
270
|
+
record.session.steer("Continue").catch(() => {});
|
|
271
|
+
} else if (record.status === "queued") {
|
|
272
|
+
// Session not yet created — queue the steer for flush on start
|
|
273
|
+
if (!record.pendingSteers) record.pendingSteers = [];
|
|
274
|
+
record.pendingSteers.push("Continue");
|
|
275
|
+
}
|
|
255
276
|
scheduleInvalidate();
|
|
256
277
|
}) as any);
|
|
257
278
|
|
package/src/index.ts
CHANGED
|
@@ -444,7 +444,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
444
444
|
if (!sessionId) return; // sessionId not yet available — try again on next event
|
|
445
445
|
const path = resolveStorePath(ctx.cwd, sessionId);
|
|
446
446
|
const store = new ScheduleStore(path);
|
|
447
|
-
scheduler.start(pi, ctx, manager, store);
|
|
447
|
+
scheduler.start(pi, ctx, manager, store, { depth: nextSubagentDepth, parentAgentId: extensionAgentId });
|
|
448
448
|
pi.events.emit("subagents:scheduler_ready", { sessionId, jobCount: store.list().length });
|
|
449
449
|
} catch (err) {
|
|
450
450
|
// Scheduling is non-essential — log and move on so the rest of the
|
|
@@ -478,6 +478,8 @@ export default function (pi: ExtensionAPI) {
|
|
|
478
478
|
pi,
|
|
479
479
|
getCtx: () => currentCtx,
|
|
480
480
|
manager,
|
|
481
|
+
depth: nextSubagentDepth,
|
|
482
|
+
parentAgentId: extensionAgentId,
|
|
481
483
|
});
|
|
482
484
|
|
|
483
485
|
// Broadcast readiness so extensions loaded after us can discover us
|
|
@@ -683,7 +685,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
683
685
|
|
|
684
686
|
const agentToolDescription = buildAgentToolDescription({
|
|
685
687
|
mode: getToolDescriptionMode(),
|
|
686
|
-
|
|
688
|
+
nextSubagentDepth,
|
|
687
689
|
schedulingEnabled: isSchedulingEnabled(),
|
|
688
690
|
});
|
|
689
691
|
|
|
@@ -1016,6 +1018,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1016
1018
|
invocation: agentInvocation,
|
|
1017
1019
|
depth: nextSubagentDepth,
|
|
1018
1020
|
parentAgentId: extensionAgentId,
|
|
1021
|
+
eventBus: pi.events,
|
|
1019
1022
|
outputFileForAgent: (agentId) => createOutputFilePath(ctx.cwd, agentId, ctx.sessionManager.getSessionId()),
|
|
1020
1023
|
onOutputFileCreated: (outputFile, agentId) => writeInitialEntry(outputFile, agentId, P.prompt!, ctx.cwd),
|
|
1021
1024
|
...bgCallbacks,
|
|
@@ -1826,6 +1829,9 @@ Write the file using the write tool. Only write the file, nothing else.`;
|
|
|
1826
1829
|
const record = await manager.spawnAndWait(pi, ctx, "general-purpose", generatePrompt, {
|
|
1827
1830
|
description: `Generate ${name} agent`,
|
|
1828
1831
|
maxTurns: 5,
|
|
1832
|
+
eventBus: pi.events,
|
|
1833
|
+
depth: nextSubagentDepth,
|
|
1834
|
+
parentAgentId: extensionAgentId,
|
|
1829
1835
|
});
|
|
1830
1836
|
|
|
1831
1837
|
if (record.status === "error") {
|
package/src/schedule.ts
CHANGED
|
@@ -45,6 +45,13 @@ export interface NewJobInput {
|
|
|
45
45
|
isolation?: IsolationMode;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
interface SchedulerSpawnDefaults {
|
|
49
|
+
/** Recursive depth for scheduled subagents fired from this session. */
|
|
50
|
+
depth?: number;
|
|
51
|
+
/** Parent subagent id for scheduled subagents fired from this session. */
|
|
52
|
+
parentAgentId?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
48
55
|
export class SubagentScheduler {
|
|
49
56
|
private jobs = new Map<string, Cron>();
|
|
50
57
|
private intervals = new Map<string, NodeJS.Timeout>();
|
|
@@ -52,13 +59,21 @@ export class SubagentScheduler {
|
|
|
52
59
|
private pi: ExtensionAPI | undefined;
|
|
53
60
|
private ctx: ExtensionContext | undefined;
|
|
54
61
|
private manager: AgentManager | undefined;
|
|
62
|
+
private spawnDefaults: SchedulerSpawnDefaults = {};
|
|
55
63
|
|
|
56
64
|
/** Start the scheduler: bind to a session's store and arm enabled jobs. */
|
|
57
|
-
start(
|
|
65
|
+
start(
|
|
66
|
+
pi: ExtensionAPI,
|
|
67
|
+
ctx: ExtensionContext,
|
|
68
|
+
manager: AgentManager,
|
|
69
|
+
store: ScheduleStore,
|
|
70
|
+
spawnDefaults: SchedulerSpawnDefaults = {},
|
|
71
|
+
): void {
|
|
58
72
|
this.pi = pi;
|
|
59
73
|
this.ctx = ctx;
|
|
60
74
|
this.manager = manager;
|
|
61
75
|
this.store = store;
|
|
76
|
+
this.spawnDefaults = spawnDefaults;
|
|
62
77
|
|
|
63
78
|
for (const job of store.list()) {
|
|
64
79
|
if (job.enabled) this.scheduleJob(job);
|
|
@@ -75,6 +90,7 @@ export class SubagentScheduler {
|
|
|
75
90
|
this.pi = undefined;
|
|
76
91
|
this.ctx = undefined;
|
|
77
92
|
this.manager = undefined;
|
|
93
|
+
this.spawnDefaults = {};
|
|
78
94
|
}
|
|
79
95
|
|
|
80
96
|
/** True if start() has bound a store and the scheduler is active. */
|
|
@@ -247,6 +263,9 @@ export class SubagentScheduler {
|
|
|
247
263
|
isolated: job.isolated,
|
|
248
264
|
thinkingLevel: job.thinking,
|
|
249
265
|
isolation: job.isolation,
|
|
266
|
+
eventBus: pi.events,
|
|
267
|
+
depth: this.spawnDefaults.depth,
|
|
268
|
+
parentAgentId: this.spawnDefaults.parentAgentId,
|
|
250
269
|
});
|
|
251
270
|
} catch (err) {
|
|
252
271
|
const error = err instanceof Error ? err.message : String(err);
|
package/bugs.txt
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
# pi-subagents bugs
|
|
2
|
-
|
|
3
|
-
## 2026-06-24: subagent-spawned reviewer c2c alias registrations route to coordinator
|
|
4
|
-
|
|
5
|
-
Observed in `/home/xertrov/src/autoplanet-harness` session while coordinating nested pi Agent reviewers.
|
|
6
|
-
|
|
7
|
-
Symptom:
|
|
8
|
-
- Coordinator received transcript notifications like `Subagent <id> registered as pi-...` for reviewer subagents spawned by an implementer subagent.
|
|
9
|
-
- The spawning implementer subagent did **not** receive those c2c alias registration notifications.
|
|
10
|
-
- This made the coordinator see aliases for subagents it did not directly start, while the parent subagent could not map its Agent handles to c2c aliases.
|
|
11
|
-
|
|
12
|
-
Concrete example:
|
|
13
|
-
- Coordinator directly started T244/G9 implementer: agent `81a4688f-82ce-40e`, alias `pi-8391d3-ae5887a`.
|
|
14
|
-
- T244 spawned reviewer/rereviewer Agent subagents with IDs:
|
|
15
|
-
- `436193fa-ef20-4d0`
|
|
16
|
-
- `97bddbcf-53a8-48c`
|
|
17
|
-
- `56a58df4-c91c-48a`
|
|
18
|
-
- `82ae1d14-707d-415`
|
|
19
|
-
- Coordinator received registrations for aliases including `pi-8391d3-a723e9a`, `pi-8391d3-a0a8a00`, `pi-8391d3-ab7ed6f`, `pi-8391d3-ab167dc`.
|
|
20
|
-
- T244 reported: its transcript only showed Agent result handles/output paths/completion summaries; it did not see `Subagent <id> registered as pi-...` messages and could not map reviewer IDs to c2c aliases.
|
|
21
|
-
|
|
22
|
-
Expected behavior:
|
|
23
|
-
- Alias registration notifications for a subagent spawned by another subagent should be delivered to the spawning subagent's transcript, or at least to both the spawning subagent and coordinator with parent/owner metadata.
|
|
24
|
-
- The notification should include parent/spawner identity so coordinators can distinguish direct children from nested reviewer subagents.
|
|
25
|
-
|
|
26
|
-
Impact:
|
|
27
|
-
- Coordinator receives noisy/ambiguous registrations for unknown aliases.
|
|
28
|
-
- Spawning subagent cannot c2c-message its own reviewers by alias or report alias ownership accurately.
|
|
29
|
-
- Requires extra coordinator debugging messages to identify ownership.
|
|
30
|
-
|
|
31
|
-
Suggested fix:
|
|
32
|
-
- Route `Subagent <id> registered as <alias>` to the agent that invoked the Agent tool.
|
|
33
|
-
- Add fields like `parent_agent_id`, `parent_alias`, and maybe `root_coordinator_alias` to registration notifications.
|
|
34
|
-
- If root coordinator must also receive nested registrations, label them explicitly as nested.
|
|
35
|
-
|
|
36
|
-
Additional observation after filing:
|
|
37
|
-
- Coordinator later received another nested-looking registration: `Subagent 8262e0d6-457e-49c registered as pi-8391d3-ab41e8a`, again without the coordinator directly starting that subagent.
|
|
38
|
-
- Confirmed with T245/G6 implementer `pi-8391d3-a861bf0`: it spawned reviewer Agent IDs `8262e0d6-457e-49c` and `14d29ca7-348e-437`, while coordinator received alias registrations `pi-8391d3-ab41e8a` and `pi-8391d3-aa90cd5`; T245 reported it had not received any c2c alias registration notifications for them.
|
|
39
|
-
- After crash recovery restart, coordinator received another nested-looking registration: `Subagent 4f396572-cab5-42c registered as pi-8391d3-a52ab8a`; likely spawned by a recovery worker/reviewer rather than directly by coordinator.
|
|
40
|
-
- Coordinator also received `Subagent 09dc9e7f-f960-411 registered as pi-8391d3-a219275` during recovery worker activity, again likely nested/reviewer notification routed to coordinator.
|
|
41
|
-
- Coordinator also received `Subagent ee594feb-8fd1-458 registered as pi-8391d3-a8f6d40` while nested reviewer activity was ongoing; likely another nested alias notification routed to coordinator.
|
|
42
|
-
- Coordinator also received `Subagent dd29fe5b-357a-48f registered as pi-8391d3-afd6cfc` during T246 nested rereview activity; likely another nested alias notification routed to coordinator.
|
|
43
|
-
- Coordinator received nested-looking registrations `Subagent 9aa66b38-3ba8-405 registered as pi-8391d3-a6cefa4` and `Subagent d33bb514-be85-4a5 registered as pi-8391d3-ae99126` during active worker review activity.
|
|
44
|
-
- Coordinator received nested-looking registration `Subagent ab7cb925-d76c-489 registered as pi-8391d3-a8c6afd` during active worker review activity.
|
|
45
|
-
- Coordinator received nested-looking registration `Subagent ba23eaaf-a706-459 registered as pi-8391d3-af5d393` during active worker review activity.
|
|
46
|
-
- Coordinator received nested-looking registration `Subagent adf52e5e-862e-4c6 registered as pi-8391d3-aa897ea` during active worker review activity.
|
|
47
|
-
- Coordinator received nested-looking registration `Subagent e6eb1662-8223-458 registered as pi-8391d3-adb2231` during active worker review activity.
|
|
48
|
-
- Coordinator received nested-looking registration `Subagent 565012aa-43b6-407 registered as pi-8391d3-a66be3e` during active worker review activity.
|
|
49
|
-
- Coordinator received nested-looking registration `Subagent 603b211a-9daa-421 registered as pi-8391d3-aa92c43` during active worker review activity.
|
|
50
|
-
- Coordinator received nested-looking registration `Subagent 734f3fa3-958b-4bd registered as pi-8391d3-a028432` during active T248 review activity.
|
|
51
|
-
- Coordinator received nested-looking registration `Subagent 2d565584-fcbc-496 registered as pi-8391d3-ac6e43c` during active T250/T251/T252 review activity.
|
|
52
|
-
- Coordinator received nested-looking registration `Subagent 5e7ee0d1-5112-4b9 registered as pi-8391d3-a7722c2` during active T250/T251/T252 review activity.
|
|
53
|
-
- Coordinator received nested-looking registration `Subagent eae7393b-dfb4-445 registered as pi-8391d3-a2a9d62` during active T250/T251/T252 review activity.
|
|
54
|
-
- Coordinator received nested-looking registration `Subagent 6629a0fe-daef-401 registered as pi-8391d3-ad0b4e5` during active T250 review activity.
|
|
55
|
-
- Coordinator received nested-looking registration `Subagent 49eb5d11-178d-4d2 registered as pi-8391d3-a979978` during active T250 review activity.
|
|
56
|
-
- Coordinator received nested-looking registration `Subagent bf846951-6b2f-4b5 registered as pi-8391d3-aabc2c5` during active T250 rereview activity.
|
|
57
|
-
- Coordinator received nested-looking registration `Subagent 3250e286-4fd4-4a2 registered as pi-8391d3-a78b404` during active T250 rereview activity.
|