@hheei/pi-subagents 0.1.2 → 0.2.3
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 +45 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -60
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/host-adapter.js
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { promisify } from "node:util";
|
|
3
|
-
const execFileAsync = promisify(execFile);
|
|
1
|
+
import { errorMessage, runCommand, shellQuote } from "@hheei/pi-ext-core";
|
|
4
2
|
const DEFAULT_HOST_ORDER = ["herdr", "cmux"];
|
|
5
3
|
/** Selects a presentation host visibly; an explicit unavailable host never falls back. */
|
|
6
4
|
export async function selectHostAdapter(options) {
|
|
@@ -20,11 +18,9 @@ export async function selectHostAdapter(options) {
|
|
|
20
18
|
available: true,
|
|
21
19
|
selectedHost: host,
|
|
22
20
|
adapter,
|
|
23
|
-
explicit,
|
|
24
21
|
reason: priorFailures === ""
|
|
25
22
|
? `${explicit ? "explicit" : "default"} host ${host} is available`
|
|
26
23
|
: `selected ${host} after ${priorFailures}`,
|
|
27
|
-
attempts,
|
|
28
24
|
};
|
|
29
25
|
}
|
|
30
26
|
}
|
|
@@ -32,36 +28,38 @@ export async function selectHostAdapter(options) {
|
|
|
32
28
|
return {
|
|
33
29
|
available: false,
|
|
34
30
|
selectedHost: null,
|
|
35
|
-
explicit,
|
|
36
31
|
reason: explicit
|
|
37
32
|
? `explicit host unavailable: ${failure}`
|
|
38
33
|
: `no presentation host available: ${failure}`,
|
|
39
|
-
attempts,
|
|
40
34
|
};
|
|
41
35
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
this.kind = kind;
|
|
49
|
-
this.result = result;
|
|
50
|
-
}
|
|
36
|
+
const HOST_COMMAND_MAX_STDOUT_BYTES = 1024 * 1024;
|
|
37
|
+
function errnoCode(error) {
|
|
38
|
+
// Node's spawn errors are plain Errors carrying a `code` string; narrowing here
|
|
39
|
+
// keeps the ENOENT decision below honest about the caught value being unknown.
|
|
40
|
+
const code = error.code;
|
|
41
|
+
return typeof code === "string" ? code : undefined;
|
|
51
42
|
}
|
|
52
43
|
export const systemHostCommandRunner = {
|
|
53
44
|
async run(command, args, options = {}) {
|
|
54
45
|
try {
|
|
55
|
-
const result = await
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
windowsHide: true,
|
|
46
|
+
const result = await runCommand(command, [...args], {
|
|
47
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
48
|
+
maxStdoutBytes: HOST_COMMAND_MAX_STDOUT_BYTES,
|
|
59
49
|
});
|
|
60
|
-
return {
|
|
50
|
+
return {
|
|
51
|
+
stdout: result.stdout.toString("utf8"),
|
|
52
|
+
stderr: result.stdoutTruncated
|
|
53
|
+
? `output exceeded ${HOST_COMMAND_MAX_STDOUT_BYTES} bytes`
|
|
54
|
+
: result.stderr.toString("utf8"),
|
|
55
|
+
// A command killed by a signal reports no exit code; the kill still counts as
|
|
56
|
+
// a failure through `timedOut` or `hostCleanupFailed`.
|
|
57
|
+
exitCode: result.signal === null ? result.code : null,
|
|
58
|
+
timedOut: result.timedOut,
|
|
59
|
+
};
|
|
61
60
|
}
|
|
62
61
|
catch (error) {
|
|
63
|
-
|
|
64
|
-
if (value.code === "ENOENT") {
|
|
62
|
+
if (errnoCode(error) === "ENOENT") {
|
|
65
63
|
return {
|
|
66
64
|
stdout: "",
|
|
67
65
|
stderr: `command not found: ${command}`,
|
|
@@ -70,10 +68,10 @@ export const systemHostCommandRunner = {
|
|
|
70
68
|
};
|
|
71
69
|
}
|
|
72
70
|
return {
|
|
73
|
-
stdout:
|
|
74
|
-
stderr:
|
|
75
|
-
exitCode:
|
|
76
|
-
timedOut:
|
|
71
|
+
stdout: "",
|
|
72
|
+
stderr: errorMessage(error),
|
|
73
|
+
exitCode: null,
|
|
74
|
+
timedOut: false,
|
|
77
75
|
};
|
|
78
76
|
}
|
|
79
77
|
},
|
|
@@ -106,27 +104,47 @@ export function createHerdrHostAdapter(options) {
|
|
|
106
104
|
? { host: "herdr", available: false, reason: "herdr probe returned no current pane id" }
|
|
107
105
|
: { host: "herdr", available: true, reason: `current pane ${pane}` };
|
|
108
106
|
},
|
|
109
|
-
async
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
async open(spec) {
|
|
108
|
+
// A dedicated tab, not a split of the parent pane: the child TUI owns its own tab, the
|
|
109
|
+
// parent session keeps its layout and focus, and the tab is labeled with the child id so
|
|
110
|
+
// a human can tell whose panel it is.
|
|
111
|
+
const workspaceId = await resolveHerdrProcessWorkspaceId(runner, timeoutMs, options.env ?? process.env);
|
|
112
|
+
const created = await runner.run("herdr", herdrTabCreateArgs(spec, workspaceId), {
|
|
113
|
+
timeoutMs,
|
|
114
|
+
});
|
|
115
|
+
const target = parseHerdrTab(created.stdout);
|
|
116
|
+
if (target === undefined) {
|
|
117
|
+
throw new Error("herdr tab create returned no tab and root pane id");
|
|
119
118
|
}
|
|
120
|
-
// pane run sends text+Enter to the
|
|
119
|
+
// pane run sends text+Enter to the tab's root shell. Timeout is not rollback: the tab
|
|
121
120
|
// already exists and the process may still start after the CLI deadline.
|
|
122
|
-
const launch = await runner.run("herdr", ["pane", "run",
|
|
121
|
+
const launch = await runner.run("herdr", ["pane", "run", target.paneId, quotedArgv(spec.command, spec.argv)], { timeoutMs });
|
|
123
122
|
return hostAttachment({
|
|
124
|
-
identity: { host: "herdr", attachmentId:
|
|
123
|
+
identity: { host: "herdr", attachmentId: target.tabId, createdBy: options.ownerId },
|
|
125
124
|
launch,
|
|
126
|
-
noun: "
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
125
|
+
noun: "tab",
|
|
126
|
+
reportsFocus: true,
|
|
127
|
+
observe: async () => {
|
|
128
|
+
// Aliveness is the pane's foreground process; focus is a fact about the tab *and*
|
|
129
|
+
// its workspace, so both listings are read together with it.
|
|
130
|
+
const [process, tabs, workspaces] = await Promise.all([
|
|
131
|
+
runner.run("herdr", ["pane", "process-info", "--pane", target.paneId], { timeoutMs }),
|
|
132
|
+
runner.run("herdr", ["tab", "list"], { timeoutMs }),
|
|
133
|
+
runner.run("herdr", ["workspace", "list"], { timeoutMs }),
|
|
134
|
+
]);
|
|
135
|
+
// A listing that did not answer says nothing about focus: reading it anyway turns a
|
|
136
|
+
// failed command into "unfocused", and the caller closes a panel on that answer.
|
|
137
|
+
const listed = herdrListingSucceeded(tabs) && herdrListingSucceeded(workspaces)
|
|
138
|
+
? herdrPanelFocused(tabs.stdout, workspaces.stdout, target.tabId)
|
|
139
|
+
: undefined;
|
|
140
|
+
const alive = herdrPaneAlive(process);
|
|
141
|
+
return {
|
|
142
|
+
process,
|
|
143
|
+
...(alive === undefined ? {} : { alive }),
|
|
144
|
+
...(listed === undefined ? {} : { focused: listed }),
|
|
145
|
+
};
|
|
146
|
+
},
|
|
147
|
+
close: () => runner.run("herdr", ["tab", "close", target.tabId], { timeoutMs }),
|
|
130
148
|
ownsAttachment: options.ownsAttachment,
|
|
131
149
|
});
|
|
132
150
|
},
|
|
@@ -150,18 +168,26 @@ export function createCmuxHostAdapter(options) {
|
|
|
150
168
|
}
|
|
151
169
|
return { host: "cmux", available: true, reason: "cmux responded and exposed capabilities" };
|
|
152
170
|
},
|
|
153
|
-
async
|
|
154
|
-
const launch = await runner.run("cmux", ["--json", "new-
|
|
171
|
+
async open(spec) {
|
|
172
|
+
const launch = await runner.run("cmux", ["--json", "new-surface", "--command", cmuxLaunchCommand(spec)], { timeoutMs });
|
|
155
173
|
const attachmentId = parseCmuxSurfaceId(launch.stdout);
|
|
156
174
|
if (attachmentId === undefined) {
|
|
157
|
-
throw new
|
|
175
|
+
throw new Error("cmux new-surface returned no surface id; process state is unknown");
|
|
158
176
|
}
|
|
159
177
|
return hostAttachment({
|
|
160
178
|
identity: { host: "cmux", attachmentId, createdBy: options.ownerId },
|
|
161
179
|
launch,
|
|
162
180
|
noun: "surface",
|
|
163
|
-
|
|
164
|
-
|
|
181
|
+
// TODO(cmux-focus): cmux does not report which surface a human is looking at, so
|
|
182
|
+
// observation cannot answer `focused` and idle reclaim fails open here: a cmux child
|
|
183
|
+
// is only ever closed by stop_agent.
|
|
184
|
+
reportsFocus: false,
|
|
185
|
+
observe: async () => {
|
|
186
|
+
const process = await runner.run("cmux", ["--json", "list-panels"], { timeoutMs });
|
|
187
|
+
if (process.timedOut || process.exitCode !== 0)
|
|
188
|
+
return { process };
|
|
189
|
+
return { process, alive: containsString(process.stdout, attachmentId) };
|
|
190
|
+
},
|
|
165
191
|
close: () => runner.run("cmux", ["--json", "close-surface", "--surface", attachmentId], {
|
|
166
192
|
timeoutMs,
|
|
167
193
|
}),
|
|
@@ -173,16 +199,21 @@ export function createCmuxHostAdapter(options) {
|
|
|
173
199
|
function hostAttachment(options) {
|
|
174
200
|
const observe = async () => {
|
|
175
201
|
const result = await options.observe();
|
|
202
|
+
const focused = options.reportsFocus ? result.focused : undefined;
|
|
176
203
|
return {
|
|
177
204
|
identity: options.identity,
|
|
178
|
-
alive:
|
|
179
|
-
|
|
180
|
-
|
|
205
|
+
// A host that could not answer is not reporting a dead child: `alive: false` is what lets
|
|
206
|
+
// the caller drop the panel and the runtime evidence, so it is only ever said on purpose.
|
|
207
|
+
alive: result.alive === true,
|
|
208
|
+
known: result.alive !== undefined,
|
|
209
|
+
...(focused === undefined ? {} : { focused }),
|
|
210
|
+
detail: result.process.stderr || result.process.stdout,
|
|
181
211
|
};
|
|
182
212
|
};
|
|
183
213
|
return {
|
|
184
214
|
identity: options.identity,
|
|
185
215
|
launch: options.launch,
|
|
216
|
+
reportsFocus: options.reportsFocus,
|
|
186
217
|
observe,
|
|
187
218
|
async cleanup() {
|
|
188
219
|
if (!options.ownsAttachment(options.identity)) {
|
|
@@ -199,29 +230,124 @@ function hostAttachment(options) {
|
|
|
199
230
|
},
|
|
200
231
|
};
|
|
201
232
|
}
|
|
202
|
-
|
|
233
|
+
/**
|
|
234
|
+
* Herdr tab creation carries cwd, env, label, and explicit workspace id matching the parent
|
|
235
|
+
* process, ensuring the child tab spawns in the process's own workspace rather than whatever
|
|
236
|
+
* workspace happens to have user focus.
|
|
237
|
+
*/
|
|
238
|
+
export function herdrTabCreateArgs(spec, workspaceId) {
|
|
203
239
|
const args = [
|
|
204
|
-
"
|
|
205
|
-
"
|
|
206
|
-
"--pane",
|
|
207
|
-
parentPane,
|
|
208
|
-
"--direction",
|
|
209
|
-
"right",
|
|
240
|
+
"tab",
|
|
241
|
+
"create",
|
|
210
242
|
"--cwd",
|
|
211
243
|
spec.cwd,
|
|
244
|
+
"--label",
|
|
245
|
+
spec.config.subagentId,
|
|
212
246
|
"--no-focus",
|
|
213
247
|
];
|
|
248
|
+
if (workspaceId !== undefined && workspaceId !== "") {
|
|
249
|
+
args.push("--workspace", workspaceId);
|
|
250
|
+
}
|
|
214
251
|
for (const [key, value] of Object.entries(spec.env)) {
|
|
215
252
|
args.push("--env", `${key}=${value}`);
|
|
216
253
|
}
|
|
217
254
|
return args;
|
|
218
255
|
}
|
|
256
|
+
export function parseHerdrWorkspaceId(output) {
|
|
257
|
+
return parseJsonId(output, ["workspace_id", "workspaceId"]);
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* Resolves the workspace id of the current process so new tabs open in the same workspace.
|
|
261
|
+
* Reads environment variables first, falling back to herdr pane queries.
|
|
262
|
+
*/
|
|
263
|
+
export async function resolveHerdrProcessWorkspaceId(runner, timeoutMs, env = process.env) {
|
|
264
|
+
const envWorkspace = env.HERDR_WORKSPACE_ID?.trim();
|
|
265
|
+
if (envWorkspace)
|
|
266
|
+
return envWorkspace;
|
|
267
|
+
const envPane = env.HERDR_PANE_ID?.trim();
|
|
268
|
+
if (envPane) {
|
|
269
|
+
const res = await runner.run("herdr", ["pane", "get", envPane], { timeoutMs });
|
|
270
|
+
if (res.exitCode === 0) {
|
|
271
|
+
const ws = parseHerdrWorkspaceId(res.stdout);
|
|
272
|
+
if (ws)
|
|
273
|
+
return ws;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
const current = await runner.run("herdr", ["pane", "current", "--current"], { timeoutMs });
|
|
277
|
+
if (current.exitCode === 0) {
|
|
278
|
+
return parseHerdrWorkspaceId(current.stdout);
|
|
279
|
+
}
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Focus is answered from two listings: `tab list` says whether the child's tab is focused and
|
|
284
|
+
* which workspace owns it, `workspace list` says whether that workspace is the rendered one. A
|
|
285
|
+
* child is being watched only when both are focused. Anything missing — a failed listing, an absent
|
|
286
|
+
* field, a tab that is not there — is unknown, never "unfocused", because the caller closes a panel
|
|
287
|
+
* on an unfocused answer.
|
|
288
|
+
*/
|
|
289
|
+
export function herdrPanelFocused(tabList, workspaceList, tabId) {
|
|
290
|
+
const tab = herdrListEntry(tabList, "tabs", "tab_id", tabId);
|
|
291
|
+
if (tab === undefined)
|
|
292
|
+
return undefined;
|
|
293
|
+
const tabFocused = tab.focused;
|
|
294
|
+
if (typeof tabFocused !== "boolean")
|
|
295
|
+
return undefined;
|
|
296
|
+
if (!tabFocused)
|
|
297
|
+
return false;
|
|
298
|
+
const workspaceId = tab.workspace_id;
|
|
299
|
+
if (typeof workspaceId !== "string" || workspaceId === "")
|
|
300
|
+
return undefined;
|
|
301
|
+
const workspace = herdrListEntry(workspaceList, "workspaces", "workspace_id", workspaceId);
|
|
302
|
+
const workspaceFocused = workspace?.focused;
|
|
303
|
+
return typeof workspaceFocused === "boolean" ? workspaceFocused : undefined;
|
|
304
|
+
}
|
|
305
|
+
function herdrListEntry(output, collection, key, id) {
|
|
306
|
+
let parsed;
|
|
307
|
+
try {
|
|
308
|
+
parsed = JSON.parse(output);
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return undefined;
|
|
312
|
+
}
|
|
313
|
+
const list = findNamedObject(parsed, "result")?.[collection];
|
|
314
|
+
if (!Array.isArray(list))
|
|
315
|
+
return undefined;
|
|
316
|
+
for (const entry of list) {
|
|
317
|
+
if (typeof entry !== "object" || entry === null)
|
|
318
|
+
continue;
|
|
319
|
+
const record = entry;
|
|
320
|
+
if (record[key] === id)
|
|
321
|
+
return record;
|
|
322
|
+
}
|
|
323
|
+
return undefined;
|
|
324
|
+
}
|
|
325
|
+
/**
|
|
326
|
+
* `herdr tab create` answers with the tab and its root pane: the pane runs the LaunchSpec and is
|
|
327
|
+
* what observation watches, while the tab is what cleanup closes.
|
|
328
|
+
*/
|
|
329
|
+
function parseHerdrTab(output) {
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = JSON.parse(output);
|
|
333
|
+
}
|
|
334
|
+
catch {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
const tabId = findNamedObject(parsed, "tab")?.tab_id;
|
|
338
|
+
const paneId = findNamedObject(parsed, "root_pane")?.pane_id;
|
|
339
|
+
if (typeof tabId !== "string" || typeof paneId !== "string")
|
|
340
|
+
return undefined;
|
|
341
|
+
return { tabId, paneId };
|
|
342
|
+
}
|
|
219
343
|
/** Herdr `pane run` concatenates extra argv with spaces, so quoting happens here once. */
|
|
220
344
|
function quotedArgv(command, argv) {
|
|
221
345
|
return [command, ...argv].map(shellQuote).join(" ");
|
|
222
346
|
}
|
|
223
347
|
/**
|
|
224
|
-
* cmux `new-
|
|
348
|
+
* cmux `new-surface --command` is shell text. Encode cwd + env + argv without inventing Pi flags.
|
|
349
|
+
* A surface is cmux's own name for a panel: attaching adds one to the current pane instead of
|
|
350
|
+
* splitting the parent's layout, and cleanup closes exactly that surface.
|
|
225
351
|
*/
|
|
226
352
|
function cmuxLaunchCommand(spec) {
|
|
227
353
|
const directory = `cd ${shellQuote(spec.cwd)}`;
|
|
@@ -232,9 +358,6 @@ function cmuxLaunchCommand(spec) {
|
|
|
232
358
|
.filter((part) => part.length > 0)
|
|
233
359
|
.join(" ");
|
|
234
360
|
}
|
|
235
|
-
function shellQuote(value) {
|
|
236
|
-
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
237
|
-
}
|
|
238
361
|
function containsString(output, target) {
|
|
239
362
|
try {
|
|
240
363
|
return valueContainsString(JSON.parse(output), target);
|
|
@@ -284,6 +407,34 @@ function findString(value, keys, accept) {
|
|
|
284
407
|
}
|
|
285
408
|
return undefined;
|
|
286
409
|
}
|
|
410
|
+
/** A listing that failed or timed out is not evidence, whatever its stdout happens to contain. */
|
|
411
|
+
function herdrListingSucceeded(result) {
|
|
412
|
+
return !result.timedOut && result.exitCode === 0;
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Reads the pane's process state. A pane that no longer exists is a definite "not alive" — that is
|
|
416
|
+
* how a closed panel looks — while a query that never ran or failed for any other reason is
|
|
417
|
+
* unknown, because the caller closes the panel on "not alive".
|
|
418
|
+
*/
|
|
419
|
+
export function herdrPaneAlive(result) {
|
|
420
|
+
if (herdrListingSucceeded(result))
|
|
421
|
+
return herdrChildProcessIsRunning(result.stdout);
|
|
422
|
+
// The CLI writes the error as JSON on stderr.
|
|
423
|
+
const code = herdrErrorCode(result.stderr) ?? herdrErrorCode(result.stdout);
|
|
424
|
+
if (code === "pane_not_found" || code === "tab_not_found")
|
|
425
|
+
return false;
|
|
426
|
+
return undefined;
|
|
427
|
+
}
|
|
428
|
+
/** The error code of a herdr CLI failure, which reports it as JSON. */
|
|
429
|
+
function herdrErrorCode(output) {
|
|
430
|
+
try {
|
|
431
|
+
const parsed = JSON.parse(output);
|
|
432
|
+
return typeof parsed.error?.code === "string" ? parsed.error.code : undefined;
|
|
433
|
+
}
|
|
434
|
+
catch {
|
|
435
|
+
return undefined;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
287
438
|
function herdrChildProcessIsRunning(output) {
|
|
288
439
|
try {
|
|
289
440
|
const info = findNamedObject(JSON.parse(output), "process_info");
|
package/dist/json-lines.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { errorMessage } from "@hheei/pi-ext-core";
|
|
1
2
|
export class JsonLineError extends Error {
|
|
2
3
|
code;
|
|
3
4
|
constructor(code, message) {
|
|
@@ -47,7 +48,7 @@ export function attachJsonLineReader(stream, options) {
|
|
|
47
48
|
options.onValue(JSON.parse(frame.toString("utf8")));
|
|
48
49
|
}
|
|
49
50
|
catch (error) {
|
|
50
|
-
fail(new JsonLineError("malformed_json", `Malformed JSON line: ${
|
|
51
|
+
fail(new JsonLineError("malformed_json", `Malformed JSON line: ${errorMessage(error)}`));
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
54
|
};
|
package/dist/launch-spec.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import type { ChildBridgeEnvironment, EffectiveLaunchConfig,
|
|
1
|
+
import type { ChildBridgeEnvironment, EffectiveLaunchConfig, PersistenceState, PiInvocation, Presentation } from "./domain.js";
|
|
2
2
|
/**
|
|
3
|
-
* The one description of how a child Pi process starts.
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* The one description of how a child Pi process starts. A panel child and a background child are
|
|
4
|
+
* the same Pi invocation; only the presentation (and therefore stdio) differs, so no path can
|
|
5
|
+
* silently invent different Pi flags.
|
|
6
6
|
*/
|
|
7
7
|
export interface LaunchSpec {
|
|
8
8
|
/** Runtime binary (node/bun/compiled pi), spawned without a shell. */
|
|
@@ -10,16 +10,18 @@ export interface LaunchSpec {
|
|
|
10
10
|
/** Full argv: host prefix args plus every Pi flag this child needs. */
|
|
11
11
|
readonly argv: readonly string[];
|
|
12
12
|
readonly cwd: string;
|
|
13
|
-
readonly
|
|
13
|
+
readonly presentation: Presentation;
|
|
14
14
|
readonly stdio: "pipe" | "inherit";
|
|
15
15
|
/** Bridge variables to add to the child environment, excluding the controller token. */
|
|
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
|
+
export declare function stripHindsightContent(text: string): string;
|
|
19
21
|
export interface BuildLaunchSpecOptions {
|
|
20
22
|
readonly config: EffectiveLaunchConfig;
|
|
21
23
|
readonly invocation: PiInvocation;
|
|
22
|
-
readonly
|
|
24
|
+
readonly presentation: Presentation;
|
|
23
25
|
readonly persistence: PersistenceState;
|
|
24
26
|
readonly bridge: ChildBridgeEnvironment;
|
|
25
27
|
}
|
package/dist/launch-spec.js
CHANGED
|
@@ -2,23 +2,42 @@ 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, CONTACT_PARENT_TOOL_NAME, isSessionId, } from "./domain.js";
|
|
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
|
+
];
|
|
16
|
+
export function stripHindsightContent(text) {
|
|
17
|
+
return text
|
|
18
|
+
.replace(/\s*<hindsight-recall>[\s\S]*?<\/hindsight-recall>\s*/gi, (match) => match.includes("\n") ? "\n" : " ")
|
|
19
|
+
.trim();
|
|
20
|
+
}
|
|
6
21
|
/**
|
|
7
22
|
* Fixed child-branch preamble. The agent definition's Markdown body supplies the
|
|
8
23
|
* task policy; this part states the parent relationship and the reporting channel
|
|
9
24
|
* every child has, so it cannot be lost by editing an agent file.
|
|
10
25
|
*/
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
26
|
+
function childBridgePrompt() {
|
|
27
|
+
return [
|
|
28
|
+
"You are a delegated Pi subagent working for a parent Pi session.",
|
|
29
|
+
"The parent owns your task, and its instructions remain the only source of new authority.",
|
|
30
|
+
`Use ${CONTACT_PARENT_TOOL_NAME} ONLY if you are blocked or urgently require parent intervention midway.`,
|
|
31
|
+
"The parent is notified automatically when you call that tool. Do not wait for the parent to poll you.",
|
|
32
|
+
"Do not send empty status pings.",
|
|
33
|
+
"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
|
+
"Do NOT call contact_parent on success or task completion.",
|
|
35
|
+
"After blocked, wait for a parent send_agent. Do not invent new authority.",
|
|
36
|
+
"Reports reach the parent as delegated results, not as new user authorization.",
|
|
37
|
+
].join("\n");
|
|
38
|
+
}
|
|
20
39
|
export function assembleChildPrompt(instructions) {
|
|
21
|
-
return `${instructions.trim()}\n\n${
|
|
40
|
+
return `${instructions.trim()}\n\n${childBridgePrompt()}`;
|
|
22
41
|
}
|
|
23
42
|
/** Adds the per-runtime controller token. Tokens stay out of launch spec snapshots and the registry. */
|
|
24
43
|
export function withBridgeToken(env, token) {
|
|
@@ -64,6 +83,15 @@ function resolveBundledPiCli() {
|
|
|
64
83
|
* argv atoms keep their exact bytes.
|
|
65
84
|
*/
|
|
66
85
|
export function resolvePiInvocation() {
|
|
86
|
+
if (process.env.PI_DEV === "1" || process.env.PI_DEV_BIN !== undefined) {
|
|
87
|
+
const devBin = process.env.PI_DEV_BIN;
|
|
88
|
+
if (devBin !== undefined && existsSync(devBin)) {
|
|
89
|
+
return { command: devBin, args: [] };
|
|
90
|
+
}
|
|
91
|
+
if (process.env.PI_DEV === "1") {
|
|
92
|
+
return { command: "pi-dev", args: [] };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
67
95
|
const execPath = process.execPath;
|
|
68
96
|
const currentScript = process.argv[1];
|
|
69
97
|
const isBunVirtualScript = currentScript?.startsWith("/$bunfs/root/") ?? false;
|
|
@@ -107,8 +135,15 @@ export function buildLaunchSpec(options) {
|
|
|
107
135
|
throw new Error("Bridge extension must be part of the effective extension selection");
|
|
108
136
|
}
|
|
109
137
|
const argv = [...options.invocation.args];
|
|
110
|
-
|
|
138
|
+
// Pi's native interactive TUI is the default, so only the headless background child asks for RPC.
|
|
139
|
+
if (options.presentation === "background") {
|
|
111
140
|
argv.push("--mode", "rpc");
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
// A panel child is launched by the parent with an explicit project configuration. Approve
|
|
144
|
+
// those local files so opening its native TUI never stops for a trust prompt.
|
|
145
|
+
argv.push("--approve");
|
|
146
|
+
}
|
|
112
147
|
if (sessionPath !== undefined)
|
|
113
148
|
argv.push("--session", sessionPath);
|
|
114
149
|
else
|
|
@@ -116,10 +151,14 @@ export function buildLaunchSpec(options) {
|
|
|
116
151
|
argv.push("--session-dir", config.sessionDir);
|
|
117
152
|
argv.push("--provider", config.model.provider, "--model", config.model.id);
|
|
118
153
|
argv.push("--thinking", config.thinking.level);
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
const effectiveTools = config.codemodeOnly && !config.tools.includes("codemode")
|
|
155
|
+
? [...config.tools, "codemode"]
|
|
156
|
+
: config.tools;
|
|
157
|
+
if (effectiveTools.length > 0)
|
|
158
|
+
argv.push("--tools", effectiveTools.join(","));
|
|
159
|
+
const excludeTools = Array.from(new Set([...config.excludeTools, ...HINDSIGHT_TOOLS]));
|
|
160
|
+
if (excludeTools.length > 0)
|
|
161
|
+
argv.push("--exclude-tools", excludeTools.join(","));
|
|
123
162
|
if (!config.extensions.discovery)
|
|
124
163
|
argv.push("--no-extensions");
|
|
125
164
|
for (const path of config.extensions.paths)
|
|
@@ -136,13 +175,17 @@ export function buildLaunchSpec(options) {
|
|
|
136
175
|
[BRIDGE_ENVIRONMENT_KEYS.endpoint]: options.bridge.endpoint,
|
|
137
176
|
[CHILD_AGENT_ENV_KEY]: config.agent.displayName ?? config.agent.name,
|
|
138
177
|
[CHILD_SESSION_ENV_KEY]: config.sessionId,
|
|
178
|
+
// Empty is the defined "derive the title in the child" case, mirroring the task channel below.
|
|
179
|
+
[CHILD_TITLE_ENV_KEY]: config.title ?? "",
|
|
180
|
+
PI_HINDSIGHT_DISABLE: "1",
|
|
181
|
+
...(config.codemodeOnly ? { PI_CODEMODE_MODE: "only" } : {}),
|
|
139
182
|
};
|
|
140
183
|
return Object.freeze({
|
|
141
184
|
command: options.invocation.command,
|
|
142
185
|
argv: Object.freeze(argv),
|
|
143
186
|
cwd: config.cwd,
|
|
144
|
-
|
|
145
|
-
stdio: options.
|
|
187
|
+
presentation: options.presentation,
|
|
188
|
+
stdio: options.presentation === "background" ? "pipe" : "inherit",
|
|
146
189
|
env: Object.freeze(env),
|
|
147
190
|
config,
|
|
148
191
|
});
|