@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/domain.d.ts
CHANGED
|
@@ -67,6 +67,11 @@ export interface Selection {
|
|
|
67
67
|
/** Extensions and skills are selected the same way; the aliases keep the two roles named. */
|
|
68
68
|
export type ExtensionSelection = Selection;
|
|
69
69
|
export type SkillSelection = Selection;
|
|
70
|
+
export interface McpSelection {
|
|
71
|
+
readonly enabled: boolean;
|
|
72
|
+
readonly servers?: readonly string[];
|
|
73
|
+
readonly excluded?: readonly string[];
|
|
74
|
+
}
|
|
70
75
|
export interface EffectiveLaunchConfig {
|
|
71
76
|
/** Logical child id; the same value the child bridge reports back. */
|
|
72
77
|
readonly subagentId: string;
|
|
@@ -77,8 +82,6 @@ export interface EffectiveLaunchConfig {
|
|
|
77
82
|
readonly sessionDir: string;
|
|
78
83
|
/** Known only once the session has been flushed to disk. */
|
|
79
84
|
readonly sessionPath?: string;
|
|
80
|
-
/** Optional path to a prior session JSONL file referenced for on-demand context inspection. */
|
|
81
|
-
readonly referencedSessionPath?: string;
|
|
82
85
|
readonly agent: ResolvedAgentIdentity;
|
|
83
86
|
readonly model: ResolvedModel;
|
|
84
87
|
readonly thinking: ResolvedThinking;
|
|
@@ -86,10 +89,9 @@ export interface EffectiveLaunchConfig {
|
|
|
86
89
|
readonly excludeTools: readonly string[];
|
|
87
90
|
readonly extensions: ExtensionSelection;
|
|
88
91
|
readonly skills: SkillSelection;
|
|
92
|
+
readonly mcp: McpSelection;
|
|
89
93
|
readonly prompt: string;
|
|
90
94
|
readonly bridgeExtensionPath: string;
|
|
91
|
-
/** Frozen at spawn. Interactive children do not auto-wake the parent except via contact_parent. */
|
|
92
|
-
readonly interactive: boolean;
|
|
93
95
|
/** Whether the subagent operates in codemode-only mode. */
|
|
94
96
|
readonly codemodeOnly?: boolean;
|
|
95
97
|
/**
|
|
@@ -106,7 +108,7 @@ export interface ResolvedAgentPolicy {
|
|
|
106
108
|
readonly excludeTools: readonly string[];
|
|
107
109
|
readonly extensions: ExtensionSelection;
|
|
108
110
|
readonly skills: SkillSelection;
|
|
109
|
-
readonly
|
|
111
|
+
readonly mcp: McpSelection;
|
|
110
112
|
readonly codemodeOnly?: boolean;
|
|
111
113
|
readonly enabled?: boolean;
|
|
112
114
|
}
|
|
@@ -150,18 +152,20 @@ export interface SubagentRecord {
|
|
|
150
152
|
}
|
|
151
153
|
export interface PublicSubagent {
|
|
152
154
|
readonly id: string;
|
|
155
|
+
/** Pi-assigned session file path; the first write may still be pending. */
|
|
156
|
+
readonly path?: string;
|
|
153
157
|
readonly agent: string;
|
|
154
158
|
readonly displayName?: string;
|
|
155
159
|
readonly state: SubagentState;
|
|
156
160
|
readonly presentation: Presentation;
|
|
157
161
|
readonly cwd: string;
|
|
158
162
|
readonly sessionId: string;
|
|
163
|
+
readonly task?: string;
|
|
159
164
|
readonly summary?: string;
|
|
160
165
|
readonly activeTool?: string;
|
|
161
166
|
readonly usage?: UsageSummary;
|
|
162
167
|
readonly interrupted?: string;
|
|
163
168
|
readonly freshness: "live" | "last_known";
|
|
164
|
-
readonly interactive: boolean;
|
|
165
169
|
readonly model: ResolvedModel;
|
|
166
170
|
readonly thinking: ResolvedThinking;
|
|
167
171
|
readonly createdAt: string;
|
|
@@ -183,21 +187,17 @@ export interface SpawnSubagentInput {
|
|
|
183
187
|
* Where to run this child. `auto` (the default) means a panel whenever this parent has a
|
|
184
188
|
* presentation host, and the background otherwise; the Task tool pins `background` because a
|
|
185
189
|
* Task is settled by its parent and never handed to a human. This is not a model-facing
|
|
186
|
-
* parameter: `
|
|
190
|
+
* parameter: `agent_spawn` always spawns with `auto`.
|
|
187
191
|
*/
|
|
188
192
|
readonly presentation?: Presentation | "auto";
|
|
189
193
|
/** Optional child session title; the child branch prefixes it with the subagent marker. */
|
|
190
194
|
readonly title?: string;
|
|
191
|
-
/**
|
|
192
|
-
* Optional session or agent to fork context from (e.g. 'parent', or a subagent id like 'agent-1').
|
|
193
|
-
* Thinking blocks are stripped and context is compacted if pi-ext-memory is active.
|
|
194
|
-
*/
|
|
195
|
-
readonly forkFrom?: string;
|
|
196
195
|
}
|
|
197
196
|
/** True when a manager call returned a structured failure instead of a value. */
|
|
198
197
|
export declare function isOperationError(value: unknown): value is OperationError;
|
|
199
198
|
/** Child bridge tool name; agent tool policy must never remove it. */
|
|
200
199
|
export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
|
|
200
|
+
export declare const DEFAULT_PI_TOOLS: readonly ["read", "bash", "edit", "write"];
|
|
201
201
|
/** Environment contract between a parent launch and the child branch of this extension. */
|
|
202
202
|
export declare const BRIDGE_ENVIRONMENT_KEYS: {
|
|
203
203
|
readonly parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID";
|
package/dist/domain.js
CHANGED
|
@@ -10,12 +10,12 @@ export function normalizeSubagentState(value) {
|
|
|
10
10
|
case "idle":
|
|
11
11
|
return "running";
|
|
12
12
|
case "done":
|
|
13
|
+
case "stopped":
|
|
13
14
|
return "done";
|
|
14
15
|
case "blocked":
|
|
15
16
|
return "blocked";
|
|
16
17
|
case "error":
|
|
17
18
|
case "failed":
|
|
18
|
-
case "stopped":
|
|
19
19
|
return "error";
|
|
20
20
|
default:
|
|
21
21
|
return undefined;
|
|
@@ -53,6 +53,7 @@ export function isOperationError(value) {
|
|
|
53
53
|
}
|
|
54
54
|
/** Child bridge tool name; agent tool policy must never remove it. */
|
|
55
55
|
export const CONTACT_PARENT_TOOL_NAME = "contact_parent";
|
|
56
|
+
export const DEFAULT_PI_TOOLS = ["read", "bash", "edit", "write"];
|
|
56
57
|
/** Environment contract between a parent launch and the child branch of this extension. */
|
|
57
58
|
export const BRIDGE_ENVIRONMENT_KEYS = {
|
|
58
59
|
parentSessionId: "PI_SUBAGENTS_PARENT_SESSION_ID",
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import type { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ExtensionUIContext, Skill } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { type BackgroundDelivery } from "@hheei/pi-ext-core";
|
|
3
|
-
import type
|
|
3
|
+
import { type SkillCatalogEntry } from "./agent-resolver.js";
|
|
4
4
|
import { type ParentChannel } from "./manager.js";
|
|
5
5
|
/** Parent channel plus the teardown its owner must call. */
|
|
6
6
|
export interface ParentChannelHandle extends ParentChannel {
|
|
7
|
+
dismiss(childId: string): void;
|
|
7
8
|
/** Unregisters delivery and appends anything still held without waking the parent. */
|
|
8
9
|
dispose(): void;
|
|
9
10
|
}
|
|
@@ -14,8 +15,9 @@ export interface ParentChannelHandle extends ParentChannel {
|
|
|
14
15
|
* background work finishes; decision requests and blockers bypass that wait. The shared gate
|
|
15
16
|
* coordinates this channel with Task/Bash results so only one message requests a new idle turn.
|
|
16
17
|
*/
|
|
17
|
-
export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on">, deps: {
|
|
18
|
+
export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on" | "appendEntry">, deps: {
|
|
18
19
|
readonly isIdle: () => boolean;
|
|
20
|
+
readonly ui: Pick<ExtensionUIContext, "notify">;
|
|
19
21
|
readonly delivery: BackgroundDelivery;
|
|
20
22
|
readonly signal?: AbortSignal;
|
|
21
23
|
}): ParentChannelHandle;
|
package/dist/extension.js
CHANGED
|
@@ -1,21 +1,23 @@
|
|
|
1
1
|
import { errorMessage, getBackgroundDelivery, registerExtensionLifecycle, TASK_REGISTRY_SERVICE_KEY, waitForService, } from "@hheei/pi-ext-core";
|
|
2
|
+
import { discoverAgents } from "./agent-resolver.js";
|
|
2
3
|
import { ChildBridgeServer } from "./bridge-server.js";
|
|
3
4
|
import { registerChildBridge } from "./child-bridge.js";
|
|
4
|
-
import {
|
|
5
|
+
import { registerParentCommands } from "./commands.js";
|
|
5
6
|
import { BRIDGE_ENVIRONMENT_KEYS, isThinkingLevel } from "./domain.js";
|
|
6
7
|
import { createCmuxHostAdapter, createHerdrHostAdapter, selectHostAdapter, } from "./host-adapter.js";
|
|
8
|
+
import { appendSubagentInfo, displaySubagentText, registerSubagentInfo } from "./info.js";
|
|
7
9
|
import { SubagentManager } from "./manager.js";
|
|
8
10
|
import { transformSubagentsMarkdown } from "./markdown.js";
|
|
9
11
|
import { createSubagentRegistry } from "./registry.js";
|
|
10
12
|
import { createRuntimeTokenStore, launchChild, openChildPanel, parentBridgeEndpoint, prepareRuntimeDirectory, } from "./runtime.js";
|
|
11
13
|
import { persistSubagentIntent, resolveSubagentLaunch } from "./session-bootstrap.js";
|
|
12
|
-
import {
|
|
14
|
+
import { createSubagentStatus } from "./status.js";
|
|
13
15
|
import { registerInteractiveToolActivation } from "./tool-activation.js";
|
|
14
16
|
import { registerParentTools } from "./tools.js";
|
|
15
|
-
import { createSubagentWidget } from "./widget.js";
|
|
16
17
|
const CHILD_ENV_KEYS = Object.values(BRIDGE_ENVIRONMENT_KEYS);
|
|
17
18
|
/** How long startup waits for a child that survived a parent restart to reconnect its bridge. */
|
|
18
19
|
const ADOPT_WINDOW_MS = 5_000;
|
|
20
|
+
const START_WINDOW_MS = 20_000;
|
|
19
21
|
function diagnose(message) {
|
|
20
22
|
if (process.env.DEBUG || process.env.PI_SUBAGENTS_DEBUG) {
|
|
21
23
|
console.error(`pi-subagents: ${message}`);
|
|
@@ -53,7 +55,9 @@ export function createParentChannel(pi, deps) {
|
|
|
53
55
|
const flush = (triggerTurn, urgentOnly = false) => {
|
|
54
56
|
if (held.length === 0)
|
|
55
57
|
return;
|
|
56
|
-
const reports = urgentOnly
|
|
58
|
+
const reports = urgentOnly
|
|
59
|
+
? held.filter((report) => report.reason === "blocked" || report.reason === "error")
|
|
60
|
+
: [...held];
|
|
57
61
|
if (reports.length === 0)
|
|
58
62
|
return;
|
|
59
63
|
for (const report of reports)
|
|
@@ -62,7 +66,7 @@ export function createParentChannel(pi, deps) {
|
|
|
62
66
|
pi.sendMessage({
|
|
63
67
|
customType: "subagent-report",
|
|
64
68
|
content: reports.map(reportText).join("\n\n"),
|
|
65
|
-
display:
|
|
69
|
+
display: false,
|
|
66
70
|
details: { reports },
|
|
67
71
|
}, { triggerTurn, deliverAs: "steer" });
|
|
68
72
|
}
|
|
@@ -70,6 +74,15 @@ export function createParentChannel(pi, deps) {
|
|
|
70
74
|
held.unshift(...reports);
|
|
71
75
|
throw error;
|
|
72
76
|
}
|
|
77
|
+
for (const report of reports) {
|
|
78
|
+
try {
|
|
79
|
+
appendSubagentInfo(pi, report);
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
// The model already received this batch; an entry failure must not resend it.
|
|
83
|
+
deps.ui.notify(displaySubagentText(`Could not display subagent report for ${report.childId}: ${errorMessage(error)}`), "error");
|
|
84
|
+
}
|
|
85
|
+
}
|
|
73
86
|
};
|
|
74
87
|
const registration = deps.delivery.registerChannel({
|
|
75
88
|
isIdle: deps.isIdle,
|
|
@@ -83,7 +96,12 @@ export function createParentChannel(pi, deps) {
|
|
|
83
96
|
if (disposed)
|
|
84
97
|
throw new Error("Parent report channel is disposed");
|
|
85
98
|
held.push(report);
|
|
86
|
-
registration.request(report.reason === "blocked");
|
|
99
|
+
registration.request(report.reason === "blocked" || report.reason === "error");
|
|
100
|
+
},
|
|
101
|
+
dismiss(childId) {
|
|
102
|
+
const index = held.findIndex((report) => report.childId === childId);
|
|
103
|
+
if (index !== -1)
|
|
104
|
+
held.splice(index, 1);
|
|
87
105
|
},
|
|
88
106
|
dispose() {
|
|
89
107
|
disposed = true;
|
|
@@ -112,6 +130,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
112
130
|
registerChildBridge(pi, child);
|
|
113
131
|
return;
|
|
114
132
|
}
|
|
133
|
+
registerSubagentInfo(pi);
|
|
115
134
|
registerExtensionLifecycle(pi, {
|
|
116
135
|
key: "@hheei/pi-subagents",
|
|
117
136
|
async start(runtime) {
|
|
@@ -133,6 +152,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
133
152
|
const delivery = getBackgroundDelivery(pi);
|
|
134
153
|
const channel = createParentChannel(pi, {
|
|
135
154
|
isIdle: () => context.isIdle(),
|
|
155
|
+
ui: context.ui,
|
|
136
156
|
delivery,
|
|
137
157
|
signal: runtime.signal,
|
|
138
158
|
});
|
|
@@ -173,7 +193,8 @@ export default function piSubagentsExtension(pi) {
|
|
|
173
193
|
// the only name → path catalog; Pi hands it over before every prompt of this session.
|
|
174
194
|
let skillCatalog = [];
|
|
175
195
|
const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
|
|
176
|
-
|
|
196
|
+
const skills = event.systemPromptOptions?.skills ?? [];
|
|
197
|
+
skillCatalog = skillCatalogFromLoaded(skills);
|
|
177
198
|
});
|
|
178
199
|
runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
|
|
179
200
|
manager = new SubagentManager({
|
|
@@ -191,27 +212,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
191
212
|
}
|
|
192
213
|
const records = await registry.list().catch(() => []);
|
|
193
214
|
const existingSubagentIds = records.map((record) => record.subagentId);
|
|
194
|
-
let referencedSessionPath;
|
|
195
|
-
const forkTarget = input.forkFrom?.trim();
|
|
196
|
-
if (forkTarget !== undefined && forkTarget !== "") {
|
|
197
|
-
try {
|
|
198
|
-
if (forkTarget === "current") {
|
|
199
|
-
throw new Error("forkFrom cannot be 'current'. Use 'parent' to reference the parent session or specify a subagent id like 'agent-1'.");
|
|
200
|
-
}
|
|
201
|
-
const parentSessionFile = context.sessionManager.getSessionFile();
|
|
202
|
-
referencedSessionPath = await resolveForkSessionReference({
|
|
203
|
-
forkFrom: forkTarget,
|
|
204
|
-
...(parentSessionFile === undefined
|
|
205
|
-
? {}
|
|
206
|
-
: { parentSessionPath: parentSessionFile }),
|
|
207
|
-
registry,
|
|
208
|
-
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
catch (error) {
|
|
212
|
-
context.ui.notify(`Referencing session failed: ${errorMessage(error)}; starting fresh session without reference`, "warning");
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
215
|
return resolveSubagentLaunch({
|
|
216
216
|
input,
|
|
217
217
|
cwd: context.cwd,
|
|
@@ -219,8 +219,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
219
219
|
skillCatalog,
|
|
220
220
|
existingSubagentIds,
|
|
221
221
|
enforceEnabled: true,
|
|
222
|
-
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
223
|
-
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
224
222
|
parent: {
|
|
225
223
|
model: { provider: model.provider, id: model.id },
|
|
226
224
|
thinking,
|
|
@@ -237,7 +235,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
237
235
|
record,
|
|
238
236
|
tokens,
|
|
239
237
|
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
240
|
-
timeoutMs:
|
|
238
|
+
timeoutMs: START_WINDOW_MS,
|
|
241
239
|
...(signal === undefined ? {} : { signal }),
|
|
242
240
|
}),
|
|
243
241
|
signal: runtime.signal,
|
|
@@ -254,7 +252,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
254
252
|
tokens,
|
|
255
253
|
host: hostSelection.adapter,
|
|
256
254
|
waitForBridge: (childId, signal) => server.waitForConnection(childId, {
|
|
257
|
-
timeoutMs:
|
|
255
|
+
timeoutMs: START_WINDOW_MS,
|
|
258
256
|
...(signal === undefined ? {} : { signal }),
|
|
259
257
|
}),
|
|
260
258
|
signal: runtime.signal,
|
|
@@ -294,7 +292,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
294
292
|
.map((failure) => `- ${failure.childId}: ${failure.reason}`)
|
|
295
293
|
.join("\n")}`, "error");
|
|
296
294
|
}
|
|
297
|
-
registerParentTools(pi, manager);
|
|
295
|
+
registerParentTools(pi, manager, await discoverAgents(context.cwd));
|
|
298
296
|
if (typeof pi.registerMarkdownTransformer === "function") {
|
|
299
297
|
pi.registerMarkdownTransformer(transformSubagentsMarkdown);
|
|
300
298
|
}
|
|
@@ -315,26 +313,26 @@ export default function piSubagentsExtension(pi) {
|
|
|
315
313
|
.catch((error) => {
|
|
316
314
|
diagnose(`Task registry service unavailable for subagents: ${errorMessage(error)}`);
|
|
317
315
|
});
|
|
318
|
-
const
|
|
319
|
-
|
|
316
|
+
const status = createSubagentStatus(context.ui, runtime.signal);
|
|
317
|
+
let refreshVersion = 0;
|
|
318
|
+
const refreshStatus = () => {
|
|
320
319
|
if (runtime.signal.aborted)
|
|
321
320
|
return;
|
|
321
|
+
const version = ++refreshVersion;
|
|
322
322
|
void manager
|
|
323
323
|
.list()
|
|
324
324
|
.then((children) => {
|
|
325
|
-
if (runtime.signal.aborted)
|
|
325
|
+
if (runtime.signal.aborted || version !== refreshVersion)
|
|
326
326
|
return;
|
|
327
|
-
|
|
327
|
+
status.refresh(children);
|
|
328
328
|
})
|
|
329
|
-
.catch(() => {
|
|
329
|
+
.catch((error) => diagnose(`Subagent status failed: ${errorMessage(error)}`));
|
|
330
330
|
};
|
|
331
|
-
const unsubscribe = manager.onChange(
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
runtime.resources.add("subagent-widget", () => {
|
|
331
|
+
const unsubscribe = manager.onChange(refreshStatus);
|
|
332
|
+
refreshStatus();
|
|
333
|
+
runtime.resources.add("subagent-status", () => {
|
|
335
334
|
unsubscribe();
|
|
336
|
-
|
|
337
|
-
widget?.dispose();
|
|
335
|
+
status.dispose();
|
|
338
336
|
});
|
|
339
337
|
runtime.resources.add("subagent-manager", () => manager.closeLocalConnections());
|
|
340
338
|
},
|
package/dist/host-adapter.js
CHANGED
|
@@ -180,7 +180,7 @@ export function createCmuxHostAdapter(options) {
|
|
|
180
180
|
noun: "surface",
|
|
181
181
|
// TODO(cmux-focus): cmux does not report which surface a human is looking at, so
|
|
182
182
|
// observation cannot answer `focused` and idle reclaim fails open here: a cmux child
|
|
183
|
-
// is only ever closed by
|
|
183
|
+
// is only ever closed by agent_stop.
|
|
184
184
|
reportsFocus: false,
|
|
185
185
|
observe: async () => {
|
|
186
186
|
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
package/dist/info.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { ExtensionAPI, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { ParentChannelReport } from "./manager.js";
|
|
3
|
+
export declare const SUBAGENT_INFO_TYPE = "subagent-info";
|
|
4
|
+
/** Keep line breaks, expand tabs, and remove terminal control characters from child text. */
|
|
5
|
+
export declare function displaySubagentText(text: string): string;
|
|
6
|
+
export declare function renderSubagentInfo(data: unknown, expanded: boolean, theme: Pick<Theme, "fg">): string | undefined;
|
|
7
|
+
/** Native transcript entries survive resume and never enter the model context. */
|
|
8
|
+
export declare function registerSubagentInfo(pi: Pick<ExtensionAPI, "registerEntryRenderer">): void;
|
|
9
|
+
export declare function appendSubagentInfo(pi: Pick<ExtensionAPI, "appendEntry">, report: ParentChannelReport): void;
|
package/dist/info.js
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { Value } from "typebox/value";
|
|
5
|
+
import { VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE } from "./domain.js";
|
|
6
|
+
import { formatSubagentId } from "./status.js";
|
|
7
|
+
export const SUBAGENT_INFO_TYPE = "subagent-info";
|
|
8
|
+
const infoSchema = Type.Object({
|
|
9
|
+
id: Type.String(),
|
|
10
|
+
agent: Type.String(),
|
|
11
|
+
status: Type.Union([
|
|
12
|
+
Type.Literal("running"),
|
|
13
|
+
Type.Literal("done"),
|
|
14
|
+
Type.Literal("blocked"),
|
|
15
|
+
Type.Literal("error"),
|
|
16
|
+
]),
|
|
17
|
+
message: Type.String(),
|
|
18
|
+
});
|
|
19
|
+
/** Keep line breaks, expand tabs, and remove terminal control characters from child text. */
|
|
20
|
+
export function displaySubagentText(text) {
|
|
21
|
+
return stripVTControlCharacters(text)
|
|
22
|
+
.replaceAll("\t", " ")
|
|
23
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, "");
|
|
24
|
+
}
|
|
25
|
+
export function renderSubagentInfo(data, expanded, theme) {
|
|
26
|
+
if (!Value.Check(infoSchema, data))
|
|
27
|
+
return undefined;
|
|
28
|
+
const id = formatSubagentId(displaySubagentText(data.id).replace(/\s+/gu, " "));
|
|
29
|
+
const agent = displaySubagentText(data.agent).replace(/\s+/gu, " ");
|
|
30
|
+
const heading = theme.fg("dim", "info · agents · ") +
|
|
31
|
+
theme.fg(VISUAL_SUBAGENT_TONE[data.status], VISUAL_SUBAGENT_GLYPH[data.status]) +
|
|
32
|
+
theme.fg("dim", ` ${id} ${agent} · ${data.status}`);
|
|
33
|
+
return expanded ? `${heading}\n${displaySubagentText(data.message)}` : heading;
|
|
34
|
+
}
|
|
35
|
+
/** Native transcript entries survive resume and never enter the model context. */
|
|
36
|
+
export function registerSubagentInfo(pi) {
|
|
37
|
+
pi.registerEntryRenderer(SUBAGENT_INFO_TYPE, (entry, options, theme) => {
|
|
38
|
+
const text = renderSubagentInfo(entry.data, options.expanded, theme);
|
|
39
|
+
return text === undefined ? undefined : new Text(text, 1, 0);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
export function appendSubagentInfo(pi, report) {
|
|
43
|
+
const status = report.reason === "success"
|
|
44
|
+
? "done"
|
|
45
|
+
: report.reason === "blocked" || report.reason === "error"
|
|
46
|
+
? report.reason
|
|
47
|
+
: report.status;
|
|
48
|
+
pi.appendEntry(SUBAGENT_INFO_TYPE, {
|
|
49
|
+
id: report.childId,
|
|
50
|
+
agent: report.agent,
|
|
51
|
+
status,
|
|
52
|
+
message: report.message,
|
|
53
|
+
});
|
|
54
|
+
}
|
package/dist/launch-spec.d.ts
CHANGED
|
@@ -16,7 +16,6 @@ export interface LaunchSpec {
|
|
|
16
16
|
readonly env: Readonly<Record<string, string>>;
|
|
17
17
|
readonly config: EffectiveLaunchConfig;
|
|
18
18
|
}
|
|
19
|
-
export declare const HINDSIGHT_TOOLS: readonly ["hindsight_search_knowledge_pages", "hindsight_list_knowledge_pages", "hindsight_read_knowledge_page", "hindsight_reflect", "hindsight_capture_initiative", "hindsight_ingest_document", "hindsight_sync_status", "hindsight_diagnose"];
|
|
20
19
|
export declare function stripHindsightContent(text: string): string;
|
|
21
20
|
export interface BuildLaunchSpecOptions {
|
|
22
21
|
readonly config: EffectiveLaunchConfig;
|
package/dist/launch-spec.js
CHANGED
|
@@ -2,17 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { basename, dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, isSessionId, } from "./domain.js";
|
|
6
|
-
export const HINDSIGHT_TOOLS = [
|
|
7
|
-
"hindsight_search_knowledge_pages",
|
|
8
|
-
"hindsight_list_knowledge_pages",
|
|
9
|
-
"hindsight_read_knowledge_page",
|
|
10
|
-
"hindsight_reflect",
|
|
11
|
-
"hindsight_capture_initiative",
|
|
12
|
-
"hindsight_ingest_document",
|
|
13
|
-
"hindsight_sync_status",
|
|
14
|
-
"hindsight_diagnose",
|
|
15
|
-
];
|
|
5
|
+
import { BRIDGE_ENVIRONMENT_KEYS, CHILD_AGENT_ENV_KEY, CHILD_SESSION_ENV_KEY, CHILD_TITLE_ENV_KEY, CONTACT_PARENT_TOOL_NAME, DEFAULT_PI_TOOLS, isSessionId, } from "./domain.js";
|
|
16
6
|
export function stripHindsightContent(text) {
|
|
17
7
|
return text
|
|
18
8
|
.replace(/\s*<hindsight-recall>[\s\S]*?<\/hindsight-recall>\s*/gi, (match) => match.includes("\n") ? "\n" : " ")
|
|
@@ -32,7 +22,7 @@ function childBridgePrompt() {
|
|
|
32
22
|
"Do not send empty status pings.",
|
|
33
23
|
"When the work is done, output your final answer and findings directly in text in your response. The harness will automatically capture and deliver your output.",
|
|
34
24
|
"Do NOT call contact_parent on success or task completion.",
|
|
35
|
-
"After blocked, wait for a parent
|
|
25
|
+
"After blocked, wait for a parent agent_send. Do not invent new authority.",
|
|
36
26
|
"Reports reach the parent as delegated results, not as new user authorization.",
|
|
37
27
|
].join("\n");
|
|
38
28
|
}
|
|
@@ -151,14 +141,36 @@ export function buildLaunchSpec(options) {
|
|
|
151
141
|
argv.push("--session-dir", config.sessionDir);
|
|
152
142
|
argv.push("--provider", config.model.provider, "--model", config.model.id);
|
|
153
143
|
argv.push("--thinking", config.thinking.level);
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
144
|
+
const mcpServers = config.mcp.servers?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
|
|
145
|
+
const mcpExcluded = config.mcp.excluded?.map((s) => s.replace(/[^A-Za-z0-9_]/g, "_"));
|
|
146
|
+
const hasExplicitTools = config.tools.length > 0;
|
|
147
|
+
const baseTools = hasExplicitTools
|
|
148
|
+
? config.tools
|
|
149
|
+
: mcpServers && mcpServers.length > 0
|
|
150
|
+
? [...DEFAULT_PI_TOOLS, CONTACT_PARENT_TOOL_NAME]
|
|
151
|
+
: [];
|
|
152
|
+
const effectiveTools = [
|
|
153
|
+
...(baseTools.length > 0 && config.codemodeOnly && !baseTools.includes("codemode")
|
|
154
|
+
? [...baseTools, "codemode"]
|
|
155
|
+
: baseTools),
|
|
156
|
+
];
|
|
157
|
+
const excludeTools = Array.from(new Set(config.excludeTools));
|
|
158
|
+
if (config.mcp.enabled) {
|
|
159
|
+
if (mcpServers && mcpServers.length > 0) {
|
|
160
|
+
for (const server of mcpServers) {
|
|
161
|
+
effectiveTools.push(`mcp__${server}__*`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
if (mcpExcluded && mcpExcluded.length > 0) {
|
|
165
|
+
for (const server of mcpExcluded) {
|
|
166
|
+
excludeTools.push(`mcp__${server}__*`);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
157
170
|
if (effectiveTools.length > 0)
|
|
158
|
-
argv.push("--tools", effectiveTools.join(","));
|
|
159
|
-
const excludeTools = Array.from(new Set([...config.excludeTools, ...HINDSIGHT_TOOLS]));
|
|
171
|
+
argv.push("--tools", Array.from(new Set(effectiveTools)).join(","));
|
|
160
172
|
if (excludeTools.length > 0)
|
|
161
|
-
argv.push("--exclude-tools", excludeTools.join(","));
|
|
173
|
+
argv.push("--exclude-tools", Array.from(new Set(excludeTools)).join(","));
|
|
162
174
|
if (!config.extensions.discovery)
|
|
163
175
|
argv.push("--no-extensions");
|
|
164
176
|
for (const path of config.extensions.paths)
|
|
@@ -167,6 +179,8 @@ export function buildLaunchSpec(options) {
|
|
|
167
179
|
argv.push("--no-skills");
|
|
168
180
|
for (const path of config.skills.paths)
|
|
169
181
|
argv.push("--skill", path);
|
|
182
|
+
if (!config.mcp.enabled)
|
|
183
|
+
argv.push("--no-mcp");
|
|
170
184
|
argv.push("--append-system-prompt", config.prompt);
|
|
171
185
|
const env = {
|
|
172
186
|
[BRIDGE_ENVIRONMENT_KEYS.parentSessionId]: options.bridge.parentSessionId,
|
|
@@ -179,6 +193,12 @@ export function buildLaunchSpec(options) {
|
|
|
179
193
|
[CHILD_TITLE_ENV_KEY]: config.title ?? "",
|
|
180
194
|
PI_HINDSIGHT_DISABLE: "1",
|
|
181
195
|
...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
|
|
196
|
+
...(mcpExcluded && mcpExcluded.length > 0
|
|
197
|
+
? { PI_SUBAGENT_MCP_EXCLUDE: mcpExcluded.join(",") }
|
|
198
|
+
: {}),
|
|
199
|
+
...(mcpServers && mcpServers.length > 0
|
|
200
|
+
? { PI_SUBAGENT_MCP_SERVERS: mcpServers.join(",") }
|
|
201
|
+
: {}),
|
|
182
202
|
};
|
|
183
203
|
return Object.freeze({
|
|
184
204
|
command: options.invocation.command,
|
package/dist/manager.d.ts
CHANGED
|
@@ -7,7 +7,7 @@ import type { SubagentRegistry } from "./registry.js";
|
|
|
7
7
|
import type { LaunchOutcome, RuntimeTokenStore } from "./runtime.js";
|
|
8
8
|
/**
|
|
9
9
|
* How long an idle child keeps its runtime. A background child holds a whole Pi process and its
|
|
10
|
-
* session lock, so an idle one is reclaimed after this window and a later `
|
|
10
|
+
* session lock, so an idle one is reclaimed after this window and a later `agent_send` starts it
|
|
11
11
|
* again from the same session.
|
|
12
12
|
*/
|
|
13
13
|
export declare const SUBAGENT_IDLE_TIMEOUT_MS = 15000;
|
|
@@ -25,6 +25,7 @@ export interface ParentChannelReport {
|
|
|
25
25
|
}
|
|
26
26
|
export interface ParentChannel {
|
|
27
27
|
deliver(report: ParentChannelReport): Promise<void>;
|
|
28
|
+
dismiss?(childId: string): void;
|
|
28
29
|
}
|
|
29
30
|
/**
|
|
30
31
|
* The parent side of the bridge, as the manager needs it: one connection per child, requests the
|
|
@@ -82,6 +83,7 @@ export interface PresentationDeps {
|
|
|
82
83
|
}
|
|
83
84
|
export interface SpawnResult {
|
|
84
85
|
readonly child: PublicSubagent;
|
|
86
|
+
readonly path: string;
|
|
85
87
|
/** Set when the child did not run where it would have by default. */
|
|
86
88
|
readonly note?: string;
|
|
87
89
|
}
|
|
@@ -106,7 +108,7 @@ export declare class SubagentManager {
|
|
|
106
108
|
* `task_result` and settle events without also owning the bridge connection.
|
|
107
109
|
*/
|
|
108
110
|
onChildEvent(listener: (childId: string, event: unknown) => void): () => void;
|
|
109
|
-
/** Presentation-only:
|
|
111
|
+
/** Presentation-only: status and similar projections re-read list() after this. */
|
|
110
112
|
onChange(listener: () => void): () => void;
|
|
111
113
|
dispose(): void;
|
|
112
114
|
/**
|