@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/child-control.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { errorMessage, isRecord } from "@hheei/pi-ext-core";
|
|
2
|
-
import {
|
|
3
|
-
import { BridgeError } from "./protocol.js";
|
|
2
|
+
import { attachChildMailbox } from "./mailbox.js";
|
|
3
|
+
import { BridgeError, CHILD_INPUT_EVENT } from "./protocol.js";
|
|
4
4
|
import { FORWARDED_PI_EVENT_TYPES, shouldForwardPiEvent } from "./rpc-events.js";
|
|
5
|
+
import { assistantUpdatePhase, transcriptFacts } from "./state.js";
|
|
5
6
|
const CHILD_INPUT_SOURCES = ["interactive", "extension", "rpc"];
|
|
6
7
|
/** The source of a Pi input event, when it is one the parent is allowed to hear about. */
|
|
7
8
|
export function childInputSource(event) {
|
|
@@ -22,47 +23,150 @@ export function promptMessage(payload) {
|
|
|
22
23
|
* Answers a parent request with this child's Pi state. Idleness is the child's own answer: the
|
|
23
24
|
* parent never infers it from how long ago it last heard something.
|
|
24
25
|
*/
|
|
25
|
-
export function childState(ctx) {
|
|
26
|
-
const
|
|
26
|
+
export function childState(ctx, isCompacting = false) {
|
|
27
|
+
const path = ctx.sessionManager.getSessionFile();
|
|
27
28
|
return {
|
|
28
29
|
idle: ctx.isIdle(),
|
|
29
30
|
pendingMessages: ctx.hasPendingMessages(),
|
|
31
|
+
...(isCompacting ? { compacting: true } : {}),
|
|
30
32
|
sessionId: ctx.sessionManager.getSessionId(),
|
|
31
|
-
...(
|
|
33
|
+
...(path ? { path } : {}),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
|
|
37
|
+
* The dedup key for a streaming update: its phase plus the tool it names. Two deltas with the same
|
|
38
|
+
* key are the same telemetry, so only the first is forwarded while the parent is listening.
|
|
39
|
+
*/
|
|
40
|
+
function assistantPhaseKey(event) {
|
|
41
|
+
const phase = assistantUpdatePhase(event.assistantMessageEvent, event.message);
|
|
42
|
+
if (phase === undefined)
|
|
43
|
+
return undefined;
|
|
44
|
+
return phase.toolName === undefined ? phase.kind : `${phase.kind}:${phase.toolName}`;
|
|
45
|
+
}
|
|
46
|
+
/** Text kept from a message part: enough for the parent's summary, never the whole payload. */
|
|
47
|
+
const BRIDGE_MESSAGE_TEXT_LIMIT = 200;
|
|
48
|
+
/** Text kept from a tool result part: the parent shows tool activity, not tool output. */
|
|
49
|
+
const BRIDGE_TOOL_TEXT_LIMIT = 1000;
|
|
50
|
+
/** Content parts kept per message, so a frame stays bounded no matter how long the stream runs. */
|
|
51
|
+
const BRIDGE_MAX_CONTENT_PARTS = 32;
|
|
52
|
+
/**
|
|
53
|
+
* Reduces one content part to what the parent's projector reads. Text is truncated, image data is
|
|
54
|
+
* replaced by a marker, and a tool call keeps its name but not its arguments: the parent projects
|
|
55
|
+
* turn-level activity, so it never needs the payload that produced it.
|
|
56
|
+
*/
|
|
57
|
+
function compactContentPart(part, textLimit) {
|
|
58
|
+
if (!isRecord(part))
|
|
59
|
+
return part;
|
|
60
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
61
|
+
return part.text.length > textLimit
|
|
62
|
+
? { ...part, text: `${part.text.slice(0, textLimit)}\n... [truncated for bridge event]` }
|
|
63
|
+
: part;
|
|
64
|
+
}
|
|
65
|
+
if (part.type === "image") {
|
|
66
|
+
return {
|
|
67
|
+
type: "image",
|
|
68
|
+
mimeType: part.mimeType,
|
|
69
|
+
data: "[image omitted for bridge event]",
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
if (part.type === "toolCall") {
|
|
73
|
+
return {
|
|
74
|
+
type: "toolCall",
|
|
75
|
+
...(typeof part.id === "string" ? { id: part.id } : {}),
|
|
76
|
+
...(typeof part.name === "string" ? { name: part.name } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
return typeof part.type === "string" ? { type: part.type } : {};
|
|
80
|
+
}
|
|
81
|
+
function compactContent(content, textLimit) {
|
|
82
|
+
if (!Array.isArray(content))
|
|
83
|
+
return undefined;
|
|
84
|
+
return content
|
|
85
|
+
.slice(0, BRIDGE_MAX_CONTENT_PARTS)
|
|
86
|
+
.map((part) => compactContentPart(part, textLimit));
|
|
87
|
+
}
|
|
88
|
+
/** Keeps only the turn-level message fields the projector reads, dropping the rest. */
|
|
89
|
+
function compactMessage(message, textLimit) {
|
|
90
|
+
const value = isRecord(message) ? message : undefined;
|
|
91
|
+
if (value === undefined)
|
|
92
|
+
return undefined;
|
|
93
|
+
const content = compactContent(value.content, textLimit);
|
|
94
|
+
return {
|
|
95
|
+
...(typeof value.role === "string" ? { role: value.role } : {}),
|
|
96
|
+
...(typeof value.stopReason === "string" ? { stopReason: value.stopReason } : {}),
|
|
97
|
+
...(typeof value.errorMessage === "string" ? { errorMessage: value.errorMessage } : {}),
|
|
98
|
+
...(content === undefined ? {} : { content }),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Compacts an advisory event to the telemetry the parent projects before it is serialized and
|
|
103
|
+
* pushed over the bridge. A streaming update carries the whole partial message — text, tool
|
|
104
|
+
* arguments, the accumulating `partial` — which is what the parent neither reads nor needs; only
|
|
105
|
+
* the phase metadata (type, content index) and the tool name survive, so a frame stays far below
|
|
106
|
+
* the protocol limit under any payload size.
|
|
38
107
|
*/
|
|
39
108
|
export function sanitizeEventForBridge(event) {
|
|
40
109
|
if (!isRecord(event))
|
|
41
110
|
return event;
|
|
111
|
+
if (event.type === "message_update") {
|
|
112
|
+
const update = isRecord(event.assistantMessageEvent) ? event.assistantMessageEvent : undefined;
|
|
113
|
+
const message = compactMessage(event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
|
|
114
|
+
return {
|
|
115
|
+
type: "message_update",
|
|
116
|
+
...(message === undefined ? {} : { message }),
|
|
117
|
+
...(update === undefined
|
|
118
|
+
? {}
|
|
119
|
+
: {
|
|
120
|
+
assistantMessageEvent: {
|
|
121
|
+
...(typeof update.type === "string" ? { type: update.type } : {}),
|
|
122
|
+
...(typeof update.contentIndex === "number"
|
|
123
|
+
? { contentIndex: update.contentIndex }
|
|
124
|
+
: {}),
|
|
125
|
+
},
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
if (event.type === "tool_execution_start") {
|
|
130
|
+
return {
|
|
131
|
+
type: "tool_execution_start",
|
|
132
|
+
...(typeof event.toolName === "string" ? { toolName: event.toolName } : {}),
|
|
133
|
+
...(typeof event.toolCallId === "string" ? { toolCallId: event.toolCallId } : {}),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
42
136
|
if (event.type === "tool_execution_end" && isRecord(event.result)) {
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
137
|
+
const content = compactContent(event.result.content, BRIDGE_TOOL_TEXT_LIMIT);
|
|
138
|
+
return {
|
|
139
|
+
...event,
|
|
140
|
+
result: {
|
|
141
|
+
...(content === undefined ? {} : { content }),
|
|
142
|
+
...(typeof event.result.isError === "boolean" ? { isError: event.result.isError } : {}),
|
|
143
|
+
},
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
if (event.type === "turn_end") {
|
|
147
|
+
const message = compactMessage(event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
|
|
148
|
+
return {
|
|
149
|
+
type: "turn_end",
|
|
150
|
+
...(typeof event.turnIndex === "number" ? { turnIndex: event.turnIndex } : {}),
|
|
151
|
+
...(message === undefined ? {} : { message }),
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
if (event.type === "agent_end" || event.type === "agent_settled") {
|
|
155
|
+
const messages = Array.isArray(event.messages) ? event.messages : undefined;
|
|
156
|
+
const last = messages?.[messages.length - 1];
|
|
157
|
+
const message = compactMessage(last ?? event.message, BRIDGE_MESSAGE_TEXT_LIMIT);
|
|
158
|
+
return {
|
|
159
|
+
type: event.type,
|
|
160
|
+
...(event.type === "agent_settled" ? { aborted: event.aborted } : {}),
|
|
161
|
+
...(message === undefined ? {} : { message, messages: [message] }),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
if (event.type === "compaction_start" || event.type === "compaction_end") {
|
|
165
|
+
return {
|
|
166
|
+
type: event.type,
|
|
167
|
+
...(typeof event.reason === "string" ? { reason: event.reason } : {}),
|
|
168
|
+
...(typeof event.aborted === "boolean" ? { aborted: event.aborted } : {}),
|
|
169
|
+
};
|
|
66
170
|
}
|
|
67
171
|
return event;
|
|
68
172
|
}
|
|
@@ -73,6 +177,21 @@ export function sanitizeEventForBridge(event) {
|
|
|
73
177
|
export function registerChildControl(pi, options) {
|
|
74
178
|
const isActive = options.isActive ?? (() => true);
|
|
75
179
|
let ctx;
|
|
180
|
+
let isCompacting = false;
|
|
181
|
+
const pendingCompactionDeliveries = [];
|
|
182
|
+
const flushCompactionQueue = () => {
|
|
183
|
+
if (pendingCompactionDeliveries.length === 0)
|
|
184
|
+
return;
|
|
185
|
+
const queued = pendingCompactionDeliveries.splice(0);
|
|
186
|
+
for (const item of queued) {
|
|
187
|
+
if (item.deliverAs !== undefined) {
|
|
188
|
+
pi.sendUserMessage(item.message, { deliverAs: item.deliverAs });
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
pi.sendUserMessage(item.message);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
};
|
|
76
195
|
const requireContext = () => {
|
|
77
196
|
if (ctx === undefined) {
|
|
78
197
|
throw new BridgeError("session_not_ready", "This child has no active Pi session yet");
|
|
@@ -83,12 +202,40 @@ export function registerChildControl(pi, options) {
|
|
|
83
202
|
const message = promptMessage(payload);
|
|
84
203
|
// A message needs a session to land in: while the process is between sessions the parent
|
|
85
204
|
// hears that instead of a silent drop.
|
|
86
|
-
requireContext();
|
|
205
|
+
const path = requireContext().sessionManager.getSessionFile();
|
|
206
|
+
if (path === undefined) {
|
|
207
|
+
throw new BridgeError("session_not_ready", "This child has no session file path");
|
|
208
|
+
}
|
|
209
|
+
if (isCompacting) {
|
|
210
|
+
pendingCompactionDeliveries.push({ message, deliverAs });
|
|
211
|
+
return { accepted: true, path, queuedForCompaction: true };
|
|
212
|
+
}
|
|
87
213
|
if (deliverAs !== undefined)
|
|
88
214
|
pi.sendUserMessage(message, { deliverAs });
|
|
89
215
|
else
|
|
90
216
|
pi.sendUserMessage(message);
|
|
91
|
-
return { accepted: true };
|
|
217
|
+
return { accepted: true, path };
|
|
218
|
+
};
|
|
219
|
+
const currentEntries = () => {
|
|
220
|
+
const sm = requireContext().sessionManager;
|
|
221
|
+
const branch = typeof sm.getBranch === "function" ? sm.getBranch() : undefined;
|
|
222
|
+
return Array.isArray(branch) && branch.length > 0 ? branch : sm.getEntries();
|
|
223
|
+
};
|
|
224
|
+
let cachedFacts;
|
|
225
|
+
const sessionFacts = (expected) => {
|
|
226
|
+
const sm = requireContext().sessionManager;
|
|
227
|
+
const leaf = typeof sm.getLeafId === "function" ? sm.getLeafId() : undefined;
|
|
228
|
+
// Append-only: a new message, compaction, or branch moves the leaf. The same leaf is the
|
|
229
|
+
// same transcript, so turn_end and agent_end do not each copy and walk it.
|
|
230
|
+
if (typeof leaf === "string" && leaf.length > 0) {
|
|
231
|
+
const key = `${leaf}:${expected ?? ""}`;
|
|
232
|
+
if (cachedFacts?.key === key)
|
|
233
|
+
return cachedFacts.facts;
|
|
234
|
+
const facts = transcriptFacts(currentEntries(), expected);
|
|
235
|
+
cachedFacts = { key, facts };
|
|
236
|
+
return facts;
|
|
237
|
+
}
|
|
238
|
+
return transcriptFacts(currentEntries(), expected);
|
|
92
239
|
};
|
|
93
240
|
const handleRequest = async (request) => {
|
|
94
241
|
if (!isActive()) {
|
|
@@ -104,9 +251,14 @@ export function registerChildControl(pi, options) {
|
|
|
104
251
|
case "follow_up":
|
|
105
252
|
return deliverPrompt(request.payload, "followUp");
|
|
106
253
|
case "get_state":
|
|
107
|
-
return childState(requireContext());
|
|
108
|
-
case "get_entries":
|
|
109
|
-
return { entries:
|
|
254
|
+
return childState(requireContext(), isCompacting);
|
|
255
|
+
case "get_entries": {
|
|
256
|
+
return { entries: currentEntries() };
|
|
257
|
+
}
|
|
258
|
+
case "session_facts": {
|
|
259
|
+
const expected = isRecord(request.payload) ? request.payload.expectedInput : undefined;
|
|
260
|
+
return sessionFacts(typeof expected === "string" ? expected : undefined);
|
|
261
|
+
}
|
|
110
262
|
case "abort":
|
|
111
263
|
requireContext().abort();
|
|
112
264
|
return { accepted: true };
|
|
@@ -117,36 +269,88 @@ export function registerChildControl(pi, options) {
|
|
|
117
269
|
throw new BridgeError("unsupported_operation", `This child does not implement ${request.operation}`);
|
|
118
270
|
}
|
|
119
271
|
};
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
272
|
+
const dispose = [];
|
|
273
|
+
let currentPhase;
|
|
274
|
+
/** The compact event carrying the current phase, resent when the parent reconnects. */
|
|
275
|
+
const phaseResetEvents = [
|
|
276
|
+
"agent_start",
|
|
277
|
+
"agent_end",
|
|
278
|
+
"turn_start",
|
|
279
|
+
"turn_end",
|
|
280
|
+
"agent_settled",
|
|
281
|
+
"tool_execution_start",
|
|
282
|
+
"tool_execution_end",
|
|
283
|
+
"compaction_start",
|
|
284
|
+
"compaction_end",
|
|
285
|
+
];
|
|
286
|
+
const client = attachChildMailbox({
|
|
287
|
+
filePath: options.endpoint,
|
|
288
|
+
parentSessionId: options.identity.parentSessionId,
|
|
289
|
+
subagentId: options.identity.subagentId,
|
|
123
290
|
handleRequest,
|
|
124
291
|
...(options.diagnose === undefined ? {} : { diagnose: options.diagnose }),
|
|
125
292
|
});
|
|
126
|
-
const dispose = [];
|
|
127
293
|
// `pi.on` is declared as literal-name overloads, so a loop over the forwarded event types cannot
|
|
128
294
|
// be typed through them; every name in FORWARDED_PI_EVENT_TYPES is a real Pi event.
|
|
129
295
|
const onPiEvent = pi.on;
|
|
130
296
|
for (const type of FORWARDED_PI_EVENT_TYPES) {
|
|
131
297
|
dispose.push(onPiEvent(type, (event) => {
|
|
132
|
-
if (!isActive())
|
|
298
|
+
if (!isActive() || !client.connected)
|
|
133
299
|
return;
|
|
134
|
-
if (shouldForwardPiEvent(event))
|
|
135
|
-
|
|
300
|
+
if (!shouldForwardPiEvent(event))
|
|
301
|
+
return;
|
|
302
|
+
const compact = sanitizeEventForBridge(event);
|
|
303
|
+
if (isRecord(event)) {
|
|
304
|
+
if (typeof event.type === "string" && phaseResetEvents.includes(event.type)) {
|
|
305
|
+
currentPhase = undefined;
|
|
306
|
+
}
|
|
307
|
+
else if (event.type === "message_update") {
|
|
308
|
+
const phase = assistantPhaseKey(event);
|
|
309
|
+
if (phase !== undefined) {
|
|
310
|
+
// While the parent is listening, repetitive deltas in the same phase are dropped to
|
|
311
|
+
// prevent socket floods. A disconnected parent suppresses nothing: the phase is
|
|
312
|
+
// resent from `onConnect` instead, so a dropped delta cannot hide it.
|
|
313
|
+
if (phase === currentPhase && client.connected)
|
|
314
|
+
return;
|
|
315
|
+
currentPhase = phase;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
client.sendEvent(compact);
|
|
136
320
|
}));
|
|
137
321
|
}
|
|
138
322
|
dispose.push(pi.on("input", (event) => {
|
|
139
|
-
if (!isActive())
|
|
323
|
+
if (!isActive() || !client.connected)
|
|
140
324
|
return;
|
|
141
325
|
const source = childInputSource(event);
|
|
142
|
-
if (source !== undefined)
|
|
143
|
-
client.
|
|
326
|
+
if (source !== undefined) {
|
|
327
|
+
client.sendEvent({
|
|
328
|
+
type: CHILD_INPUT_EVENT,
|
|
329
|
+
parentSessionId: options.identity.parentSessionId,
|
|
330
|
+
childId: options.identity.subagentId,
|
|
331
|
+
runtimeIdentity: options.identity.runtimeIdentity,
|
|
332
|
+
source,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
144
335
|
}));
|
|
145
336
|
dispose.push(pi.on("session_start", (_event, context) => {
|
|
146
337
|
ctx = context;
|
|
147
338
|
}));
|
|
339
|
+
dispose.push(onPiEvent("compaction_start", () => {
|
|
340
|
+
isCompacting = true;
|
|
341
|
+
}));
|
|
342
|
+
dispose.push(onPiEvent("compaction_end", () => {
|
|
343
|
+
isCompacting = false;
|
|
344
|
+
queueMicrotask(() => {
|
|
345
|
+
if (!isCompacting) {
|
|
346
|
+
flushCompactionQueue();
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}));
|
|
148
350
|
dispose.push(pi.on("session_shutdown", () => {
|
|
149
351
|
ctx = undefined;
|
|
352
|
+
isCompacting = false;
|
|
353
|
+
pendingCompactionDeliveries.length = 0;
|
|
150
354
|
}));
|
|
151
355
|
let started = false;
|
|
152
356
|
return {
|
|
@@ -158,7 +362,7 @@ export function registerChildControl(pi, options) {
|
|
|
158
362
|
started = true;
|
|
159
363
|
client.start();
|
|
160
364
|
},
|
|
161
|
-
report: (operation, payload, signal) => client.
|
|
365
|
+
report: (operation, payload, signal) => client.report(operation, payload, signal),
|
|
162
366
|
sendEvent: (event) => client.sendEvent(event),
|
|
163
367
|
dispose: () => {
|
|
164
368
|
for (const off of dispose) {
|
package/dist/child-process.d.ts
CHANGED
|
@@ -19,3 +19,5 @@ export interface ChildRuntime {
|
|
|
19
19
|
* supervisor process, so nothing else can end a background child after its parent is gone.
|
|
20
20
|
*/
|
|
21
21
|
export declare function spawnChildRuntime(options: ChildProcessOptions): ChildRuntime;
|
|
22
|
+
/** The process-scoped stdin leash for a background child that survived an extension reload. */
|
|
23
|
+
export declare function attachRetainedChild(pid: number): ChildRuntime | undefined;
|
package/dist/child-process.js
CHANGED
|
@@ -18,6 +18,7 @@ function hasExited(child) {
|
|
|
18
18
|
* would send it an EOF and end a child that only its parent's exit is supposed to end.
|
|
19
19
|
*/
|
|
20
20
|
const retained = new Set();
|
|
21
|
+
const retainedRuntimes = new Map();
|
|
21
22
|
/**
|
|
22
23
|
* Starts a detached-from-the-terminal child Pi. The caller keeps the handle: this package has no
|
|
23
24
|
* supervisor process, so nothing else can end a background child after its parent is gone.
|
|
@@ -43,22 +44,14 @@ export function spawnChildRuntime(options) {
|
|
|
43
44
|
resolve(code);
|
|
44
45
|
});
|
|
45
46
|
});
|
|
46
|
-
|
|
47
|
+
const pid = child.pid;
|
|
48
|
+
if (pid === undefined) {
|
|
47
49
|
child.kill("SIGKILL");
|
|
48
50
|
throw new Error(`Child process ${options.command} did not start`);
|
|
49
51
|
}
|
|
50
52
|
retained.add(child);
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
child.stdout.on("data", () => { });
|
|
54
|
-
child.stderr.on("data", (chunk) => {
|
|
55
|
-
const text = chunk.toString("utf8").trim();
|
|
56
|
-
if (text !== "")
|
|
57
|
-
diagnose(text);
|
|
58
|
-
});
|
|
59
|
-
child.stdin.on("error", () => { });
|
|
60
|
-
return {
|
|
61
|
-
pid: child.pid,
|
|
53
|
+
const runtime = {
|
|
54
|
+
pid,
|
|
62
55
|
get alive() {
|
|
63
56
|
return !hasExited(child);
|
|
64
57
|
},
|
|
@@ -76,6 +69,25 @@ export function spawnChildRuntime(options) {
|
|
|
76
69
|
}
|
|
77
70
|
},
|
|
78
71
|
};
|
|
72
|
+
retainedRuntimes.set(pid, runtime);
|
|
73
|
+
void exited.then(() => {
|
|
74
|
+
retainedRuntimes.delete(pid);
|
|
75
|
+
});
|
|
76
|
+
// stdout is the child's Pi protocol stream and stderr its diagnostics; neither is a control
|
|
77
|
+
// channel, so they are drained to keep the pipes from filling and reported only as diagnostics.
|
|
78
|
+
child.stdout.on("data", () => { });
|
|
79
|
+
child.stderr.on("data", (chunk) => {
|
|
80
|
+
const text = chunk.toString("utf8").trim();
|
|
81
|
+
if (text !== "")
|
|
82
|
+
diagnose(text);
|
|
83
|
+
});
|
|
84
|
+
child.stdin.on("error", () => { });
|
|
85
|
+
return runtime;
|
|
86
|
+
}
|
|
87
|
+
/** The process-scoped stdin leash for a background child that survived an extension reload. */
|
|
88
|
+
export function attachRetainedChild(pid) {
|
|
89
|
+
const runtime = retainedRuntimes.get(pid);
|
|
90
|
+
return runtime?.alive === true ? runtime : undefined;
|
|
79
91
|
}
|
|
80
92
|
/** True when `work` settled within the window; the child process outlives this wait either way. */
|
|
81
93
|
function settled(work, timeoutMs) {
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,3 @@
|
|
|
1
|
-
import type { ExtensionAPI
|
|
2
|
-
import { type PublicSubagent } from "./domain.js";
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
3
2
|
import type { SubagentManager } from "./manager.js";
|
|
4
|
-
export declare function formatStatusLine(children: readonly PublicSubagent[]): string | undefined;
|
|
5
3
|
export declare function registerParentCommands(pi: ExtensionAPI, manager: SubagentManager): void;
|
|
6
|
-
export declare function bindParentStatus(_pi: ExtensionAPI, context: ExtensionContext, manager: SubagentManager, signal?: AbortSignal): () => void;
|
package/dist/commands.js
CHANGED
|
@@ -1,23 +1,8 @@
|
|
|
1
1
|
import { splitSubcommand, subcommandCompletions } from "@hheei/pi-ext-core";
|
|
2
2
|
import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH } from "./domain.js";
|
|
3
|
-
const STATUS_KEY = "pi-subagents";
|
|
4
3
|
/** Verbs of the dispatcher below; the completer and the no-argument picker share them. */
|
|
5
4
|
const SUBAGENT_SUBCOMMANDS = ["list", "inspect", "send", "stop"];
|
|
6
5
|
const SUBAGENT_COMMAND_USAGE = "Usage: /subagents list|inspect|send|stop [id]";
|
|
7
|
-
export function formatStatusLine(children) {
|
|
8
|
-
const visible = children.filter((child) => child.state !== "done" && child.state !== "error");
|
|
9
|
-
if (visible.length === 0)
|
|
10
|
-
return undefined;
|
|
11
|
-
return visible
|
|
12
|
-
.map((child) => {
|
|
13
|
-
const visual = toVisualSubagentState(child.state, child.interrupted);
|
|
14
|
-
const glyph = VISUAL_SUBAGENT_GLYPH[visual];
|
|
15
|
-
const name = child.displayName ?? child.agent;
|
|
16
|
-
const mode = child.presentation === "panel" ? `${visual} panel` : visual;
|
|
17
|
-
return `${glyph} ${name} ${mode}`;
|
|
18
|
-
})
|
|
19
|
-
.join(" · ");
|
|
20
|
-
}
|
|
21
6
|
function notifyResult(ctx, result, ok) {
|
|
22
7
|
if ("reason" in result && typeof result.reason === "string") {
|
|
23
8
|
ctx.ui.notify(result.reason, "error");
|
|
@@ -61,7 +46,7 @@ export function registerParentCommands(pi, manager) {
|
|
|
61
46
|
getArgumentCompletions: subcommandCompletions(SUBAGENT_SUBCOMMANDS),
|
|
62
47
|
handler: async (args, ctx) => {
|
|
63
48
|
if (ctx.mode !== "tui") {
|
|
64
|
-
ctx.ui.notify("Use
|
|
49
|
+
ctx.ui.notify("Use agent_spawn / agent_send / agent_get / agent_stop in this mode");
|
|
65
50
|
return;
|
|
66
51
|
}
|
|
67
52
|
const { verb, rest: target } = splitSubcommand(args);
|
|
@@ -109,39 +94,3 @@ export function registerParentCommands(pi, manager) {
|
|
|
109
94
|
},
|
|
110
95
|
});
|
|
111
96
|
}
|
|
112
|
-
export function bindParentStatus(_pi, context, manager, signal) {
|
|
113
|
-
let disposed = false;
|
|
114
|
-
const refresh = () => {
|
|
115
|
-
if (disposed || signal?.aborted)
|
|
116
|
-
return;
|
|
117
|
-
void manager
|
|
118
|
-
.list()
|
|
119
|
-
.then((children) => {
|
|
120
|
-
if (disposed || signal?.aborted)
|
|
121
|
-
return;
|
|
122
|
-
try {
|
|
123
|
-
context.ui.setStatus(STATUS_KEY, formatStatusLine(children));
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
// Context may be stale after session replacement or reload.
|
|
127
|
-
}
|
|
128
|
-
})
|
|
129
|
-
.catch(() => { });
|
|
130
|
-
};
|
|
131
|
-
const unsubscribe = manager.onChange(refresh);
|
|
132
|
-
refresh();
|
|
133
|
-
return () => {
|
|
134
|
-
if (disposed)
|
|
135
|
-
return;
|
|
136
|
-
disposed = true;
|
|
137
|
-
unsubscribe();
|
|
138
|
-
if (signal?.aborted)
|
|
139
|
-
return;
|
|
140
|
-
try {
|
|
141
|
-
context.ui.setStatus(STATUS_KEY, undefined);
|
|
142
|
-
}
|
|
143
|
-
catch {
|
|
144
|
-
// Context may be stale after session replacement or reload.
|
|
145
|
-
}
|
|
146
|
-
};
|
|
147
|
-
}
|
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
|
}
|
|
@@ -114,9 +116,24 @@ export interface ResolvedAgentPolicy {
|
|
|
114
116
|
* The runtime attachment a child was last launched with. It is evidence, never a liveness proof:
|
|
115
117
|
* whether that process is still serving this child is answered by the bridge connection.
|
|
116
118
|
*/
|
|
119
|
+
export type PanelHost = "herdr" | "cmux";
|
|
120
|
+
/**
|
|
121
|
+
* The surface that owns a child runtime. A panel id is a host tab or surface. A process id is a
|
|
122
|
+
* background child. Neither one is liveness by itself: the host list or a pid check says whether
|
|
123
|
+
* it is still there.
|
|
124
|
+
*/
|
|
125
|
+
export type RuntimeAttachment = {
|
|
126
|
+
readonly kind: "panel";
|
|
127
|
+
readonly host: PanelHost;
|
|
128
|
+
readonly id: string;
|
|
129
|
+
} | {
|
|
130
|
+
readonly kind: "process";
|
|
131
|
+
readonly pid: number;
|
|
132
|
+
};
|
|
117
133
|
export interface RuntimeMetadata {
|
|
118
134
|
readonly runtimeIdentity: string;
|
|
119
135
|
readonly endpoint: string;
|
|
136
|
+
readonly attachment?: RuntimeAttachment;
|
|
120
137
|
}
|
|
121
138
|
export interface UsageSummary {
|
|
122
139
|
readonly inputTokens: number;
|
|
@@ -150,18 +167,20 @@ export interface SubagentRecord {
|
|
|
150
167
|
}
|
|
151
168
|
export interface PublicSubagent {
|
|
152
169
|
readonly id: string;
|
|
170
|
+
/** Pi-assigned session file path; the first write may still be pending. */
|
|
171
|
+
readonly path?: string;
|
|
153
172
|
readonly agent: string;
|
|
154
173
|
readonly displayName?: string;
|
|
155
174
|
readonly state: SubagentState;
|
|
156
175
|
readonly presentation: Presentation;
|
|
157
176
|
readonly cwd: string;
|
|
158
177
|
readonly sessionId: string;
|
|
178
|
+
readonly task?: string;
|
|
159
179
|
readonly summary?: string;
|
|
160
180
|
readonly activeTool?: string;
|
|
161
181
|
readonly usage?: UsageSummary;
|
|
162
182
|
readonly interrupted?: string;
|
|
163
183
|
readonly freshness: "live" | "last_known";
|
|
164
|
-
readonly interactive: boolean;
|
|
165
184
|
readonly model: ResolvedModel;
|
|
166
185
|
readonly thinking: ResolvedThinking;
|
|
167
186
|
readonly createdAt: string;
|
|
@@ -183,21 +202,17 @@ export interface SpawnSubagentInput {
|
|
|
183
202
|
* Where to run this child. `auto` (the default) means a panel whenever this parent has a
|
|
184
203
|
* presentation host, and the background otherwise; the Task tool pins `background` because a
|
|
185
204
|
* Task is settled by its parent and never handed to a human. This is not a model-facing
|
|
186
|
-
* parameter: `
|
|
205
|
+
* parameter: `agent_spawn` always spawns with `auto`.
|
|
187
206
|
*/
|
|
188
207
|
readonly presentation?: Presentation | "auto";
|
|
189
208
|
/** Optional child session title; the child branch prefixes it with the subagent marker. */
|
|
190
209
|
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
210
|
}
|
|
197
211
|
/** True when a manager call returned a structured failure instead of a value. */
|
|
198
212
|
export declare function isOperationError(value: unknown): value is OperationError;
|
|
199
213
|
/** Child bridge tool name; agent tool policy must never remove it. */
|
|
200
214
|
export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
|
|
215
|
+
export declare const DEFAULT_PI_TOOLS: readonly ["read", "bash", "edit", "write"];
|
|
201
216
|
/** Environment contract between a parent launch and the child branch of this extension. */
|
|
202
217
|
export declare const BRIDGE_ENVIRONMENT_KEYS: {
|
|
203
218
|
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",
|