@hheei/pi-subagents 0.2.3 → 0.3.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 +32 -17
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- package/dist/bridge-client.d.ts +5 -0
- package/dist/bridge-client.js +6 -0
- package/dist/builtin-agents.js +50 -37
- package/dist/child-bridge.d.ts +0 -8
- package/dist/child-bridge.js +60 -43
- package/dist/child-control.d.ts +5 -3
- package/dist/child-control.js +176 -35
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +12 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +5 -3
- package/dist/extension.js +42 -44
- package/dist/host-adapter.js +1 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +54 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/manager.d.ts +4 -2
- package/dist/manager.js +299 -114
- package/dist/registry.js +19 -5
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +0 -1
- package/dist/runtime.js +8 -0
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state.d.ts +13 -0
- package/dist/state.js +59 -16
- package/dist/status.d.ts +14 -0
- package/dist/status.js +36 -0
- package/dist/tool-activation.d.ts +3 -4
- package/dist/tool-activation.js +13 -215
- package/dist/tool-presentation.d.ts +17 -0
- package/dist/tool-presentation.js +160 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +133 -180
- package/package.json +6 -6
- package/dist/session-fork.d.ts +0 -16
- package/dist/session-fork.js +0 -67
- package/dist/widget.d.ts +0 -12
- package/dist/widget.js +0 -149
package/dist/registry.js
CHANGED
|
@@ -57,11 +57,10 @@ const LAUNCH_CONFIG_FIELDS = {
|
|
|
57
57
|
skills: true,
|
|
58
58
|
prompt: true,
|
|
59
59
|
bridgeExtensionPath: true,
|
|
60
|
-
interactive: true,
|
|
61
60
|
title: true,
|
|
62
61
|
task: true,
|
|
63
|
-
referencedSessionPath: true,
|
|
64
62
|
codemodeOnly: true,
|
|
63
|
+
mcp: true,
|
|
65
64
|
};
|
|
66
65
|
const AGENT_FIELDS = {
|
|
67
66
|
name: true,
|
|
@@ -221,6 +220,23 @@ function parseRuntime(value, path) {
|
|
|
221
220
|
const endpoint = expectString(raw.endpoint, "runtime.endpoint", path);
|
|
222
221
|
return Object.freeze({ runtimeIdentity, endpoint });
|
|
223
222
|
}
|
|
223
|
+
function parseMcpSelection(value, path) {
|
|
224
|
+
if (value === undefined)
|
|
225
|
+
return Object.freeze({ enabled: true });
|
|
226
|
+
const raw = expectObject(value, "launchConfig.mcp", path);
|
|
227
|
+
const enabled = expectOptionalBoolean(raw.enabled, "launchConfig.mcp.enabled", path, true);
|
|
228
|
+
const servers = raw.servers === undefined
|
|
229
|
+
? undefined
|
|
230
|
+
: Object.freeze(expectStringArray(raw.servers, "launchConfig.mcp.servers", path));
|
|
231
|
+
const excluded = raw.excluded === undefined
|
|
232
|
+
? undefined
|
|
233
|
+
: Object.freeze(expectStringArray(raw.excluded, "launchConfig.mcp.excluded", path));
|
|
234
|
+
return Object.freeze({
|
|
235
|
+
enabled,
|
|
236
|
+
...(servers === undefined ? {} : { servers }),
|
|
237
|
+
...(excluded === undefined ? {} : { excluded }),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
224
240
|
function parseLaunchConfig(value, path) {
|
|
225
241
|
const raw = expectObject(value, "launchConfig", path);
|
|
226
242
|
expectKeys(raw, LAUNCH_CONFIG_FIELDS, "launchConfig", path);
|
|
@@ -228,7 +244,6 @@ function parseLaunchConfig(value, path) {
|
|
|
228
244
|
if (!isSessionId(sessionId))
|
|
229
245
|
throw invalid(path, "launchConfig.sessionId is not a valid session id");
|
|
230
246
|
const sessionPath = expectOptionalString(raw.sessionPath, "launchConfig.sessionPath", path);
|
|
231
|
-
const referencedSessionPath = expectOptionalString(raw.referencedSessionPath, "launchConfig.referencedSessionPath", path);
|
|
232
247
|
const extensions = parseSelection(raw.extensions, path, "extensions");
|
|
233
248
|
const bridgeExtensionPath = expectString(raw.bridgeExtensionPath, "launchConfig.bridgeExtensionPath", path);
|
|
234
249
|
if (!extensions.paths.includes(bridgeExtensionPath)) {
|
|
@@ -245,7 +260,6 @@ function parseLaunchConfig(value, path) {
|
|
|
245
260
|
sessionId,
|
|
246
261
|
sessionDir: expectString(raw.sessionDir, "launchConfig.sessionDir", path),
|
|
247
262
|
...(sessionPath === undefined ? {} : { sessionPath }),
|
|
248
|
-
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
249
263
|
agent: parseAgent(raw.agent, path),
|
|
250
264
|
model: parseModel(raw.model, path),
|
|
251
265
|
thinking: parseThinking(raw.thinking, path),
|
|
@@ -253,9 +267,9 @@ function parseLaunchConfig(value, path) {
|
|
|
253
267
|
excludeTools: Object.freeze(expectStringArray(raw.excludeTools, "launchConfig.excludeTools", path)),
|
|
254
268
|
extensions,
|
|
255
269
|
skills: parseSelection(raw.skills, path, "skills"),
|
|
270
|
+
mcp: parseMcpSelection(raw.mcp, path),
|
|
256
271
|
prompt: expectString(raw.prompt, "launchConfig.prompt", path),
|
|
257
272
|
bridgeExtensionPath,
|
|
258
|
-
interactive: expectOptionalBoolean(raw.interactive, "launchConfig.interactive", path, false),
|
|
259
273
|
...(expectOptionalBoolean(raw.codemodeOnly, "launchConfig.codemodeOnly", path, false)
|
|
260
274
|
? { codemodeOnly: true }
|
|
261
275
|
: {}),
|
package/dist/rpc-events.d.ts
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
* The Pi events a child forwards to its parent. Everything else — streaming token noise,
|
|
3
3
|
* undocumented internals — is dropped, so the parent's projection only sees turn-level facts.
|
|
4
4
|
*/
|
|
5
|
-
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "
|
|
5
|
+
export declare const FORWARDED_PI_EVENT_TYPES: readonly ["agent_start", "agent_end", "turn_start", "turn_end", "tool_execution_start", "tool_execution_end", "auto_retry", "auto_retry_start", "auto_retry_end", "message_update", "agent_settled"];
|
|
6
6
|
export declare function shouldForwardPiEvent(event: unknown): boolean;
|
package/dist/rpc-events.js
CHANGED
package/dist/runtime.js
CHANGED
|
@@ -167,6 +167,10 @@ export async function launchChild(options) {
|
|
|
167
167
|
};
|
|
168
168
|
}
|
|
169
169
|
options.tokens.forget(plan.runtimeIdentity);
|
|
170
|
+
await options.registry.update(record.subagentId, undefined, (current) => {
|
|
171
|
+
const { runtime: _runtime, ...rest } = current;
|
|
172
|
+
return rest;
|
|
173
|
+
}, plan.runtimeIdentity);
|
|
170
174
|
return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
|
|
171
175
|
}
|
|
172
176
|
/**
|
|
@@ -214,6 +218,10 @@ export async function openChildPanel(options) {
|
|
|
214
218
|
};
|
|
215
219
|
}
|
|
216
220
|
options.tokens.forget(plan.runtimeIdentity);
|
|
221
|
+
await options.registry.update(record.subagentId, undefined, (current) => {
|
|
222
|
+
const { runtime: _runtime, ...rest } = current;
|
|
223
|
+
return rest;
|
|
224
|
+
}, plan.runtimeIdentity);
|
|
217
225
|
return { failure: `Child ${record.subagentId} never connected to the parent bridge` };
|
|
218
226
|
}
|
|
219
227
|
/** Creates the runtime directory if needed; the socket itself is 0600 in a 0700 directory. */
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { ModelRegistryLike, ParentAgentDefaults, SkillCatalogEntry } from "./agent-resolver.js";
|
|
2
2
|
import type { EffectiveLaunchConfig, PersistenceState, PiInvocation, Presentation, SpawnSubagentInput, SubagentRecord } from "./domain.js";
|
|
3
3
|
import type { SubagentRegistry } from "./registry.js";
|
|
4
|
+
/** Reads all JSON entries from a session file on disk. */
|
|
5
|
+
export declare function readSessionJsonlEntries(sessionPath: string): Promise<unknown[]>;
|
|
4
6
|
export interface SessionPlacement {
|
|
5
7
|
readonly sessionId: string;
|
|
6
8
|
readonly sessionDir: string;
|
|
@@ -62,7 +64,6 @@ export interface ResolveSubagentLaunchOptions {
|
|
|
62
64
|
readonly existingSubagentIds?: readonly string[];
|
|
63
65
|
readonly enforceEnabled?: boolean;
|
|
64
66
|
readonly sessionPath?: string;
|
|
65
|
-
readonly referencedSessionPath?: string;
|
|
66
67
|
}
|
|
67
68
|
/**
|
|
68
69
|
* One resolution chain for a spawn: agent definition + explicit parent defaults
|
|
@@ -1,13 +1,27 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { open, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import { open, readdir, readFile, stat } from "node:fs/promises";
|
|
3
3
|
import { join, resolve } from "node:path";
|
|
4
|
-
import { SessionManager } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { getAgentDir, SessionManager } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import { isRecord } from "@hheei/pi-ext-core";
|
|
6
6
|
import { resolveAgent } from "./agent-resolver.js";
|
|
7
7
|
import { builtinAgentToolProblem } from "./builtin-agents.js";
|
|
8
8
|
import { isSessionId } from "./domain.js";
|
|
9
9
|
import { assembleChildPrompt, resolveBridgeExtensionPath, resolvePiInvocation, } from "./launch-spec.js";
|
|
10
|
-
|
|
10
|
+
/** Reads all JSON entries from a session file on disk. */
|
|
11
|
+
export async function readSessionJsonlEntries(sessionPath) {
|
|
12
|
+
const raw = await readFile(sessionPath, "utf8");
|
|
13
|
+
const lines = raw.split("\n").filter((line) => line.trim() !== "");
|
|
14
|
+
const entries = [];
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
try {
|
|
17
|
+
entries.push(JSON.parse(line));
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
// ignore trailing malformed line
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return entries;
|
|
24
|
+
}
|
|
11
25
|
/** Bounded read for a session header; the first line holds the whole identity of the file. */
|
|
12
26
|
const HEADER_READ_BYTES = 64 * 1024;
|
|
13
27
|
/**
|
|
@@ -151,10 +165,10 @@ export function createSubagentId(existingIds) {
|
|
|
151
165
|
export async function resolveSubagentLaunch(options) {
|
|
152
166
|
const task = options.input.task.trim();
|
|
153
167
|
if (task === "")
|
|
154
|
-
throw new Error("
|
|
168
|
+
throw new Error("agent_spawn requires a non-empty task");
|
|
155
169
|
const name = options.input.agent?.trim();
|
|
156
170
|
if (name === undefined || name === "") {
|
|
157
|
-
throw new Error("
|
|
171
|
+
throw new Error("agent_spawn requires an explicit agent name; V1 has no built-in default agent");
|
|
158
172
|
}
|
|
159
173
|
const cwd = resolve(options.input.cwd?.trim() || options.cwd);
|
|
160
174
|
const requestedTitle = options.input.title?.trim();
|
|
@@ -174,12 +188,9 @@ export async function resolveSubagentLaunch(options) {
|
|
|
174
188
|
...(options.onWarning === undefined ? {} : { onWarning: options.onWarning }),
|
|
175
189
|
});
|
|
176
190
|
if (options.enforceEnabled === true && policy.enabled === false) {
|
|
177
|
-
throw new Error(`Agent "${policy.agent.name}" is disabled: no model is specified in user configuration (
|
|
178
|
-
}
|
|
179
|
-
let instructions = policy.agent.instructions;
|
|
180
|
-
if (options.referencedSessionPath !== undefined && options.referencedSessionPath.trim() !== "") {
|
|
181
|
-
instructions = `${instructions.trim()}\n\n${formatSessionReferencePrompt(options.referencedSessionPath)}`;
|
|
191
|
+
throw new Error(`Agent "${policy.agent.name}" is disabled: no model is specified in user configuration (${join(getAgentDir(), "agents")}/ or .pi/agents/)`);
|
|
182
192
|
}
|
|
193
|
+
const instructions = policy.agent.instructions;
|
|
183
194
|
return Object.freeze({
|
|
184
195
|
subagentId,
|
|
185
196
|
invocation: options.invocation ?? resolvePiInvocation(),
|
|
@@ -187,9 +198,6 @@ export async function resolveSubagentLaunch(options) {
|
|
|
187
198
|
sessionId,
|
|
188
199
|
sessionDir: resolveSubagentSessionDir(cwd),
|
|
189
200
|
...(options.sessionPath === undefined ? {} : { sessionPath: options.sessionPath }),
|
|
190
|
-
...(options.referencedSessionPath === undefined
|
|
191
|
-
? {}
|
|
192
|
-
: { referencedSessionPath: options.referencedSessionPath }),
|
|
193
201
|
agent: policy.agent,
|
|
194
202
|
model: policy.model,
|
|
195
203
|
thinking: policy.thinking,
|
|
@@ -197,9 +205,9 @@ export async function resolveSubagentLaunch(options) {
|
|
|
197
205
|
excludeTools: policy.excludeTools,
|
|
198
206
|
extensions: policy.extensions,
|
|
199
207
|
skills: policy.skills,
|
|
208
|
+
mcp: policy.mcp,
|
|
200
209
|
prompt: assembleChildPrompt(instructions),
|
|
201
210
|
bridgeExtensionPath,
|
|
202
|
-
interactive: policy.interactive,
|
|
203
211
|
...(policy.codemodeOnly ? { codemodeOnly: true } : {}),
|
|
204
212
|
...(title === undefined ? {} : { title }),
|
|
205
213
|
});
|
package/dist/state.d.ts
CHANGED
|
@@ -19,4 +19,17 @@ export interface StateProjector {
|
|
|
19
19
|
}
|
|
20
20
|
export declare function summarizeCurrentBranch(entries: readonly unknown[]): string | undefined;
|
|
21
21
|
export declare function aggregateUsage(entries: readonly unknown[]): UsageSummary;
|
|
22
|
+
/** The phase a streaming assistant update is in: what the parent shows as the child's activity. */
|
|
23
|
+
export type AssistantPhaseKind = "thinking" | "generating" | "toolcall";
|
|
24
|
+
export interface AssistantPhase {
|
|
25
|
+
readonly kind: AssistantPhaseKind;
|
|
26
|
+
/** The tool being streamed, when the phase is a tool call. */
|
|
27
|
+
readonly toolName?: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Categorizes a streaming assistant update into the phase the parent projects. The child throttles
|
|
31
|
+
* duplicate deltas with the same answer, so both ends of the bridge agree on what "still thinking"
|
|
32
|
+
* means instead of each re-deriving it from a different subset of the event.
|
|
33
|
+
*/
|
|
34
|
+
export declare function assistantUpdatePhase(assistantMessageEvent: unknown, message: unknown): AssistantPhase | undefined;
|
|
22
35
|
export declare function createStateProjector(initialState?: SubagentState): StateProjector;
|
package/dist/state.js
CHANGED
|
@@ -101,6 +101,35 @@ export function aggregateUsage(entries) {
|
|
|
101
101
|
}
|
|
102
102
|
return total;
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* Categorizes a streaming assistant update into the phase the parent projects. The child throttles
|
|
106
|
+
* duplicate deltas with the same answer, so both ends of the bridge agree on what "still thinking"
|
|
107
|
+
* means instead of each re-deriving it from a different subset of the event.
|
|
108
|
+
*/
|
|
109
|
+
export function assistantUpdatePhase(assistantMessageEvent, message) {
|
|
110
|
+
const update = record(assistantMessageEvent);
|
|
111
|
+
const type = typeof update?.type === "string" ? update.type : undefined;
|
|
112
|
+
const current = record(message);
|
|
113
|
+
const currentText = current === undefined ? undefined : text(current);
|
|
114
|
+
if (type === undefined) {
|
|
115
|
+
return currentText === undefined ? undefined : { kind: "generating" };
|
|
116
|
+
}
|
|
117
|
+
if (type.startsWith("thinking_"))
|
|
118
|
+
return { kind: "thinking" };
|
|
119
|
+
if (type.startsWith("toolcall_")) {
|
|
120
|
+
const index = typeof update?.contentIndex === "number" ? update.contentIndex : undefined;
|
|
121
|
+
const part = current !== undefined && Array.isArray(current.content) && index !== undefined
|
|
122
|
+
? record(current.content[index])
|
|
123
|
+
: undefined;
|
|
124
|
+
return {
|
|
125
|
+
kind: "toolcall",
|
|
126
|
+
...(typeof part?.name === "string" ? { toolName: part.name } : {}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (type.startsWith("text_") || currentText !== undefined)
|
|
130
|
+
return { kind: "generating" };
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
104
133
|
export function createStateProjector(initialState = "running") {
|
|
105
134
|
let state = initialState;
|
|
106
135
|
let summary;
|
|
@@ -120,47 +149,61 @@ export function createStateProjector(initialState = "running") {
|
|
|
120
149
|
if (value === undefined)
|
|
121
150
|
return snapshot();
|
|
122
151
|
if (value.type === "agent_start" ||
|
|
152
|
+
value.type === "turn_start" ||
|
|
123
153
|
value.type === "auto_retry_start" ||
|
|
124
154
|
value.type === "auto_retry") {
|
|
125
155
|
state = "running";
|
|
156
|
+
summary = "thinking...";
|
|
126
157
|
interrupted = undefined;
|
|
127
158
|
activeTool = undefined;
|
|
128
159
|
}
|
|
129
|
-
else if (value.type === "tool_execution_start") {
|
|
160
|
+
else if (value.type === "tool_execution_start" || value.type === "tool_execution_update") {
|
|
130
161
|
activeTool = typeof value.toolName === "string" ? value.toolName : undefined;
|
|
131
162
|
}
|
|
132
163
|
else if (value.type === "tool_execution_end") {
|
|
133
164
|
activeTool = undefined;
|
|
165
|
+
summary = "thinking...";
|
|
134
166
|
}
|
|
135
167
|
else if (value.type === "message_update") {
|
|
136
168
|
const message = record(value.message);
|
|
137
169
|
if (message?.role === "assistant") {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
summary =
|
|
170
|
+
const phase = assistantUpdatePhase(value.assistantMessageEvent, message);
|
|
171
|
+
if (phase?.kind === "thinking") {
|
|
172
|
+
summary = "thinking...";
|
|
173
|
+
activeTool = undefined;
|
|
174
|
+
}
|
|
175
|
+
else if (phase?.kind === "toolcall") {
|
|
176
|
+
activeTool = phase.toolName;
|
|
177
|
+
}
|
|
178
|
+
else if (phase?.kind === "generating") {
|
|
179
|
+
summary = "generating...";
|
|
141
180
|
activeTool = undefined;
|
|
142
181
|
}
|
|
143
182
|
}
|
|
144
183
|
}
|
|
145
184
|
else if (value.type === "agent_end" || value.type === "agent_settled") {
|
|
146
185
|
activeTool = undefined;
|
|
147
|
-
// agent_end precedes retries, compaction and queued continuation work.
|
|
148
|
-
if (value.type === "agent_settled") {
|
|
149
|
-
if (state !== "error" && state !== "blocked") {
|
|
150
|
-
state = "done";
|
|
151
|
-
interrupted = undefined;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
186
|
const messages = Array.isArray(value.messages) ? value.messages : [];
|
|
155
187
|
const message = record(messages.length > 0 ? messages[messages.length - 1] : value.message);
|
|
156
188
|
if (message?.role === "assistant") {
|
|
157
189
|
summary = text(message) ?? summary;
|
|
158
|
-
if (value.type === "
|
|
190
|
+
if (value.type === "agent_end" && message.stopReason === "error") {
|
|
191
|
+
state = "error";
|
|
192
|
+
interrupted = diagnostic(message);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
if (value.type === "agent_settled") {
|
|
196
|
+
if (value.aborted === true) {
|
|
197
|
+
state = "blocked";
|
|
198
|
+
interrupted = "Assistant turn was interrupted";
|
|
199
|
+
}
|
|
200
|
+
else if (message?.stopReason === "error") {
|
|
201
|
+
state = "error";
|
|
159
202
|
interrupted = diagnostic(message);
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
state = "done";
|
|
206
|
+
interrupted = undefined;
|
|
164
207
|
}
|
|
165
208
|
}
|
|
166
209
|
}
|
package/dist/status.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ThemeLike } from "@hheei/pi-ext-core";
|
|
3
|
+
import { type PublicSubagent } from "./domain.js";
|
|
4
|
+
export declare const SUBAGENT_STATUS_KEY = "pi-subagents";
|
|
5
|
+
/** Display shorthand only; commands and tools still use the full child id. */
|
|
6
|
+
export declare function formatSubagentId(id: string): string;
|
|
7
|
+
export declare function formatSubagentStatus(children: readonly PublicSubagent[], theme: ThemeLike): string | undefined;
|
|
8
|
+
/** Native status transport; no widget, timers or dependency on pi-status. */
|
|
9
|
+
export declare function createSubagentStatus(ui: Pick<ExtensionUIContext, "setStatus"> & {
|
|
10
|
+
readonly theme: ThemeLike;
|
|
11
|
+
}, signal: AbortSignal): {
|
|
12
|
+
refresh(children: readonly PublicSubagent[]): void;
|
|
13
|
+
dispose(): void;
|
|
14
|
+
};
|
package/dist/status.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH } from "./domain.js";
|
|
2
|
+
export const SUBAGENT_STATUS_KEY = "pi-subagents";
|
|
3
|
+
/** Display shorthand only; commands and tools still use the full child id. */
|
|
4
|
+
export function formatSubagentId(id) {
|
|
5
|
+
return `#${id.replace(/^agent-/u, "")}`;
|
|
6
|
+
}
|
|
7
|
+
export function formatSubagentStatus(children, theme) {
|
|
8
|
+
const running = children.filter((child) => toVisualSubagentState(child.state, child.interrupted) === "running");
|
|
9
|
+
if (running.length === 0)
|
|
10
|
+
return undefined;
|
|
11
|
+
return ("🤖 " +
|
|
12
|
+
theme.fg("accent", VISUAL_SUBAGENT_GLYPH.running) +
|
|
13
|
+
running.map((child) => theme.fg("dim", formatSubagentId(child.id))).join(" "));
|
|
14
|
+
}
|
|
15
|
+
/** Native status transport; no widget, timers or dependency on pi-status. */
|
|
16
|
+
export function createSubagentStatus(ui, signal) {
|
|
17
|
+
let disposed = false;
|
|
18
|
+
const dispose = () => {
|
|
19
|
+
if (disposed)
|
|
20
|
+
return;
|
|
21
|
+
disposed = true;
|
|
22
|
+
signal.removeEventListener("abort", dispose);
|
|
23
|
+
ui.setStatus(SUBAGENT_STATUS_KEY, undefined);
|
|
24
|
+
};
|
|
25
|
+
if (signal.aborted)
|
|
26
|
+
dispose();
|
|
27
|
+
else
|
|
28
|
+
signal.addEventListener("abort", dispose, { once: true });
|
|
29
|
+
return {
|
|
30
|
+
refresh(children) {
|
|
31
|
+
if (!disposed)
|
|
32
|
+
ui.setStatus(SUBAGENT_STATUS_KEY, formatSubagentStatus(children, ui.theme));
|
|
33
|
+
},
|
|
34
|
+
dispose,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { SubagentManager } from "./manager.js";
|
|
3
3
|
/** Interactive agent tools managed by dynamic activation. */
|
|
4
|
-
export declare const INTERACTIVE_TOOL_NAMES: readonly ["
|
|
4
|
+
export declare const INTERACTIVE_TOOL_NAMES: readonly ["agent_spawn", "agent_send", "agent_get", "agent_stop"];
|
|
5
5
|
export type InteractiveToolName = (typeof INTERACTIVE_TOOL_NAMES)[number];
|
|
6
|
-
export declare const
|
|
7
|
-
export declare const
|
|
8
|
-
export declare function supportsDynamicTools(pi: ExtensionAPI): boolean;
|
|
6
|
+
export declare const SUBAGENT_OPTIONAL_TOOL_KEY = "subagents";
|
|
7
|
+
export declare const SUBAGENT_OPTIONAL_TOOL_BULLET = "subagents (query: \"agent\"): Subagent orchestration and delegation (agent_spawn, agent_send, agent_get, agent_stop).";
|
|
9
8
|
export interface RegisterInteractiveToolActivationOptions {
|
|
10
9
|
readonly pi: ExtensionAPI;
|
|
11
10
|
readonly manager: SubagentManager;
|
package/dist/tool-activation.js
CHANGED
|
@@ -1,227 +1,25 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { getToolTui, isRecord, textToolResult } from "@hheei/pi-ext-core";
|
|
3
|
-
import { Type } from "typebox";
|
|
4
|
-
import { discoverAgents } from "./agent-resolver.js";
|
|
5
|
-
import { builtinAgents } from "./builtin-agents.js";
|
|
1
|
+
import { setOptionalToolBullet } from "@hheei/pi-ext-core";
|
|
6
2
|
/** Interactive agent tools managed by dynamic activation. */
|
|
7
3
|
export const INTERACTIVE_TOOL_NAMES = [
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
4
|
+
"agent_spawn",
|
|
5
|
+
"agent_send",
|
|
6
|
+
"agent_get",
|
|
7
|
+
"agent_stop",
|
|
12
8
|
];
|
|
13
|
-
export const
|
|
14
|
-
export const
|
|
15
|
-
const DESCRIPTION = "Enable interactive agent tools (spawn_agent, send_agent, get_agent, stop_agent) for running interactive subagents. Enabled tools are available on the next model request.";
|
|
16
|
-
const SNIPPET = "Interactive subagents (spawn_agent, send_agent, get_agent, stop_agent) require activation. Call subagent_enable to activate them.";
|
|
17
|
-
const emptySchema = Type.Object({}, { additionalProperties: false });
|
|
18
|
-
export function supportsDynamicTools(pi) {
|
|
19
|
-
return (typeof pi.getAllTools === "function" &&
|
|
20
|
-
typeof pi.getActiveTools === "function" &&
|
|
21
|
-
typeof pi.setActiveTools === "function");
|
|
22
|
-
}
|
|
23
|
-
function hasToolDeclarations(messages) {
|
|
24
|
-
return messages.some((message) => isRecord(message) && ("toolsAdded" in message || "toolsRemoved" in message));
|
|
25
|
-
}
|
|
26
|
-
function currentTranscriptToolNames(messages) {
|
|
27
|
-
const tools = new Set();
|
|
28
|
-
for (const message of messages) {
|
|
29
|
-
if (!isRecord(message))
|
|
30
|
-
continue;
|
|
31
|
-
const declaration = message;
|
|
32
|
-
for (const tool of declaration.toolsRemoved ?? [])
|
|
33
|
-
tools.delete(tool.name);
|
|
34
|
-
for (const tool of declaration.toolsAdded ?? [])
|
|
35
|
-
tools.add(tool.name);
|
|
36
|
-
}
|
|
37
|
-
return [...tools];
|
|
38
|
-
}
|
|
39
|
-
const LEGACY_TOOL_NAMES = new Set([
|
|
40
|
-
"spawn_subagent",
|
|
41
|
-
"send_subagent",
|
|
42
|
-
"get_subagent",
|
|
43
|
-
"stop_subagent",
|
|
44
|
-
"list_subagents",
|
|
45
|
-
"subagents_enable",
|
|
46
|
-
]);
|
|
47
|
-
function branchHasSubagentCalls(messages, toolNames) {
|
|
48
|
-
const names = new Set([...toolNames, ...LEGACY_TOOL_NAMES]);
|
|
49
|
-
for (const msg of messages) {
|
|
50
|
-
if (!isRecord(msg))
|
|
51
|
-
continue;
|
|
52
|
-
if (msg.role === "assistant" && Array.isArray(msg.content)) {
|
|
53
|
-
for (const part of msg.content) {
|
|
54
|
-
if (isRecord(part) &&
|
|
55
|
-
(part.type === "tool_use" || part.type === "toolCall") &&
|
|
56
|
-
typeof part.name === "string") {
|
|
57
|
-
if (part.name === SUBAGENT_LOADER_NAME || names.has(part.name))
|
|
58
|
-
return true;
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
if (msg.role === "tool" &&
|
|
63
|
-
typeof msg.toolName === "string" &&
|
|
64
|
-
(msg.toolName === SUBAGENT_LOADER_NAME || names.has(msg.toolName))) {
|
|
65
|
-
return true;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
return false;
|
|
69
|
-
}
|
|
9
|
+
export const SUBAGENT_OPTIONAL_TOOL_KEY = "subagents";
|
|
10
|
+
export const SUBAGENT_OPTIONAL_TOOL_BULLET = 'subagents (query: "agent"): Subagent orchestration and delegation (agent_spawn, agent_send, agent_get, agent_stop).';
|
|
70
11
|
export function registerInteractiveToolActivation(options) {
|
|
71
|
-
const { pi
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
const names = INTERACTIVE_TOOL_NAMES;
|
|
79
|
-
const tui = getToolTui(pi);
|
|
80
|
-
const loaderTool = {
|
|
81
|
-
name: SUBAGENT_LOADER_NAME,
|
|
82
|
-
label: "Enable Interactive Subagents",
|
|
83
|
-
description: DESCRIPTION,
|
|
84
|
-
promptSnippet: SNIPPET,
|
|
85
|
-
parameters: emptySchema,
|
|
86
|
-
async execute(_id, _params, _signal, _onUpdate, execCtx) {
|
|
87
|
-
const registered = new Set(pi.getAllTools().map((tool) => tool.name));
|
|
88
|
-
const unavailable = names.filter((name) => !registered.has(name));
|
|
89
|
-
if (unavailable.length > 0) {
|
|
90
|
-
throw new Error(`Cannot enable unavailable tools: ${unavailable.join(", ")}.`);
|
|
91
|
-
}
|
|
92
|
-
const cwd = execCtx !== undefined &&
|
|
93
|
-
typeof execCtx === "object" &&
|
|
94
|
-
"cwd" in execCtx &&
|
|
95
|
-
typeof execCtx.cwd === "string"
|
|
96
|
-
? execCtx.cwd
|
|
97
|
-
: (context.cwd ?? process.cwd());
|
|
98
|
-
let allAgents;
|
|
99
|
-
try {
|
|
100
|
-
allAgents = await discoverAgents(cwd);
|
|
101
|
-
}
|
|
102
|
-
catch {
|
|
103
|
-
allAgents = builtinAgents();
|
|
104
|
-
}
|
|
105
|
-
const interactiveAgents = allAgents
|
|
106
|
-
.filter((agent) => agent.frontmatter.interactive === true && agent.frontmatter.hidden !== true)
|
|
107
|
-
.map((agent) => ({
|
|
108
|
-
name: agent.name,
|
|
109
|
-
description: typeof agent.frontmatter.description === "string" &&
|
|
110
|
-
agent.frontmatter.description.trim() !== ""
|
|
111
|
-
? agent.frontmatter.description.trim()
|
|
112
|
-
: "No description provided.",
|
|
113
|
-
}))
|
|
114
|
-
.sort((a, b) => a.name.localeCompare(b.name));
|
|
115
|
-
const currentlyActive = new Set(pi.getActiveTools());
|
|
116
|
-
const alreadyActive = names.every((name) => currentlyActive.has(name));
|
|
117
|
-
if (!alreadyActive) {
|
|
118
|
-
try {
|
|
119
|
-
pi.setActiveTools([...new Set([...pi.getActiveTools(), ...names])]);
|
|
120
|
-
}
|
|
121
|
-
catch (error) {
|
|
122
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
123
|
-
throw new Error(`Activation failed: ${message}.`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
const lines = [
|
|
127
|
-
alreadyActive
|
|
128
|
-
? `Interactive agent tools are already enabled: ${names.join(", ")}.`
|
|
129
|
-
: `Enabled interactive agent tools: ${names.join(", ")}. They will be available on the next model request.`,
|
|
130
|
-
];
|
|
131
|
-
if (interactiveAgents.length > 0) {
|
|
132
|
-
lines.push("");
|
|
133
|
-
lines.push("<interactive_agents>");
|
|
134
|
-
for (const agent of interactiveAgents) {
|
|
135
|
-
lines.push(`- ${agent.name}: ${agent.description}`);
|
|
136
|
-
}
|
|
137
|
-
lines.push("</interactive_agents>");
|
|
138
|
-
}
|
|
139
|
-
return textToolResult(lines.join("\n"), {
|
|
140
|
-
enabled: [...names],
|
|
141
|
-
...(interactiveAgents.length > 0 ? { interactiveAgents } : {}),
|
|
142
|
-
});
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
pi.registerTool(tui.frame(loaderTool, {
|
|
146
|
-
summary: () => "enable interactive subagents",
|
|
147
|
-
headerLine: "truncate",
|
|
148
|
-
footer: (result) => {
|
|
149
|
-
const details = result.details;
|
|
150
|
-
const count = details?.enabled?.length;
|
|
151
|
-
return count !== undefined
|
|
152
|
-
? count === 1
|
|
153
|
-
? "1 tool enabled"
|
|
154
|
-
: `${count} tools enabled`
|
|
155
|
-
: undefined;
|
|
156
|
-
},
|
|
157
|
-
}));
|
|
158
|
-
function loaderAvailable() {
|
|
159
|
-
return pi.getAllTools().some((tool) => tool.name === SUBAGENT_LOADER_NAME);
|
|
160
|
-
}
|
|
161
|
-
async function selectFromSession(ctx) {
|
|
162
|
-
if (!loaderAvailable())
|
|
163
|
-
return;
|
|
164
|
-
try {
|
|
165
|
-
let messages = [];
|
|
166
|
-
try {
|
|
167
|
-
const branch = ctx.sessionManager?.getBranch?.();
|
|
168
|
-
if (Array.isArray(branch) && branch.length > 0) {
|
|
169
|
-
messages = buildSessionContext(branch).messages;
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
catch {
|
|
173
|
-
// branch or sessionManager may not be available in mock contexts
|
|
174
|
-
}
|
|
175
|
-
let hasLiveChildren = false;
|
|
176
|
-
try {
|
|
177
|
-
const children = await manager.list();
|
|
178
|
-
hasLiveChildren = children.some((child) => child.freshness === "live" || child.state === "running");
|
|
179
|
-
}
|
|
180
|
-
catch {
|
|
181
|
-
// manager may be recovering or unavailable
|
|
182
|
-
}
|
|
183
|
-
const recorded = hasToolDeclarations(messages)
|
|
184
|
-
? new Set(currentTranscriptToolNames(messages))
|
|
185
|
-
: messages.length > 0 && branchHasSubagentCalls(messages, names)
|
|
186
|
-
? new Set(names)
|
|
187
|
-
: new Set();
|
|
188
|
-
const shouldBeActive = hasLiveChildren || names.some((name) => recorded.has(name));
|
|
189
|
-
const heavy = new Set(names);
|
|
190
|
-
const active = pi.getActiveTools().filter((name) => !heavy.has(name));
|
|
191
|
-
if (shouldBeActive) {
|
|
192
|
-
for (const name of names)
|
|
193
|
-
active.push(name);
|
|
194
|
-
}
|
|
195
|
-
pi.setActiveTools([...new Set([...active, SUBAGENT_LOADER_NAME])]);
|
|
12
|
+
const { pi } = options;
|
|
13
|
+
const unbindBeforeAgent = pi.on("before_agent_start", (event) => {
|
|
14
|
+
const sections = event.systemPromptOptions?.sections;
|
|
15
|
+
if (sections !== undefined) {
|
|
16
|
+
setOptionalToolBullet(sections, SUBAGENT_OPTIONAL_TOOL_KEY, SUBAGENT_OPTIONAL_TOOL_BULLET);
|
|
196
17
|
}
|
|
197
|
-
catch (error) {
|
|
198
|
-
console.warn(`[pi-subagents] Subagent tool activation setup fallback: ${error instanceof Error ? error.message : String(error)}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
const unbindSessionStart = pi.on("session_start", (_event, ctx) => {
|
|
202
|
-
void selectFromSession(ctx);
|
|
203
|
-
});
|
|
204
|
-
const unbindSessionTree = pi.on("session_tree", (_event, ctx) => {
|
|
205
|
-
void selectFromSession(ctx);
|
|
206
18
|
});
|
|
207
|
-
const unbindBeforeAgentStart = () => {
|
|
208
|
-
if (!loaderAvailable() || pi.getActiveTools().includes(SUBAGENT_LOADER_NAME))
|
|
209
|
-
return;
|
|
210
|
-
try {
|
|
211
|
-
pi.setActiveTools([...pi.getActiveTools(), SUBAGENT_LOADER_NAME]);
|
|
212
|
-
}
|
|
213
|
-
catch {
|
|
214
|
-
// Best effort
|
|
215
|
-
}
|
|
216
|
-
};
|
|
217
|
-
const unbindBeforeAgent = pi.on("before_agent_start", unbindBeforeAgentStart);
|
|
218
|
-
void selectFromSession(context);
|
|
219
19
|
return {
|
|
220
20
|
dispose() {
|
|
221
|
-
unbindSessionStart();
|
|
222
|
-
unbindSessionTree();
|
|
223
21
|
unbindBeforeAgent();
|
|
224
22
|
},
|
|
225
|
-
selectFromSession,
|
|
23
|
+
selectFromSession: async () => { },
|
|
226
24
|
};
|
|
227
25
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
/**
|
|
3
|
+
* Owned presentations for the four parent-facing subagent tools. The native renderers keep the
|
|
4
|
+
* model-visible text and raw errors when pi-ext-ui is absent; a loaded pi-ext-ui wraps the same
|
|
5
|
+
* presentation through the ext-core provider registry, so both paths share one semantic source.
|
|
6
|
+
*/
|
|
7
|
+
export interface AgentSpawnDetails {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly path: string;
|
|
10
|
+
}
|
|
11
|
+
/** One renderer set per tool, named exactly after its tool so nested calls resolve too. */
|
|
12
|
+
export declare function parentToolRenderers(pi: ExtensionAPI): {
|
|
13
|
+
readonly spawn: ToolRenderers;
|
|
14
|
+
readonly send: ToolRenderers;
|
|
15
|
+
readonly get: ToolRenderers;
|
|
16
|
+
readonly stop: ToolRenderers;
|
|
17
|
+
};
|