@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/extension.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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 {
|
|
@@ -10,12 +10,13 @@ export interface ParentChannelHandle extends ParentChannel {
|
|
|
10
10
|
/**
|
|
11
11
|
* How child reports reach the parent conversation.
|
|
12
12
|
*
|
|
13
|
-
* Busy parents receive a steer into the next model step. Idle parents
|
|
14
|
-
*
|
|
15
|
-
*
|
|
13
|
+
* Busy parents receive a steer into the next model step. Idle parents get one new turn per
|
|
14
|
+
* microtask batch; the shared coordinator makes this channel and the Bash one agree on which
|
|
15
|
+
* message requests it.
|
|
16
16
|
*/
|
|
17
|
-
export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on">, deps: {
|
|
17
|
+
export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on" | "appendEntry">, deps: {
|
|
18
18
|
readonly isIdle: () => boolean;
|
|
19
|
+
readonly ui: Pick<ExtensionUIContext, "notify">;
|
|
19
20
|
readonly delivery: BackgroundDelivery;
|
|
20
21
|
readonly signal?: AbortSignal;
|
|
21
22
|
}): ParentChannelHandle;
|
package/dist/extension.js
CHANGED
|
@@ -1,18 +1,19 @@
|
|
|
1
|
-
import { errorMessage, getBackgroundDelivery, registerExtensionLifecycle,
|
|
2
|
-
import {
|
|
1
|
+
import { errorMessage, getBackgroundDelivery, registerExtensionLifecycle, } from "@hheei/pi-ext-core";
|
|
2
|
+
import { discoverAgents } from "./agent-resolver.js";
|
|
3
3
|
import { registerChildBridge } from "./child-bridge.js";
|
|
4
|
-
import {
|
|
4
|
+
import { registerParentCommands } from "./commands.js";
|
|
5
5
|
import { BRIDGE_ENVIRONMENT_KEYS, isThinkingLevel } from "./domain.js";
|
|
6
|
-
import { createCmuxHostAdapter, createHerdrHostAdapter, selectHostAdapter, } from "./host-adapter.js";
|
|
6
|
+
import { createCmuxHostAdapter, createHerdrHostAdapter, listCmuxAttachments, listHerdrAttachments, rebindCmuxSurface, rebindHerdrTab, selectHostAdapter, systemHostCommandRunner, } from "./host-adapter.js";
|
|
7
|
+
import { appendSubagentInfo, displaySubagentText, registerSubagentInfo } from "./info.js";
|
|
8
|
+
import { createMailboxTransport } from "./mailbox.js";
|
|
7
9
|
import { SubagentManager } from "./manager.js";
|
|
8
10
|
import { transformSubagentsMarkdown } from "./markdown.js";
|
|
9
11
|
import { createSubagentRegistry } from "./registry.js";
|
|
10
|
-
import { createRuntimeTokenStore, launchChild, openChildPanel,
|
|
12
|
+
import { createRuntimeTokenStore, launchChild, openChildPanel, 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;
|
|
@@ -44,26 +45,23 @@ function childIdentityFromEnv(env) {
|
|
|
44
45
|
/**
|
|
45
46
|
* How child reports reach the parent conversation.
|
|
46
47
|
*
|
|
47
|
-
* Busy parents receive a steer into the next model step. Idle parents
|
|
48
|
-
*
|
|
49
|
-
*
|
|
48
|
+
* Busy parents receive a steer into the next model step. Idle parents get one new turn per
|
|
49
|
+
* microtask batch; the shared coordinator makes this channel and the Bash one agree on which
|
|
50
|
+
* message requests it.
|
|
50
51
|
*/
|
|
51
52
|
export function createParentChannel(pi, deps) {
|
|
52
53
|
const held = [];
|
|
53
54
|
let disposed = false;
|
|
54
|
-
const flush = (triggerTurn
|
|
55
|
+
const flush = (triggerTurn) => {
|
|
55
56
|
if (held.length === 0)
|
|
56
57
|
return;
|
|
57
|
-
const reports =
|
|
58
|
-
|
|
59
|
-
return;
|
|
60
|
-
for (const report of reports)
|
|
61
|
-
held.splice(held.indexOf(report), 1);
|
|
58
|
+
const reports = [...held];
|
|
59
|
+
held.length = 0;
|
|
62
60
|
try {
|
|
63
61
|
pi.sendMessage({
|
|
64
62
|
customType: "subagent-report",
|
|
65
63
|
content: reports.map(reportText).join("\n\n"),
|
|
66
|
-
display:
|
|
64
|
+
display: false,
|
|
67
65
|
details: { reports },
|
|
68
66
|
}, { triggerTurn, deliverAs: "steer" });
|
|
69
67
|
}
|
|
@@ -71,6 +69,15 @@ export function createParentChannel(pi, deps) {
|
|
|
71
69
|
held.unshift(...reports);
|
|
72
70
|
throw error;
|
|
73
71
|
}
|
|
72
|
+
for (const report of reports) {
|
|
73
|
+
try {
|
|
74
|
+
appendSubagentInfo(pi, report);
|
|
75
|
+
}
|
|
76
|
+
catch (error) {
|
|
77
|
+
// The model already received this batch; an entry failure must not resend it.
|
|
78
|
+
deps.ui.notify(displaySubagentText(`Could not display subagent report for ${report.childId}: ${errorMessage(error)}`), "error");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
74
81
|
};
|
|
75
82
|
const registration = deps.delivery.registerChannel({
|
|
76
83
|
isIdle: deps.isIdle,
|
|
@@ -84,7 +91,7 @@ export function createParentChannel(pi, deps) {
|
|
|
84
91
|
if (disposed)
|
|
85
92
|
throw new Error("Parent report channel is disposed");
|
|
86
93
|
held.push(report);
|
|
87
|
-
registration.request(
|
|
94
|
+
registration.request();
|
|
88
95
|
},
|
|
89
96
|
dispose() {
|
|
90
97
|
disposed = true;
|
|
@@ -113,6 +120,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
113
120
|
registerChildBridge(pi, child);
|
|
114
121
|
return;
|
|
115
122
|
}
|
|
123
|
+
registerSubagentInfo(pi);
|
|
116
124
|
registerExtensionLifecycle(pi, {
|
|
117
125
|
key: "@hheei/pi-subagents",
|
|
118
126
|
async start(runtime) {
|
|
@@ -134,47 +142,27 @@ export default function piSubagentsExtension(pi) {
|
|
|
134
142
|
const delivery = getBackgroundDelivery(pi);
|
|
135
143
|
const channel = createParentChannel(pi, {
|
|
136
144
|
isIdle: () => context.isIdle(),
|
|
145
|
+
ui: context.ui,
|
|
137
146
|
delivery,
|
|
138
147
|
signal: runtime.signal,
|
|
139
148
|
});
|
|
140
149
|
runtime.resources.add("parent-channel", () => channel.dispose());
|
|
141
150
|
let manager;
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
endpoint: parentBridgeEndpoint(parentSessionId),
|
|
151
|
+
// One sqlite file is the ledger and the mailbox. Presence is not liveness.
|
|
152
|
+
const mailbox = createMailboxTransport({
|
|
153
|
+
filePath: registry.path,
|
|
146
154
|
parentSessionId,
|
|
147
|
-
authorize: async (frame) => {
|
|
148
|
-
const expected = tokens.get(frame.runtimeIdentity);
|
|
149
|
-
if (expected === undefined) {
|
|
150
|
-
return `no bridge token is recorded for runtime ${frame.runtimeIdentity}`;
|
|
151
|
-
}
|
|
152
|
-
if (expected !== frame.token)
|
|
153
|
-
return "bridge token mismatch";
|
|
154
|
-
// The token says which runtime is calling; the registry says which child that runtime
|
|
155
|
-
// belongs to. Without this check a child could name another child's id and take over
|
|
156
|
-
// its control channel, so a hello that does not match its record is refused.
|
|
157
|
-
const record = await registry.get(frame.subagentId).catch(() => undefined);
|
|
158
|
-
if (record === undefined)
|
|
159
|
-
return `no recorded child ${frame.subagentId}`;
|
|
160
|
-
if (record.runtime?.runtimeIdentity !== frame.runtimeIdentity) {
|
|
161
|
-
return `runtime ${frame.runtimeIdentity} is not the recorded runtime of child ${frame.subagentId}`;
|
|
162
|
-
}
|
|
163
|
-
return undefined;
|
|
164
|
-
},
|
|
165
155
|
handleRequest: (request) => manager.handleChildRequest(request.childId, request.operation, request.payload),
|
|
166
156
|
diagnose,
|
|
167
157
|
});
|
|
168
158
|
await prepareRuntimeDirectory(diagnose);
|
|
169
|
-
|
|
170
|
-
// Closing is awaited: the endpoint is unlinked after the server is closed, so a reload that
|
|
171
|
-
// started the next server first could have this close unlink the new socket path.
|
|
172
|
-
runtime.resources.add("subagent-bridge", () => server.close());
|
|
159
|
+
runtime.resources.add("subagent-bridge", () => mailbox.close());
|
|
173
160
|
// A definition names skills the way Pi lists them, so the parent's loaded skill set is
|
|
174
161
|
// the only name → path catalog; Pi hands it over before every prompt of this session.
|
|
175
162
|
let skillCatalog = [];
|
|
176
163
|
const unsubscribeSkillCatalog = pi.on("before_agent_start", (event) => {
|
|
177
|
-
|
|
164
|
+
const skills = event.systemPromptOptions?.skills ?? [];
|
|
165
|
+
skillCatalog = skillCatalogFromLoaded(skills);
|
|
178
166
|
});
|
|
179
167
|
runtime.resources.add("subagent-skill-catalog", unsubscribeSkillCatalog);
|
|
180
168
|
manager = new SubagentManager({
|
|
@@ -192,27 +180,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
192
180
|
}
|
|
193
181
|
const records = await registry.list().catch(() => []);
|
|
194
182
|
const existingSubagentIds = records.map((record) => record.subagentId);
|
|
195
|
-
let referencedSessionPath;
|
|
196
|
-
const forkTarget = input.forkFrom?.trim();
|
|
197
|
-
if (forkTarget !== undefined && forkTarget !== "") {
|
|
198
|
-
try {
|
|
199
|
-
if (forkTarget === "current") {
|
|
200
|
-
throw new Error("forkFrom cannot be 'current'. Use 'parent' to reference the parent session or specify a subagent id like 'agent-1'.");
|
|
201
|
-
}
|
|
202
|
-
const parentSessionFile = context.sessionManager.getSessionFile();
|
|
203
|
-
referencedSessionPath = await resolveForkSessionReference({
|
|
204
|
-
forkFrom: forkTarget,
|
|
205
|
-
...(parentSessionFile === undefined
|
|
206
|
-
? {}
|
|
207
|
-
: { parentSessionPath: parentSessionFile }),
|
|
208
|
-
registry,
|
|
209
|
-
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
catch (error) {
|
|
213
|
-
context.ui.notify(`Referencing session failed: ${errorMessage(error)}; starting fresh session without reference`, "warning");
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
183
|
return resolveSubagentLaunch({
|
|
217
184
|
input,
|
|
218
185
|
cwd: context.cwd,
|
|
@@ -220,8 +187,6 @@ export default function piSubagentsExtension(pi) {
|
|
|
220
187
|
skillCatalog,
|
|
221
188
|
existingSubagentIds,
|
|
222
189
|
enforceEnabled: true,
|
|
223
|
-
onWarning: (message) => context.ui.notify(message, "warning"),
|
|
224
|
-
...(referencedSessionPath === undefined ? {} : { referencedSessionPath }),
|
|
225
190
|
parent: {
|
|
226
191
|
model: { provider: model.provider, id: model.id },
|
|
227
192
|
thinking,
|
|
@@ -231,13 +196,13 @@ export default function piSubagentsExtension(pi) {
|
|
|
231
196
|
bootstrap(input) {
|
|
232
197
|
return persistSubagentIntent({ registry, ...input });
|
|
233
198
|
},
|
|
234
|
-
bridge:
|
|
199
|
+
bridge: mailbox,
|
|
235
200
|
launch(record) {
|
|
236
201
|
return launchChild({
|
|
237
202
|
registry,
|
|
238
203
|
record,
|
|
239
204
|
tokens,
|
|
240
|
-
waitForBridge: (childId, signal) =>
|
|
205
|
+
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
241
206
|
timeoutMs: START_WINDOW_MS,
|
|
242
207
|
...(signal === undefined ? {} : { signal }),
|
|
243
208
|
}),
|
|
@@ -254,7 +219,7 @@ export default function piSubagentsExtension(pi) {
|
|
|
254
219
|
record,
|
|
255
220
|
tokens,
|
|
256
221
|
host: hostSelection.adapter,
|
|
257
|
-
waitForBridge: (childId, signal) =>
|
|
222
|
+
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
258
223
|
timeoutMs: START_WINDOW_MS,
|
|
259
224
|
...(signal === undefined ? {} : { signal }),
|
|
260
225
|
}),
|
|
@@ -267,35 +232,64 @@ export default function piSubagentsExtension(pi) {
|
|
|
267
232
|
// Adoption is a wait, never a spawn: a child that survived this parent reconnects on
|
|
268
233
|
// its own, and one that did not stays recorded until a send starts it again.
|
|
269
234
|
connect(record) {
|
|
270
|
-
return
|
|
235
|
+
return mailbox.waitUntilSeen(record.subagentId, {
|
|
271
236
|
timeoutMs: ADOPT_WINDOW_MS,
|
|
272
237
|
signal: runtime.signal,
|
|
273
238
|
});
|
|
274
239
|
},
|
|
240
|
+
...(hostSelection.available
|
|
241
|
+
? {
|
|
242
|
+
directory: {
|
|
243
|
+
host: hostSelection.selectedHost,
|
|
244
|
+
list: () => hostSelection.selectedHost === "herdr"
|
|
245
|
+
? listHerdrAttachments(systemHostCommandRunner, 10_000)
|
|
246
|
+
: listCmuxAttachments(systemHostCommandRunner, 10_000),
|
|
247
|
+
open: async (record) => {
|
|
248
|
+
const launch = await openChildPanel({
|
|
249
|
+
registry,
|
|
250
|
+
record,
|
|
251
|
+
tokens,
|
|
252
|
+
host: hostSelection.adapter,
|
|
253
|
+
waitForBridge: (childId, signal) => mailbox.waitUntilSeen(childId, {
|
|
254
|
+
timeoutMs: START_WINDOW_MS,
|
|
255
|
+
...(signal === undefined ? {} : { signal }),
|
|
256
|
+
}),
|
|
257
|
+
signal: runtime.signal,
|
|
258
|
+
diagnose,
|
|
259
|
+
});
|
|
260
|
+
if (launch.handle !== undefined) {
|
|
261
|
+
manager.holdAttachment(record.subagentId, launch.handle);
|
|
262
|
+
}
|
|
263
|
+
if (launch.handle === undefined || launch.failure !== undefined) {
|
|
264
|
+
return { failure: launch.failure ?? "panel did not open" };
|
|
265
|
+
}
|
|
266
|
+
return { id: launch.handle.identity.attachmentId };
|
|
267
|
+
},
|
|
268
|
+
rebind: (attachment) => {
|
|
269
|
+
if (attachment.kind !== "panel")
|
|
270
|
+
return undefined;
|
|
271
|
+
return attachment.host === "herdr"
|
|
272
|
+
? rebindHerdrTab({ tabId: attachment.id, ownerId, ownsAttachment })
|
|
273
|
+
: rebindCmuxSurface({ surfaceId: attachment.id, ownerId, ownsAttachment });
|
|
274
|
+
},
|
|
275
|
+
},
|
|
276
|
+
}
|
|
277
|
+
: {}),
|
|
275
278
|
});
|
|
276
|
-
const unregisterWork = delivery.registerSource({
|
|
277
|
-
activeCount: () => manager.activeCount,
|
|
278
|
-
onChange: (listener) => manager.onChange(listener),
|
|
279
|
-
});
|
|
280
|
-
runtime.resources.add("subagent-background-work", unregisterWork);
|
|
281
279
|
// Recovery must never take the extension down with it: a registry this version cannot
|
|
282
280
|
// read (for example records written by an older release) is reported in the session and
|
|
283
281
|
// the user is told how to reset it.
|
|
284
|
-
const recovery = await manager.
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
],
|
|
292
|
-
}));
|
|
293
|
-
if (recovery.failures.length > 0) {
|
|
294
|
-
context.ui.notify(`Subagent recovery failed:\n${recovery.failures
|
|
295
|
-
.map((failure) => `- ${failure.childId}: ${failure.reason}`)
|
|
296
|
-
.join("\n")}`, "error");
|
|
282
|
+
const recovery = await manager.reconcile().catch((error) => {
|
|
283
|
+
context.ui.notify(`Subagent recovery failed: ${errorMessage(error)}. Delete the stale records (or the whole registry file) to reset this parent session's subagent state.`, "error");
|
|
284
|
+
return undefined;
|
|
285
|
+
});
|
|
286
|
+
if (recovery?.ok === false) {
|
|
287
|
+
context.ui.notify("Subagent panels could not be listed, so none were reopened. No second runtime was started.", "warning");
|
|
297
288
|
}
|
|
298
|
-
|
|
289
|
+
pi.on("before_agent_start", async () => {
|
|
290
|
+
await manager.ready();
|
|
291
|
+
});
|
|
292
|
+
registerParentTools(pi, manager, await discoverAgents(context.cwd));
|
|
299
293
|
if (typeof pi.registerMarkdownTransformer === "function") {
|
|
300
294
|
pi.registerMarkdownTransformer(transformSubagentsMarkdown);
|
|
301
295
|
}
|
|
@@ -306,36 +300,26 @@ export default function piSubagentsExtension(pi) {
|
|
|
306
300
|
});
|
|
307
301
|
runtime.resources.add("interactive-tool-activation", () => activation.dispose());
|
|
308
302
|
registerParentCommands(pi, manager);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return;
|
|
313
|
-
const unbind = manager.bindTaskRegistry(taskRegistry);
|
|
314
|
-
runtime.resources.add("subagent-task-registry", unbind);
|
|
315
|
-
})
|
|
316
|
-
.catch((error) => {
|
|
317
|
-
diagnose(`Task registry service unavailable for subagents: ${errorMessage(error)}`);
|
|
318
|
-
});
|
|
319
|
-
const widget = createSubagentWidget(pi, context, runtime.signal);
|
|
320
|
-
const refreshWidget = () => {
|
|
303
|
+
const status = createSubagentStatus(context.ui, runtime.signal);
|
|
304
|
+
let refreshVersion = 0;
|
|
305
|
+
const refreshStatus = () => {
|
|
321
306
|
if (runtime.signal.aborted)
|
|
322
307
|
return;
|
|
308
|
+
const version = ++refreshVersion;
|
|
323
309
|
void manager
|
|
324
310
|
.list()
|
|
325
311
|
.then((children) => {
|
|
326
|
-
if (runtime.signal.aborted)
|
|
312
|
+
if (runtime.signal.aborted || version !== refreshVersion)
|
|
327
313
|
return;
|
|
328
|
-
|
|
314
|
+
status.refresh(children);
|
|
329
315
|
})
|
|
330
|
-
.catch(() => {
|
|
316
|
+
.catch((error) => diagnose(`Subagent status failed: ${errorMessage(error)}`));
|
|
331
317
|
};
|
|
332
|
-
const unsubscribe = manager.onChange(
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
runtime.resources.add("subagent-widget", () => {
|
|
318
|
+
const unsubscribe = manager.onChange(refreshStatus);
|
|
319
|
+
refreshStatus();
|
|
320
|
+
runtime.resources.add("subagent-status", () => {
|
|
336
321
|
unsubscribe();
|
|
337
|
-
|
|
338
|
-
widget?.dispose();
|
|
322
|
+
status.dispose();
|
|
339
323
|
});
|
|
340
324
|
runtime.resources.add("subagent-manager", () => manager.closeLocalConnections());
|
|
341
325
|
},
|
package/dist/host-adapter.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LaunchSpec } from "./launch-spec.js";
|
|
2
|
+
import type { HostList } from "./lease.js";
|
|
2
3
|
export type HostKind = "herdr" | "cmux";
|
|
3
4
|
export interface HostCommandResult {
|
|
4
5
|
readonly stdout: string;
|
|
@@ -93,6 +94,24 @@ export declare function resolveHerdrProcessWorkspaceId(runner: HostCommandRunner
|
|
|
93
94
|
* on an unfocused answer.
|
|
94
95
|
*/
|
|
95
96
|
export declare function herdrPanelFocused(tabList: string, workspaceList: string, tabId: string): boolean | undefined;
|
|
97
|
+
export declare function listHerdrAttachments(runner: HostCommandRunner, timeoutMs: number): Promise<HostList>;
|
|
98
|
+
export declare function herdrTabIds(output: string): ReadonlySet<string> | undefined;
|
|
99
|
+
export declare function listCmuxAttachments(runner: HostCommandRunner, timeoutMs: number): Promise<HostList>;
|
|
100
|
+
export declare function cmuxSurfaceIds(output: string): ReadonlySet<string> | undefined;
|
|
101
|
+
export declare function rebindHerdrTab(options: {
|
|
102
|
+
readonly runner?: HostCommandRunner;
|
|
103
|
+
readonly tabId: string;
|
|
104
|
+
readonly ownerId: string;
|
|
105
|
+
readonly timeoutMs?: number;
|
|
106
|
+
readonly ownsAttachment: (identity: HostAttachmentIdentity) => boolean;
|
|
107
|
+
}): HostAttachment;
|
|
108
|
+
export declare function rebindCmuxSurface(options: {
|
|
109
|
+
readonly runner?: HostCommandRunner;
|
|
110
|
+
readonly surfaceId: string;
|
|
111
|
+
readonly ownerId: string;
|
|
112
|
+
readonly timeoutMs?: number;
|
|
113
|
+
readonly ownsAttachment: (identity: HostAttachmentIdentity) => boolean;
|
|
114
|
+
}): HostAttachment;
|
|
96
115
|
/**
|
|
97
116
|
* Reads the pane's process state. A pane that no longer exists is a definite "not alive" — that is
|
|
98
117
|
* how a closed panel looks — while a query that never ran or failed for any other reason is
|
package/dist/host-adapter.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { statSync } from "node:fs";
|
|
1
2
|
import { errorMessage, runCommand, shellQuote } from "@hheei/pi-ext-core";
|
|
2
3
|
const DEFAULT_HOST_ORDER = ["herdr", "cmux"];
|
|
3
4
|
/** Selects a presentation host visibly; an explicit unavailable host never falls back. */
|
|
@@ -180,7 +181,7 @@ export function createCmuxHostAdapter(options) {
|
|
|
180
181
|
noun: "surface",
|
|
181
182
|
// TODO(cmux-focus): cmux does not report which surface a human is looking at, so
|
|
182
183
|
// observation cannot answer `focused` and idle reclaim fails open here: a cmux child
|
|
183
|
-
// is only ever closed by
|
|
184
|
+
// is only ever closed by agent_stop.
|
|
184
185
|
reportsFocus: false,
|
|
185
186
|
observe: async () => {
|
|
186
187
|
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
@@ -302,6 +303,144 @@ export function herdrPanelFocused(tabList, workspaceList, tabId) {
|
|
|
302
303
|
const workspaceFocused = workspace?.focused;
|
|
303
304
|
return typeof workspaceFocused === "boolean" ? workspaceFocused : undefined;
|
|
304
305
|
}
|
|
306
|
+
export async function listHerdrAttachments(runner, timeoutMs) {
|
|
307
|
+
const status = await runner.run("herdr", ["status", "server"], { timeoutMs });
|
|
308
|
+
if (status.timedOut || status.exitCode !== 0)
|
|
309
|
+
return { known: false };
|
|
310
|
+
const socket = statusField(status.stdout, "socket");
|
|
311
|
+
if (socket === undefined)
|
|
312
|
+
return { known: false };
|
|
313
|
+
let generation;
|
|
314
|
+
try {
|
|
315
|
+
const stat = statSync(socket);
|
|
316
|
+
generation = `${stat.dev}:${stat.ino}`;
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return { known: false };
|
|
320
|
+
}
|
|
321
|
+
const tabs = await runner.run("herdr", ["tab", "list"], { timeoutMs });
|
|
322
|
+
if (tabs.timedOut || tabs.exitCode !== 0)
|
|
323
|
+
return { known: false };
|
|
324
|
+
const present = herdrTabIds(tabs.stdout);
|
|
325
|
+
if (present === undefined)
|
|
326
|
+
return { known: false };
|
|
327
|
+
return { known: true, generation, present };
|
|
328
|
+
}
|
|
329
|
+
export function herdrTabIds(output) {
|
|
330
|
+
const entries = herdrCollection(output, "tabs");
|
|
331
|
+
if (entries === undefined)
|
|
332
|
+
return undefined;
|
|
333
|
+
const ids = new Set();
|
|
334
|
+
for (const entry of entries) {
|
|
335
|
+
if (typeof entry !== "object" || entry === null)
|
|
336
|
+
continue;
|
|
337
|
+
const id = entry.tab_id;
|
|
338
|
+
if (typeof id === "string" && id !== "")
|
|
339
|
+
ids.add(id);
|
|
340
|
+
}
|
|
341
|
+
return ids;
|
|
342
|
+
}
|
|
343
|
+
export async function listCmuxAttachments(runner, timeoutMs) {
|
|
344
|
+
const panels = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
345
|
+
if (panels.timedOut || panels.exitCode !== 0)
|
|
346
|
+
return { known: false };
|
|
347
|
+
const present = cmuxSurfaceIds(panels.stdout);
|
|
348
|
+
if (present === undefined)
|
|
349
|
+
return { known: false };
|
|
350
|
+
return { known: true, generation: "cmux", present };
|
|
351
|
+
}
|
|
352
|
+
export function cmuxSurfaceIds(output) {
|
|
353
|
+
let parsed;
|
|
354
|
+
try {
|
|
355
|
+
parsed = JSON.parse(output);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return undefined;
|
|
359
|
+
}
|
|
360
|
+
const panels = findNamedObject(parsed, "result")?.panels ?? findNamedObject(parsed, "panels");
|
|
361
|
+
const list = Array.isArray(panels) ? panels : Array.isArray(parsed) ? parsed : undefined;
|
|
362
|
+
if (list === undefined)
|
|
363
|
+
return undefined;
|
|
364
|
+
const ids = new Set();
|
|
365
|
+
for (const entry of list) {
|
|
366
|
+
if (typeof entry === "string" && entry !== "")
|
|
367
|
+
ids.add(entry);
|
|
368
|
+
else if (typeof entry === "object" && entry !== null) {
|
|
369
|
+
const id = entry.surface_id;
|
|
370
|
+
if (typeof id === "string" && id !== "")
|
|
371
|
+
ids.add(id);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return ids;
|
|
375
|
+
}
|
|
376
|
+
export function rebindHerdrTab(options) {
|
|
377
|
+
const runner = options.runner ?? systemHostCommandRunner;
|
|
378
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
379
|
+
return hostAttachment({
|
|
380
|
+
identity: { host: "herdr", attachmentId: options.tabId, createdBy: options.ownerId },
|
|
381
|
+
launch: { stdout: "", stderr: "", exitCode: 0, timedOut: false },
|
|
382
|
+
noun: "tab",
|
|
383
|
+
reportsFocus: true,
|
|
384
|
+
observe: async () => {
|
|
385
|
+
const tabs = await runner.run("herdr", ["tab", "list"], { timeoutMs });
|
|
386
|
+
if (tabs.timedOut || tabs.exitCode !== 0)
|
|
387
|
+
return { process: tabs };
|
|
388
|
+
const ids = herdrTabIds(tabs.stdout);
|
|
389
|
+
if (ids === undefined)
|
|
390
|
+
return { process: tabs };
|
|
391
|
+
return { process: tabs, alive: ids.has(options.tabId) };
|
|
392
|
+
},
|
|
393
|
+
close: () => runner.run("herdr", ["tab", "close", options.tabId], { timeoutMs }),
|
|
394
|
+
ownsAttachment: options.ownsAttachment,
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
export function rebindCmuxSurface(options) {
|
|
398
|
+
const runner = options.runner ?? systemHostCommandRunner;
|
|
399
|
+
const timeoutMs = options.timeoutMs ?? 10_000;
|
|
400
|
+
return hostAttachment({
|
|
401
|
+
identity: { host: "cmux", attachmentId: options.surfaceId, createdBy: options.ownerId },
|
|
402
|
+
launch: { stdout: "", stderr: "", exitCode: 0, timedOut: false },
|
|
403
|
+
noun: "surface",
|
|
404
|
+
reportsFocus: false,
|
|
405
|
+
observe: async () => {
|
|
406
|
+
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
407
|
+
if (process.timedOut || process.exitCode !== 0)
|
|
408
|
+
return { process };
|
|
409
|
+
const ids = cmuxSurfaceIds(process.stdout);
|
|
410
|
+
if (ids === undefined)
|
|
411
|
+
return { process };
|
|
412
|
+
return { process, alive: ids.has(options.surfaceId) };
|
|
413
|
+
},
|
|
414
|
+
close: () => runner.run("cmux", ["--json", "close-surface", "--surface", options.surfaceId], {
|
|
415
|
+
timeoutMs,
|
|
416
|
+
}),
|
|
417
|
+
ownsAttachment: options.ownsAttachment,
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
function herdrCollection(output, collection) {
|
|
421
|
+
let parsed;
|
|
422
|
+
try {
|
|
423
|
+
parsed = JSON.parse(output);
|
|
424
|
+
}
|
|
425
|
+
catch {
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
const list = findNamedObject(parsed, "result")?.[collection];
|
|
429
|
+
return Array.isArray(list) ? list : undefined;
|
|
430
|
+
}
|
|
431
|
+
function statusField(output, name) {
|
|
432
|
+
for (const line of output.split("\n")) {
|
|
433
|
+
const separator = line.indexOf(":");
|
|
434
|
+
if (separator < 0)
|
|
435
|
+
continue;
|
|
436
|
+
if (line.slice(0, separator).trim() !== name)
|
|
437
|
+
continue;
|
|
438
|
+
const value = line.slice(separator + 1).trim();
|
|
439
|
+
if (value !== "")
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
return undefined;
|
|
443
|
+
}
|
|
305
444
|
function herdrListEntry(output, collection, key, id) {
|
|
306
445
|
let parsed;
|
|
307
446
|
try {
|
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,80 @@
|
|
|
1
|
+
import { stripVTControlCharacters } from "node:util";
|
|
2
|
+
import { Text } from "@earendil-works/pi-tui";
|
|
3
|
+
import { readMessageView } from "@hheei/pi-ext-core";
|
|
4
|
+
import { Type } from "typebox";
|
|
5
|
+
import { Value } from "typebox/value";
|
|
6
|
+
import { VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE } from "./domain.js";
|
|
7
|
+
import { formatSubagentId } from "./status.js";
|
|
8
|
+
export const SUBAGENT_INFO_TYPE = "subagent-info";
|
|
9
|
+
const infoSchema = Type.Object({
|
|
10
|
+
id: Type.String(),
|
|
11
|
+
agent: Type.String(),
|
|
12
|
+
status: Type.Union([
|
|
13
|
+
Type.Literal("running"),
|
|
14
|
+
Type.Literal("done"),
|
|
15
|
+
Type.Literal("blocked"),
|
|
16
|
+
Type.Literal("error"),
|
|
17
|
+
]),
|
|
18
|
+
message: Type.String(),
|
|
19
|
+
});
|
|
20
|
+
/** Keep line breaks, expand tabs, and remove terminal control characters from child text. */
|
|
21
|
+
export function displaySubagentText(text) {
|
|
22
|
+
return stripVTControlCharacters(text)
|
|
23
|
+
.replaceAll("\t", " ")
|
|
24
|
+
.replace(/[\u0000-\u0008\u000b-\u001f\u007f-\u009f]/gu, "");
|
|
25
|
+
}
|
|
26
|
+
function shownSubagent(data) {
|
|
27
|
+
if (!Value.Check(infoSchema, data))
|
|
28
|
+
return undefined;
|
|
29
|
+
return {
|
|
30
|
+
id: formatSubagentId(displaySubagentText(data.id).replace(/\s+/gu, " ")),
|
|
31
|
+
agent: displaySubagentText(data.agent).replace(/\s+/gu, " "),
|
|
32
|
+
status: data.status,
|
|
33
|
+
message: displaySubagentText(data.message),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
export function renderSubagentInfo(data, expanded, theme) {
|
|
37
|
+
const shown = shownSubagent(data);
|
|
38
|
+
if (shown === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
const heading = theme.fg("dim", "info · agents · ") +
|
|
41
|
+
theme.fg(VISUAL_SUBAGENT_TONE[shown.status], VISUAL_SUBAGENT_GLYPH[shown.status]) +
|
|
42
|
+
theme.fg("dim", ` ${shown.id} ${shown.agent} · ${shown.status}`);
|
|
43
|
+
return expanded ? `${heading}\n${shown.message}` : heading;
|
|
44
|
+
}
|
|
45
|
+
/** Native transcript entries survive resume and never enter the model context. */
|
|
46
|
+
export function registerSubagentInfo(pi) {
|
|
47
|
+
pi.registerEntryRenderer(SUBAGENT_INFO_TYPE, (entry, options, theme) => {
|
|
48
|
+
const create = readMessageView(pi);
|
|
49
|
+
const shown = create === undefined ? undefined : shownSubagent(entry.data);
|
|
50
|
+
if (create !== undefined && shown !== undefined) {
|
|
51
|
+
const message = shown.message.trim();
|
|
52
|
+
return create({
|
|
53
|
+
theme,
|
|
54
|
+
header: {
|
|
55
|
+
kind: "agents",
|
|
56
|
+
summary: `${VISUAL_SUBAGENT_GLYPH[shown.status]} ${shown.id} ${shown.agent} · ${shown.status}`,
|
|
57
|
+
tone: VISUAL_SUBAGENT_TONE[shown.status],
|
|
58
|
+
},
|
|
59
|
+
...(message === "" ? {} : { body: shown.message }),
|
|
60
|
+
stateHolder: entry,
|
|
61
|
+
forcedExpanded: options.expanded,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
const text = renderSubagentInfo(entry.data, options.expanded, theme);
|
|
65
|
+
return text === undefined ? undefined : new Text(text, 1, 0);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
export function appendSubagentInfo(pi, report) {
|
|
69
|
+
const status = report.reason === "success"
|
|
70
|
+
? "done"
|
|
71
|
+
: report.reason === "blocked" || report.reason === "error"
|
|
72
|
+
? report.reason
|
|
73
|
+
: report.status;
|
|
74
|
+
pi.appendEntry(SUBAGENT_INFO_TYPE, {
|
|
75
|
+
id: report.childId,
|
|
76
|
+
agent: report.agent,
|
|
77
|
+
status,
|
|
78
|
+
message: report.message,
|
|
79
|
+
});
|
|
80
|
+
}
|
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;
|