@hheei/pi-subagents 0.2.4 → 0.4.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 +35 -20
- package/dist/agent-resolver.d.ts +9 -0
- package/dist/agent-resolver.js +278 -51
- 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 +6 -4
- package/dist/child-control.js +251 -47
- package/dist/child-process.d.ts +2 -0
- package/dist/child-process.js +24 -12
- package/dist/commands.d.ts +1 -4
- package/dist/commands.js +1 -52
- package/dist/domain.d.ts +27 -12
- package/dist/domain.js +2 -1
- package/dist/extension.d.ts +7 -6
- package/dist/extension.js +96 -112
- package/dist/host-adapter.d.ts +19 -0
- package/dist/host-adapter.js +140 -1
- package/dist/info.d.ts +9 -0
- package/dist/info.js +80 -0
- package/dist/launch-spec.d.ts +0 -1
- package/dist/launch-spec.js +38 -18
- package/dist/lease.d.ts +83 -0
- package/dist/lease.js +239 -0
- package/dist/mailbox.d.ts +46 -0
- package/dist/mailbox.js +368 -0
- package/dist/manager.d.ts +20 -20
- package/dist/manager.js +421 -398
- package/dist/process-lock.d.ts +11 -0
- package/dist/process-lock.js +55 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/protocol.js +1 -0
- package/dist/registry.d.ts +1 -1
- package/dist/registry.js +99 -82
- package/dist/rpc-events.d.ts +1 -1
- package/dist/rpc-events.js +2 -1
- package/dist/runtime.d.ts +8 -21
- package/dist/runtime.js +61 -83
- package/dist/session-bootstrap.d.ts +2 -1
- package/dist/session-bootstrap.js +22 -14
- package/dist/state-db.d.ts +52 -0
- package/dist/state-db.js +209 -0
- package/dist/state.d.ts +25 -0
- package/dist/state.js +166 -37
- 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 +14 -0
- package/dist/tool-presentation.js +193 -0
- package/dist/tools.d.ts +6 -1
- package/dist/tools.js +134 -180
- package/package.json +6 -6
- package/dist/bridge-client.d.ts +0 -55
- package/dist/bridge-client.js +0 -453
- package/dist/bridge-server.d.ts +0 -72
- package/dist/bridge-server.js +0 -490
- 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/state.js
CHANGED
|
@@ -101,6 +101,118 @@ export function aggregateUsage(entries) {
|
|
|
101
101
|
}
|
|
102
102
|
return total;
|
|
103
103
|
}
|
|
104
|
+
function userText(message) {
|
|
105
|
+
if (typeof message.content === "string")
|
|
106
|
+
return message.content;
|
|
107
|
+
if (!Array.isArray(message.content))
|
|
108
|
+
return undefined;
|
|
109
|
+
return message.content
|
|
110
|
+
.map((part) => {
|
|
111
|
+
const content = record(part);
|
|
112
|
+
return content?.type === "text" && typeof content.text === "string" ? content.text : "";
|
|
113
|
+
})
|
|
114
|
+
.join("");
|
|
115
|
+
}
|
|
116
|
+
function lastCompletedAssistant(entries) {
|
|
117
|
+
let last;
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const item = assistant(entry);
|
|
120
|
+
if (item === undefined || !completed(item.message))
|
|
121
|
+
continue;
|
|
122
|
+
last = item.message;
|
|
123
|
+
}
|
|
124
|
+
return last;
|
|
125
|
+
}
|
|
126
|
+
/** What the parent projects from a session. The transcript itself never crosses the bridge. */
|
|
127
|
+
export function transcriptFacts(entries, expectedInput) {
|
|
128
|
+
const last = lastCompletedAssistant(entries);
|
|
129
|
+
const summary = summarizeCurrentBranch(entries);
|
|
130
|
+
const stopReason = typeof last?.stopReason === "string" ? last.stopReason : undefined;
|
|
131
|
+
const interrupted = last === undefined ? undefined : diagnostic(last);
|
|
132
|
+
const containsInput = expectedInput !== undefined &&
|
|
133
|
+
entries.some((entry) => {
|
|
134
|
+
const value = record(entry);
|
|
135
|
+
const message = record(value?.message);
|
|
136
|
+
return message?.role === "user" && userText(message) === expectedInput;
|
|
137
|
+
});
|
|
138
|
+
return {
|
|
139
|
+
usage: aggregateUsage(entries),
|
|
140
|
+
...(summary === undefined ? {} : { summary }),
|
|
141
|
+
...(stopReason === undefined ? {} : { stopReason }),
|
|
142
|
+
...(interrupted === undefined ? {} : { interrupted }),
|
|
143
|
+
containsInput,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
function usageNumber(value) {
|
|
147
|
+
return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
148
|
+
}
|
|
149
|
+
/** A mailbox answer is untrusted. Anything that is not these fields is not facts. */
|
|
150
|
+
export function parseTranscriptFacts(value) {
|
|
151
|
+
if (!isRecord(value) || typeof value.containsInput !== "boolean" || !isRecord(value.usage)) {
|
|
152
|
+
return undefined;
|
|
153
|
+
}
|
|
154
|
+
const usage = value.usage;
|
|
155
|
+
const inputTokens = usageNumber(usage.inputTokens);
|
|
156
|
+
const outputTokens = usageNumber(usage.outputTokens);
|
|
157
|
+
const cacheReadTokens = usageNumber(usage.cacheReadTokens);
|
|
158
|
+
const cacheWriteTokens = usageNumber(usage.cacheWriteTokens);
|
|
159
|
+
const turns = usageNumber(usage.turns);
|
|
160
|
+
const costUsd = usage.costUsd === null ? null : usageNumber(usage.costUsd);
|
|
161
|
+
if (inputTokens === undefined ||
|
|
162
|
+
outputTokens === undefined ||
|
|
163
|
+
cacheReadTokens === undefined ||
|
|
164
|
+
cacheWriteTokens === undefined ||
|
|
165
|
+
turns === undefined ||
|
|
166
|
+
costUsd === undefined) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const summary = typeof value.summary === "string" ? value.summary : undefined;
|
|
170
|
+
const stopReason = typeof value.stopReason === "string" ? value.stopReason : undefined;
|
|
171
|
+
const interrupted = typeof value.interrupted === "string" ? value.interrupted : undefined;
|
|
172
|
+
return {
|
|
173
|
+
usage: {
|
|
174
|
+
inputTokens,
|
|
175
|
+
outputTokens,
|
|
176
|
+
cacheReadTokens,
|
|
177
|
+
cacheWriteTokens,
|
|
178
|
+
costUsd,
|
|
179
|
+
turns,
|
|
180
|
+
},
|
|
181
|
+
...(summary === undefined ? {} : { summary }),
|
|
182
|
+
...(stopReason === undefined ? {} : { stopReason }),
|
|
183
|
+
...(interrupted === undefined ? {} : { interrupted }),
|
|
184
|
+
containsInput: value.containsInput,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Categorizes a streaming assistant update into the phase the parent projects. The child throttles
|
|
189
|
+
* duplicate deltas with the same answer, so both ends of the bridge agree on what "still thinking"
|
|
190
|
+
* means instead of each re-deriving it from a different subset of the event.
|
|
191
|
+
*/
|
|
192
|
+
export function assistantUpdatePhase(assistantMessageEvent, message) {
|
|
193
|
+
const update = record(assistantMessageEvent);
|
|
194
|
+
const type = typeof update?.type === "string" ? update.type : undefined;
|
|
195
|
+
const current = record(message);
|
|
196
|
+
const currentText = current === undefined ? undefined : text(current);
|
|
197
|
+
if (type === undefined) {
|
|
198
|
+
return currentText === undefined ? undefined : { kind: "generating" };
|
|
199
|
+
}
|
|
200
|
+
if (type.startsWith("thinking_"))
|
|
201
|
+
return { kind: "thinking" };
|
|
202
|
+
if (type.startsWith("toolcall_")) {
|
|
203
|
+
const index = typeof update?.contentIndex === "number" ? update.contentIndex : undefined;
|
|
204
|
+
const part = current !== undefined && Array.isArray(current.content) && index !== undefined
|
|
205
|
+
? record(current.content[index])
|
|
206
|
+
: undefined;
|
|
207
|
+
return {
|
|
208
|
+
kind: "toolcall",
|
|
209
|
+
...(typeof part?.name === "string" ? { toolName: part.name } : {}),
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
if (type.startsWith("text_") || currentText !== undefined)
|
|
213
|
+
return { kind: "generating" };
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
104
216
|
export function createStateProjector(initialState = "running") {
|
|
105
217
|
let state = initialState;
|
|
106
218
|
let summary;
|
|
@@ -114,55 +226,89 @@ export function createStateProjector(initialState = "running") {
|
|
|
114
226
|
...(activeTool === undefined ? {} : { activeTool }),
|
|
115
227
|
usage,
|
|
116
228
|
});
|
|
229
|
+
const applyFacts = (facts) => {
|
|
230
|
+
activeTool = undefined;
|
|
231
|
+
summary = facts.summary;
|
|
232
|
+
usage = facts.usage;
|
|
233
|
+
interrupted = facts.interrupted;
|
|
234
|
+
if (facts.stopReason === "error")
|
|
235
|
+
state = "error";
|
|
236
|
+
else if (facts.stopReason !== undefined && state === "error")
|
|
237
|
+
state = "done";
|
|
238
|
+
return snapshot();
|
|
239
|
+
};
|
|
117
240
|
return {
|
|
118
241
|
applyEvent(event) {
|
|
119
242
|
const value = record(event);
|
|
120
243
|
if (value === undefined)
|
|
121
244
|
return snapshot();
|
|
122
245
|
if (value.type === "agent_start" ||
|
|
246
|
+
value.type === "turn_start" ||
|
|
123
247
|
value.type === "auto_retry_start" ||
|
|
124
248
|
value.type === "auto_retry") {
|
|
125
249
|
state = "running";
|
|
250
|
+
summary = "thinking...";
|
|
126
251
|
interrupted = undefined;
|
|
127
252
|
activeTool = undefined;
|
|
128
253
|
}
|
|
129
|
-
else if (value.type === "tool_execution_start") {
|
|
254
|
+
else if (value.type === "tool_execution_start" || value.type === "tool_execution_update") {
|
|
130
255
|
activeTool = typeof value.toolName === "string" ? value.toolName : undefined;
|
|
131
256
|
}
|
|
132
257
|
else if (value.type === "tool_execution_end") {
|
|
133
258
|
activeTool = undefined;
|
|
259
|
+
summary = "thinking...";
|
|
134
260
|
}
|
|
135
261
|
else if (value.type === "message_update") {
|
|
136
262
|
const message = record(value.message);
|
|
137
263
|
if (message?.role === "assistant") {
|
|
138
|
-
const
|
|
139
|
-
if (
|
|
140
|
-
summary =
|
|
264
|
+
const phase = assistantUpdatePhase(value.assistantMessageEvent, message);
|
|
265
|
+
if (phase?.kind === "thinking") {
|
|
266
|
+
summary = "thinking...";
|
|
267
|
+
activeTool = undefined;
|
|
268
|
+
}
|
|
269
|
+
else if (phase?.kind === "toolcall") {
|
|
270
|
+
activeTool = phase.toolName;
|
|
271
|
+
}
|
|
272
|
+
else if (phase?.kind === "generating") {
|
|
273
|
+
summary = "generating...";
|
|
141
274
|
activeTool = undefined;
|
|
142
275
|
}
|
|
143
276
|
}
|
|
144
277
|
}
|
|
145
278
|
else if (value.type === "agent_end" || value.type === "agent_settled") {
|
|
146
279
|
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
280
|
const messages = Array.isArray(value.messages) ? value.messages : [];
|
|
155
281
|
const message = record(messages.length > 0 ? messages[messages.length - 1] : value.message);
|
|
156
282
|
if (message?.role === "assistant") {
|
|
157
283
|
summary = text(message) ?? summary;
|
|
158
|
-
if (value.type === "
|
|
284
|
+
if (value.type === "agent_end" && message.stopReason === "error") {
|
|
285
|
+
state = "error";
|
|
159
286
|
interrupted = diagnostic(message);
|
|
160
|
-
if (message.stopReason === "error")
|
|
161
|
-
state = "error";
|
|
162
|
-
else
|
|
163
|
-
interrupted = undefined;
|
|
164
287
|
}
|
|
165
288
|
}
|
|
289
|
+
if (value.type === "agent_settled") {
|
|
290
|
+
if (value.aborted === true) {
|
|
291
|
+
state = "blocked";
|
|
292
|
+
interrupted = "Assistant turn was interrupted";
|
|
293
|
+
}
|
|
294
|
+
else if (message?.stopReason === "error") {
|
|
295
|
+
state = "error";
|
|
296
|
+
interrupted = diagnostic(message);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
state = "done";
|
|
300
|
+
interrupted = undefined;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
else if (value.type === "compaction_start") {
|
|
305
|
+
summary = "compacting...";
|
|
306
|
+
activeTool = undefined;
|
|
307
|
+
}
|
|
308
|
+
else if (value.type === "compaction_end") {
|
|
309
|
+
if (summary === "compacting...") {
|
|
310
|
+
summary = undefined;
|
|
311
|
+
}
|
|
166
312
|
}
|
|
167
313
|
else if (value.type === "error") {
|
|
168
314
|
state = "error";
|
|
@@ -172,27 +318,10 @@ export function createStateProjector(initialState = "running") {
|
|
|
172
318
|
return snapshot();
|
|
173
319
|
},
|
|
174
320
|
rebuild(entries) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const entry of entries) {
|
|
180
|
-
const item = assistant(entry);
|
|
181
|
-
if (item === undefined || !completed(item.message))
|
|
182
|
-
continue;
|
|
183
|
-
lastAssistant = item.message;
|
|
184
|
-
}
|
|
185
|
-
if (lastAssistant !== undefined) {
|
|
186
|
-
interrupted = diagnostic(lastAssistant);
|
|
187
|
-
if (lastAssistant.stopReason === "error")
|
|
188
|
-
state = "error";
|
|
189
|
-
else if (state === "error")
|
|
190
|
-
state = "done";
|
|
191
|
-
}
|
|
192
|
-
else {
|
|
193
|
-
interrupted = undefined;
|
|
194
|
-
}
|
|
195
|
-
return snapshot();
|
|
321
|
+
return applyFacts(transcriptFacts(entries));
|
|
322
|
+
},
|
|
323
|
+
applyTranscriptFacts(facts) {
|
|
324
|
+
return applyFacts(facts);
|
|
196
325
|
},
|
|
197
326
|
syncState(next) {
|
|
198
327
|
state = next;
|
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,14 @@
|
|
|
1
|
+
import type { ExtensionAPI, ToolRenderers } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
export interface AgentSpawnDetails {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly path: string;
|
|
5
|
+
}
|
|
6
|
+
/** One renderer set per tool, named exactly after its tool so nested calls resolve too. */
|
|
7
|
+
export declare function parentToolRenderers(pi: ExtensionAPI): {
|
|
8
|
+
readonly spawn: ToolRenderers;
|
|
9
|
+
readonly send: ToolRenderers;
|
|
10
|
+
readonly get: ToolRenderers;
|
|
11
|
+
readonly stop: ToolRenderers;
|
|
12
|
+
};
|
|
13
|
+
/** The child's blocker report names the reason in the header and keeps the full message readable. */
|
|
14
|
+
export declare function contactParentRenderers(pi: ExtensionAPI): ToolRenderers;
|