@hheei/pi-subagents 0.1.2 → 0.2.2
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 +42 -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 -52
- 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
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The child Pi process a parent Pi spawns directly.
|
|
3
|
+
*
|
|
4
|
+
* A background child has no terminal and no supervisor, so the parent that started it is also what
|
|
5
|
+
* ends it: the process is held open through stdin (never written to) and its output is drained
|
|
6
|
+
* rather than parsed, because the bridge is the control plane and stdio is plumbing.
|
|
7
|
+
*/
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
import { errorMessage } from "@hheei/pi-ext-core";
|
|
10
|
+
export const DEFAULT_TERMINATE_GRACE_MS = 2_000;
|
|
11
|
+
const DEFAULT_KILL_GRACE_MS = 1_000;
|
|
12
|
+
function hasExited(child) {
|
|
13
|
+
return child.exitCode !== null || child.signalCode !== null;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Live handles are held for the lifetime of this process. A background child is held open by the
|
|
17
|
+
* write end of its stdin, so dropping the handle early (an extension reload, a garbage collection)
|
|
18
|
+
* would send it an EOF and end a child that only its parent's exit is supposed to end.
|
|
19
|
+
*/
|
|
20
|
+
const retained = new Set();
|
|
21
|
+
/**
|
|
22
|
+
* Starts a detached-from-the-terminal child Pi. The caller keeps the handle: this package has no
|
|
23
|
+
* supervisor process, so nothing else can end a background child after its parent is gone.
|
|
24
|
+
*/
|
|
25
|
+
export function spawnChildRuntime(options) {
|
|
26
|
+
const diagnose = options.diagnose ?? (() => { });
|
|
27
|
+
const child = spawn(options.command, [...options.args], {
|
|
28
|
+
cwd: options.cwd,
|
|
29
|
+
env: options.env,
|
|
30
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
31
|
+
});
|
|
32
|
+
// Node reports a failed spawn asynchronously, so the listener is attached before anything else:
|
|
33
|
+
// a command that never started must not surface as an unhandled 'error' event.
|
|
34
|
+
const exited = new Promise((resolve) => {
|
|
35
|
+
child.once("error", (error) => {
|
|
36
|
+
diagnose(`child process error: ${errorMessage(error)}`);
|
|
37
|
+
resolve(null);
|
|
38
|
+
});
|
|
39
|
+
child.once("exit", (code, signal) => {
|
|
40
|
+
retained.delete(child);
|
|
41
|
+
if (signal !== null)
|
|
42
|
+
diagnose(`child process exited on ${signal}`);
|
|
43
|
+
resolve(code);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
if (child.pid === undefined) {
|
|
47
|
+
child.kill("SIGKILL");
|
|
48
|
+
throw new Error(`Child process ${options.command} did not start`);
|
|
49
|
+
}
|
|
50
|
+
retained.add(child);
|
|
51
|
+
// stdout is the child's Pi protocol stream and stderr its diagnostics; neither is a control
|
|
52
|
+
// channel, so they are drained to keep the pipes from filling and reported only as diagnostics.
|
|
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,
|
|
62
|
+
get alive() {
|
|
63
|
+
return !hasExited(child);
|
|
64
|
+
},
|
|
65
|
+
exited,
|
|
66
|
+
async terminate(graceMs = DEFAULT_TERMINATE_GRACE_MS) {
|
|
67
|
+
if (hasExited(child))
|
|
68
|
+
return;
|
|
69
|
+
child.kill("SIGTERM");
|
|
70
|
+
if (await settled(exited, graceMs))
|
|
71
|
+
return;
|
|
72
|
+
diagnose(`child process ${child.pid} ignored SIGTERM; sending SIGKILL`);
|
|
73
|
+
child.kill("SIGKILL");
|
|
74
|
+
if (!(await settled(exited, DEFAULT_KILL_GRACE_MS))) {
|
|
75
|
+
diagnose(`child process ${child.pid} did not exit after SIGKILL`);
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
/** True when `work` settled within the window; the child process outlives this wait either way. */
|
|
81
|
+
function settled(work, timeoutMs) {
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
const timer = setTimeout(() => resolve(false), timeoutMs);
|
|
84
|
+
timer.unref?.();
|
|
85
|
+
void work.then(() => {
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
resolve(true);
|
|
88
|
+
}, () => {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
resolve(true);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
}
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
2
|
+
import { type PublicSubagent } from "./domain.js";
|
|
3
3
|
import type { SubagentManager } from "./manager.js";
|
|
4
4
|
export declare function formatStatusLine(children: readonly PublicSubagent[]): string | undefined;
|
|
5
5
|
export declare function registerParentCommands(pi: ExtensionAPI, manager: SubagentManager): void;
|
|
6
|
-
export declare function bindParentStatus(_pi: ExtensionAPI, context: ExtensionContext, manager: SubagentManager): () => void;
|
|
6
|
+
export declare function bindParentStatus(_pi: ExtensionAPI, context: ExtensionContext, manager: SubagentManager, signal?: AbortSignal): () => void;
|
package/dist/commands.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { splitSubcommand, subcommandCompletions } from "@hheei/pi-ext-core";
|
|
2
|
+
import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH } from "./domain.js";
|
|
1
3
|
const STATUS_KEY = "pi-subagents";
|
|
4
|
+
/** Verbs of the dispatcher below; the completer and the no-argument picker share them. */
|
|
5
|
+
const SUBAGENT_SUBCOMMANDS = ["list", "inspect", "send", "stop"];
|
|
6
|
+
const SUBAGENT_COMMAND_USAGE = "Usage: /subagents list|inspect|send|stop [id]";
|
|
2
7
|
export function formatStatusLine(children) {
|
|
3
|
-
const visible = children.filter((child) => child.state !== "done" && child.state !== "
|
|
8
|
+
const visible = children.filter((child) => child.state !== "done" && child.state !== "error");
|
|
4
9
|
if (visible.length === 0)
|
|
5
10
|
return undefined;
|
|
6
11
|
return visible
|
|
7
12
|
.map((child) => {
|
|
13
|
+
const visual = toVisualSubagentState(child.state, child.interrupted);
|
|
14
|
+
const glyph = VISUAL_SUBAGENT_GLYPH[visual];
|
|
8
15
|
const name = child.displayName ?? child.agent;
|
|
9
|
-
const mode = child.
|
|
10
|
-
|
|
11
|
-
return `${name} ${mode}${flag}`;
|
|
16
|
+
const mode = child.presentation === "panel" ? `${visual} panel` : visual;
|
|
17
|
+
return `${glyph} ${name} ${mode}`;
|
|
12
18
|
})
|
|
13
19
|
.join(" · ");
|
|
14
20
|
}
|
|
@@ -20,8 +26,10 @@ function notifyResult(ctx, result, ok) {
|
|
|
20
26
|
ctx.ui.notify(ok);
|
|
21
27
|
}
|
|
22
28
|
function childLabel(child) {
|
|
29
|
+
const visual = toVisualSubagentState(child.state, child.interrupted);
|
|
30
|
+
const glyph = VISUAL_SUBAGENT_GLYPH[visual];
|
|
23
31
|
const name = child.displayName ?? child.agent;
|
|
24
|
-
return `${
|
|
32
|
+
return `${glyph} ${name} #${child.id} ${visual}/${child.presentation}`;
|
|
25
33
|
}
|
|
26
34
|
async function pickChild(ctx, children, title) {
|
|
27
35
|
if (children.length === 0) {
|
|
@@ -36,23 +44,12 @@ async function pickChild(ctx, children, title) {
|
|
|
36
44
|
return undefined;
|
|
37
45
|
return children[labels.indexOf(selected)];
|
|
38
46
|
}
|
|
39
|
-
async function attachSelected(ctx, manager, id) {
|
|
40
|
-
if (id !== undefined && id !== "") {
|
|
41
|
-
notifyResult(ctx, await manager.attach(id), `Attached ${id}`);
|
|
42
|
-
return;
|
|
43
|
-
}
|
|
44
|
-
const idle = (await manager.list()).filter((child) => child.mode === "rpc" && child.state === "idle");
|
|
45
|
-
const child = await pickChild(ctx, idle, "Attach idle subagent");
|
|
46
|
-
if (child === undefined)
|
|
47
|
-
return;
|
|
48
|
-
notifyResult(ctx, await manager.attach(child.id), `Attached ${child.id}`);
|
|
49
|
-
}
|
|
50
47
|
async function stopSelected(ctx, manager, id) {
|
|
51
48
|
if (id !== undefined && id !== "") {
|
|
52
49
|
notifyResult(ctx, await manager.stop(id), `Stopped ${id}`);
|
|
53
50
|
return;
|
|
54
51
|
}
|
|
55
|
-
const live = (await manager.list()).filter((child) => child.state !== "done" && child.state !== "
|
|
52
|
+
const live = (await manager.list()).filter((child) => child.state !== "done" && child.state !== "error");
|
|
56
53
|
const child = await pickChild(ctx, live, "Stop subagent");
|
|
57
54
|
if (child === undefined)
|
|
58
55
|
return;
|
|
@@ -60,14 +57,16 @@ async function stopSelected(ctx, manager, id) {
|
|
|
60
57
|
}
|
|
61
58
|
export function registerParentCommands(pi, manager) {
|
|
62
59
|
pi.registerCommand("subagents", {
|
|
63
|
-
description: "
|
|
60
|
+
description: "Manage subagents: /subagents [list | inspect | send | stop [id]]",
|
|
61
|
+
getArgumentCompletions: subcommandCompletions(SUBAGENT_SUBCOMMANDS),
|
|
64
62
|
handler: async (args, ctx) => {
|
|
65
63
|
if (ctx.mode !== "tui") {
|
|
66
|
-
ctx.ui.notify("Use
|
|
64
|
+
ctx.ui.notify("Use spawn_agent / send_agent / get_agent / stop_agent in this mode");
|
|
67
65
|
return;
|
|
68
66
|
}
|
|
69
|
-
const
|
|
70
|
-
|
|
67
|
+
const { verb, rest: target } = splitSubcommand(args);
|
|
68
|
+
// Only `attach` and `stop` take a subagent id; the picker verbs ignore the rest.
|
|
69
|
+
const action = verb === "" ? await ctx.ui.select("Subagents", [...SUBAGENT_SUBCOMMANDS]) : verb;
|
|
71
70
|
if (action === undefined)
|
|
72
71
|
return;
|
|
73
72
|
const children = await manager.list();
|
|
@@ -91,16 +90,12 @@ export function registerParentCommands(pi, manager) {
|
|
|
91
90
|
ctx.ui.notify(`${childLabel(inspected)}\n${inspected.summary ?? "no summary"}\nmodel ${inspected.model.provider}/${inspected.model.id}`);
|
|
92
91
|
return;
|
|
93
92
|
}
|
|
94
|
-
if (action === "attach") {
|
|
95
|
-
await attachSelected(ctx, manager);
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
98
93
|
if (action === "stop") {
|
|
99
|
-
await stopSelected(ctx, manager);
|
|
94
|
+
await stopSelected(ctx, manager, target);
|
|
100
95
|
return;
|
|
101
96
|
}
|
|
102
97
|
if (action === "send") {
|
|
103
|
-
const live = children.filter((child) => child.
|
|
98
|
+
const live = children.filter((child) => child.state === "running");
|
|
104
99
|
const child = await pickChild(ctx, live, "Send to subagent");
|
|
105
100
|
if (child === undefined)
|
|
106
101
|
return;
|
|
@@ -108,27 +103,9 @@ export function registerParentCommands(pi, manager) {
|
|
|
108
103
|
if (message === undefined || message.trim() === "")
|
|
109
104
|
return;
|
|
110
105
|
notifyResult(ctx, await manager.send(child.id, message.trim()), `Sent to ${child.id}`);
|
|
111
|
-
}
|
|
112
|
-
},
|
|
113
|
-
});
|
|
114
|
-
pi.registerCommand("attach-subagent", {
|
|
115
|
-
description: "Open an RPC child as a native Pi TUI. Idle children switch immediately; busy children wait for the current turn to finish. The session must already be flushed.",
|
|
116
|
-
handler: async (args, ctx) => {
|
|
117
|
-
await attachSelected(ctx, manager, args.trim());
|
|
118
|
-
},
|
|
119
|
-
});
|
|
120
|
-
pi.registerCommand("stop-subagent", {
|
|
121
|
-
description: "Stop an owned subagent",
|
|
122
|
-
handler: async (args, ctx) => {
|
|
123
|
-
await stopSelected(ctx, manager, args.trim());
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
pi.registerShortcut("ctrl+shift+a", {
|
|
127
|
-
description: "Attach idle subagent",
|
|
128
|
-
handler: (ctx) => {
|
|
129
|
-
if (ctx.mode !== "tui")
|
|
130
106
|
return;
|
|
131
|
-
|
|
107
|
+
}
|
|
108
|
+
ctx.ui.notify(`Unknown subcommand "${action}". ${SUBAGENT_COMMAND_USAGE}`, "warning");
|
|
132
109
|
},
|
|
133
110
|
});
|
|
134
111
|
pi.registerShortcut("ctrl+shift+s", {
|
|
@@ -140,16 +117,39 @@ export function registerParentCommands(pi, manager) {
|
|
|
140
117
|
},
|
|
141
118
|
});
|
|
142
119
|
}
|
|
143
|
-
export function bindParentStatus(_pi, context, manager) {
|
|
120
|
+
export function bindParentStatus(_pi, context, manager, signal) {
|
|
121
|
+
let disposed = false;
|
|
144
122
|
const refresh = () => {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
123
|
+
if (disposed || signal?.aborted)
|
|
124
|
+
return;
|
|
125
|
+
void manager
|
|
126
|
+
.list()
|
|
127
|
+
.then((children) => {
|
|
128
|
+
if (disposed || signal?.aborted)
|
|
129
|
+
return;
|
|
130
|
+
try {
|
|
131
|
+
context.ui.setStatus(STATUS_KEY, formatStatusLine(children));
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
// Context may be stale after session replacement or reload.
|
|
135
|
+
}
|
|
136
|
+
})
|
|
137
|
+
.catch(() => { });
|
|
148
138
|
};
|
|
149
139
|
const unsubscribe = manager.onChange(refresh);
|
|
150
140
|
refresh();
|
|
151
141
|
return () => {
|
|
142
|
+
if (disposed)
|
|
143
|
+
return;
|
|
144
|
+
disposed = true;
|
|
152
145
|
unsubscribe();
|
|
153
|
-
|
|
146
|
+
if (signal?.aborted)
|
|
147
|
+
return;
|
|
148
|
+
try {
|
|
149
|
+
context.ui.setStatus(STATUS_KEY, undefined);
|
|
150
|
+
}
|
|
151
|
+
catch {
|
|
152
|
+
// Context may be stale after session replacement or reload.
|
|
153
|
+
}
|
|
154
154
|
};
|
|
155
155
|
}
|
package/dist/diagnostics.d.ts
CHANGED
package/dist/diagnostics.js
CHANGED
|
@@ -10,11 +10,3 @@ export function writeDiagnostic(scope, message) {
|
|
|
10
10
|
export function toError(value) {
|
|
11
11
|
return value instanceof Error ? value : new Error(String(value));
|
|
12
12
|
}
|
|
13
|
-
export function errorMessage(value) {
|
|
14
|
-
return value instanceof Error ? value.message : String(value);
|
|
15
|
-
}
|
|
16
|
-
export function abortError() {
|
|
17
|
-
const error = new Error("Operation aborted");
|
|
18
|
-
error.name = "AbortError";
|
|
19
|
-
return error;
|
|
20
|
-
}
|
package/dist/domain.d.ts
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
export declare const PROTOCOL_VERSION: 1;
|
|
2
2
|
export declare const REGISTRY_VERSION: 1;
|
|
3
|
-
export type SubagentState = "
|
|
4
|
-
export
|
|
3
|
+
export type SubagentState = "running" | "done" | "blocked" | "error";
|
|
4
|
+
export declare function normalizeSubagentState(value: unknown): SubagentState | undefined;
|
|
5
|
+
/**
|
|
6
|
+
* Where a child is presented: `panel` is the native Pi TUI in a host panel (herdr tab / cmux
|
|
7
|
+
* surface), `background` is a headless Pi the parent owns over stdio. It is frozen when the child
|
|
8
|
+
* is created, because it decides who holds the process for the rest of the child's life.
|
|
9
|
+
*/
|
|
10
|
+
export type Presentation = "panel" | "background";
|
|
5
11
|
export type SendMode = "steer" | "follow_up" | "auto";
|
|
12
|
+
export type VisualSubagentState = SubagentState;
|
|
13
|
+
export declare function toVisualSubagentState(state: SubagentState, interrupted?: string): VisualSubagentState;
|
|
14
|
+
export declare const VISUAL_SUBAGENT_GLYPH: Record<VisualSubagentState, string>;
|
|
15
|
+
export declare const VISUAL_SUBAGENT_TONE: Record<VisualSubagentState, "accent" | "success" | "warning" | "error">;
|
|
16
|
+
export declare const SUBAGENT_GLYPH: Record<SubagentState, string>;
|
|
17
|
+
export declare const SUBAGENT_TONE: Record<SubagentState, "accent" | "success" | "warning" | "error">;
|
|
6
18
|
export type ThinkingLevel = "off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
7
19
|
export type LaunchValueSource = "agent" | "parent";
|
|
8
20
|
export type SubagentIntent = "active" | "stopped";
|
|
@@ -48,14 +60,13 @@ export interface ResolvedThinking {
|
|
|
48
60
|
readonly level: ThinkingLevel;
|
|
49
61
|
readonly source: LaunchValueSource;
|
|
50
62
|
}
|
|
51
|
-
export interface
|
|
52
|
-
readonly discovery: boolean;
|
|
53
|
-
readonly paths: readonly string[];
|
|
54
|
-
}
|
|
55
|
-
export interface SkillSelection {
|
|
63
|
+
export interface Selection {
|
|
56
64
|
readonly discovery: boolean;
|
|
57
65
|
readonly paths: readonly string[];
|
|
58
66
|
}
|
|
67
|
+
/** Extensions and skills are selected the same way; the aliases keep the two roles named. */
|
|
68
|
+
export type ExtensionSelection = Selection;
|
|
69
|
+
export type SkillSelection = Selection;
|
|
59
70
|
export interface EffectiveLaunchConfig {
|
|
60
71
|
/** Logical child id; the same value the child bridge reports back. */
|
|
61
72
|
readonly subagentId: string;
|
|
@@ -66,6 +77,8 @@ export interface EffectiveLaunchConfig {
|
|
|
66
77
|
readonly sessionDir: string;
|
|
67
78
|
/** Known only once the session has been flushed to disk. */
|
|
68
79
|
readonly sessionPath?: string;
|
|
80
|
+
/** Optional path to a prior session JSONL file referenced for on-demand context inspection. */
|
|
81
|
+
readonly referencedSessionPath?: string;
|
|
69
82
|
readonly agent: ResolvedAgentIdentity;
|
|
70
83
|
readonly model: ResolvedModel;
|
|
71
84
|
readonly thinking: ResolvedThinking;
|
|
@@ -77,6 +90,13 @@ export interface EffectiveLaunchConfig {
|
|
|
77
90
|
readonly bridgeExtensionPath: string;
|
|
78
91
|
/** Frozen at spawn. Interactive children do not auto-wake the parent except via contact_parent. */
|
|
79
92
|
readonly interactive: boolean;
|
|
93
|
+
/** Whether the subagent operates in codemode-only mode. */
|
|
94
|
+
readonly codemodeOnly?: boolean;
|
|
95
|
+
/**
|
|
96
|
+
* Session title asked for at spawn, frozen so a relaunched child re-applies the same name.
|
|
97
|
+
* Presentation only: it never changes the child's identity or its host attachment label.
|
|
98
|
+
*/
|
|
99
|
+
readonly title?: string;
|
|
80
100
|
}
|
|
81
101
|
export interface ResolvedAgentPolicy {
|
|
82
102
|
readonly agent: ResolvedAgentIdentity;
|
|
@@ -87,24 +107,16 @@ export interface ResolvedAgentPolicy {
|
|
|
87
107
|
readonly extensions: ExtensionSelection;
|
|
88
108
|
readonly skills: SkillSelection;
|
|
89
109
|
readonly interactive: boolean;
|
|
110
|
+
readonly codemodeOnly?: boolean;
|
|
111
|
+
readonly enabled?: boolean;
|
|
90
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* The runtime attachment a child was last launched with. It is evidence, never a liveness proof:
|
|
115
|
+
* whether that process is still serving this child is answered by the bridge connection.
|
|
116
|
+
*/
|
|
91
117
|
export interface RuntimeMetadata {
|
|
92
118
|
readonly runtimeIdentity: string;
|
|
93
119
|
readonly endpoint: string;
|
|
94
|
-
readonly pid?: number;
|
|
95
|
-
}
|
|
96
|
-
export type RuntimeClaimKind = "reconnect" | "replacement";
|
|
97
|
-
/** Non-secret, durable ownership claim used to serialize reconnect and replacement. */
|
|
98
|
-
export interface RuntimeClaim {
|
|
99
|
-
readonly claimId: string;
|
|
100
|
-
readonly kind: RuntimeClaimKind;
|
|
101
|
-
readonly holderPid: number;
|
|
102
|
-
readonly runtimeIdentity: string;
|
|
103
|
-
readonly endpoint: string;
|
|
104
|
-
/** SHA-256 of the one-time controller token; the token itself is never persisted. */
|
|
105
|
-
readonly controllerTokenHash: string;
|
|
106
|
-
/** Set by the runner before it starts the Pi writer. */
|
|
107
|
-
readonly runnerPid?: number;
|
|
108
120
|
}
|
|
109
121
|
export interface UsageSummary {
|
|
110
122
|
readonly inputTokens: number;
|
|
@@ -126,12 +138,12 @@ export interface SubagentRecord {
|
|
|
126
138
|
readonly initialTask: string;
|
|
127
139
|
readonly intent: SubagentIntent;
|
|
128
140
|
readonly state: SubagentState;
|
|
129
|
-
readonly
|
|
141
|
+
readonly presentation: Presentation;
|
|
130
142
|
readonly persistence: PersistenceState;
|
|
131
143
|
readonly launchConfig: EffectiveLaunchConfig;
|
|
132
144
|
readonly runtime?: RuntimeMetadata;
|
|
133
|
-
readonly claim?: RuntimeClaim;
|
|
134
145
|
readonly latestSummary?: string;
|
|
146
|
+
readonly activeTool?: string;
|
|
135
147
|
readonly usage?: UsageSummary;
|
|
136
148
|
readonly interrupted?: string;
|
|
137
149
|
readonly unacknowledgedInput?: string;
|
|
@@ -141,10 +153,11 @@ export interface PublicSubagent {
|
|
|
141
153
|
readonly agent: string;
|
|
142
154
|
readonly displayName?: string;
|
|
143
155
|
readonly state: SubagentState;
|
|
144
|
-
readonly
|
|
156
|
+
readonly presentation: Presentation;
|
|
145
157
|
readonly cwd: string;
|
|
146
158
|
readonly sessionId: string;
|
|
147
159
|
readonly summary?: string;
|
|
160
|
+
readonly activeTool?: string;
|
|
148
161
|
readonly usage?: UsageSummary;
|
|
149
162
|
readonly interrupted?: string;
|
|
150
163
|
readonly freshness: "live" | "last_known";
|
|
@@ -166,7 +179,23 @@ export interface SpawnSubagentInput {
|
|
|
166
179
|
readonly task: string;
|
|
167
180
|
readonly agent: string;
|
|
168
181
|
readonly cwd?: string;
|
|
169
|
-
|
|
182
|
+
/**
|
|
183
|
+
* Where to run this child. `auto` (the default) means a panel whenever this parent has a
|
|
184
|
+
* presentation host, and the background otherwise; the Task tool pins `background` because a
|
|
185
|
+
* Task is settled by its parent and never handed to a human. This is not a model-facing
|
|
186
|
+
* parameter: `spawn_agent` always spawns with `auto`.
|
|
187
|
+
*/
|
|
188
|
+
readonly presentation?: Presentation | "auto";
|
|
189
|
+
/** Optional child session title; the child branch prefixes it with the subagent marker. */
|
|
190
|
+
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
|
+
}
|
|
197
|
+
/** True when a manager call returned a structured failure instead of a value. */
|
|
198
|
+
export declare function isOperationError(value: unknown): value is OperationError;
|
|
170
199
|
/** Child bridge tool name; agent tool policy must never remove it. */
|
|
171
200
|
export declare const CONTACT_PARENT_TOOL_NAME: "contact_parent";
|
|
172
201
|
/** Environment contract between a parent launch and the child branch of this extension. */
|
|
@@ -179,14 +208,15 @@ export declare const BRIDGE_ENVIRONMENT_KEYS: {
|
|
|
179
208
|
};
|
|
180
209
|
/** Non-secret agent label for the child TUI identity line. Not part of handshake. */
|
|
181
210
|
export declare const CHILD_AGENT_ENV_KEY: "PI_SUBAGENTS_AGENT";
|
|
211
|
+
/** Session title requested at spawn; empty means the child derives one. Not part of handshake. */
|
|
212
|
+
export declare const CHILD_TITLE_ENV_KEY: "PI_SUBAGENTS_TITLE";
|
|
182
213
|
/** Bound Pi session id for the child branch. Not part of the runner handshake. */
|
|
183
214
|
export declare const CHILD_SESSION_ENV_KEY: "PI_SUBAGENTS_SESSION_ID";
|
|
184
215
|
/** Pi session ids reach argv and file names, so they must stay in Pi's accepted alphabet. */
|
|
185
216
|
export declare function isSessionId(value: unknown): value is string;
|
|
186
217
|
export declare function isThinkingLevel(value: unknown): value is ThinkingLevel;
|
|
187
218
|
export declare function isSubagentState(value: unknown): value is SubagentState;
|
|
188
|
-
export declare function
|
|
219
|
+
export declare function isPresentation(value: unknown): value is Presentation;
|
|
189
220
|
export declare function isSubagentIntent(value: unknown): value is SubagentIntent;
|
|
190
221
|
export declare function isPersistenceState(value: unknown): value is PersistenceState;
|
|
191
222
|
export declare function isLaunchValueSource(value: unknown): value is LaunchValueSource;
|
|
192
|
-
export declare function isRecord(value: unknown): value is Record<string, unknown>;
|
package/dist/domain.js
CHANGED
|
@@ -1,5 +1,56 @@
|
|
|
1
|
+
import { isRecord } from "@hheei/pi-ext-core";
|
|
1
2
|
export const PROTOCOL_VERSION = 1;
|
|
2
3
|
export const REGISTRY_VERSION = 1;
|
|
4
|
+
export function normalizeSubagentState(value) {
|
|
5
|
+
if (typeof value !== "string")
|
|
6
|
+
return undefined;
|
|
7
|
+
switch (value) {
|
|
8
|
+
case "running":
|
|
9
|
+
case "starting":
|
|
10
|
+
case "idle":
|
|
11
|
+
return "running";
|
|
12
|
+
case "done":
|
|
13
|
+
return "done";
|
|
14
|
+
case "blocked":
|
|
15
|
+
return "blocked";
|
|
16
|
+
case "error":
|
|
17
|
+
case "failed":
|
|
18
|
+
case "stopped":
|
|
19
|
+
return "error";
|
|
20
|
+
default:
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export function toVisualSubagentState(state, interrupted) {
|
|
25
|
+
if (state === "error")
|
|
26
|
+
return "error";
|
|
27
|
+
if (state === "blocked")
|
|
28
|
+
return "blocked";
|
|
29
|
+
if (interrupted !== undefined && state !== "done")
|
|
30
|
+
return "blocked";
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
export const VISUAL_SUBAGENT_GLYPH = {
|
|
34
|
+
running: "",
|
|
35
|
+
done: "",
|
|
36
|
+
blocked: "",
|
|
37
|
+
error: "",
|
|
38
|
+
};
|
|
39
|
+
export const VISUAL_SUBAGENT_TONE = {
|
|
40
|
+
running: "accent",
|
|
41
|
+
done: "success",
|
|
42
|
+
blocked: "warning",
|
|
43
|
+
error: "error",
|
|
44
|
+
};
|
|
45
|
+
export const SUBAGENT_GLYPH = VISUAL_SUBAGENT_GLYPH;
|
|
46
|
+
export const SUBAGENT_TONE = VISUAL_SUBAGENT_TONE;
|
|
47
|
+
/** True when a manager call returned a structured failure instead of a value. */
|
|
48
|
+
export function isOperationError(value) {
|
|
49
|
+
return (isRecord(value) &&
|
|
50
|
+
typeof value.operation === "string" &&
|
|
51
|
+
typeof value.reason === "string" &&
|
|
52
|
+
Array.isArray(value.sideEffects));
|
|
53
|
+
}
|
|
3
54
|
/** Child bridge tool name; agent tool policy must never remove it. */
|
|
4
55
|
export const CONTACT_PARENT_TOOL_NAME = "contact_parent";
|
|
5
56
|
/** Environment contract between a parent launch and the child branch of this extension. */
|
|
@@ -12,6 +63,8 @@ export const BRIDGE_ENVIRONMENT_KEYS = {
|
|
|
12
63
|
};
|
|
13
64
|
/** Non-secret agent label for the child TUI identity line. Not part of handshake. */
|
|
14
65
|
export const CHILD_AGENT_ENV_KEY = "PI_SUBAGENTS_AGENT";
|
|
66
|
+
/** Session title requested at spawn; empty means the child derives one. Not part of handshake. */
|
|
67
|
+
export const CHILD_TITLE_ENV_KEY = "PI_SUBAGENTS_TITLE";
|
|
15
68
|
/** Bound Pi session id for the child branch. Not part of the runner handshake. */
|
|
16
69
|
export const CHILD_SESSION_ENV_KEY = "PI_SUBAGENTS_SESSION_ID";
|
|
17
70
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
|
|
@@ -32,19 +85,18 @@ export function isThinkingLevel(value) {
|
|
|
32
85
|
return typeof value === "string" && THINKING_LEVELS[value] === true;
|
|
33
86
|
}
|
|
34
87
|
const SUBAGENT_STATES = {
|
|
35
|
-
starting: true,
|
|
36
88
|
running: true,
|
|
37
|
-
idle: true,
|
|
38
89
|
done: true,
|
|
39
|
-
|
|
40
|
-
|
|
90
|
+
blocked: true,
|
|
91
|
+
error: true,
|
|
41
92
|
};
|
|
42
93
|
export function isSubagentState(value) {
|
|
43
|
-
return typeof value === "string" &&
|
|
94
|
+
return (typeof value === "string" &&
|
|
95
|
+
(SUBAGENT_STATES[value] === true || normalizeSubagentState(value) !== undefined));
|
|
44
96
|
}
|
|
45
|
-
const
|
|
46
|
-
export function
|
|
47
|
-
return typeof value === "string" &&
|
|
97
|
+
const PRESENTATIONS = { panel: true, background: true };
|
|
98
|
+
export function isPresentation(value) {
|
|
99
|
+
return typeof value === "string" && PRESENTATIONS[value] === true;
|
|
48
100
|
}
|
|
49
101
|
const SUBAGENT_INTENTS = { active: true, stopped: true };
|
|
50
102
|
export function isSubagentIntent(value) {
|
|
@@ -58,6 +110,3 @@ const LAUNCH_VALUE_SOURCES = { agent: true, parent: true };
|
|
|
58
110
|
export function isLaunchValueSource(value) {
|
|
59
111
|
return typeof value === "string" && LAUNCH_VALUE_SOURCES[value] === true;
|
|
60
112
|
}
|
|
61
|
-
export function isRecord(value) {
|
|
62
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63
|
-
}
|
package/dist/extension.d.ts
CHANGED
|
@@ -1,2 +1,27 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, Skill } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type BackgroundDelivery } from "@hheei/pi-ext-core";
|
|
3
|
+
import type { SkillCatalogEntry } from "./agent-resolver.js";
|
|
4
|
+
import { type ParentChannel } from "./manager.js";
|
|
5
|
+
/** Parent channel plus the teardown its owner must call. */
|
|
6
|
+
export interface ParentChannelHandle extends ParentChannel {
|
|
7
|
+
/** Unregisters delivery and appends anything still held without waking the parent. */
|
|
8
|
+
dispose(): void;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* How child reports reach the parent conversation.
|
|
12
|
+
*
|
|
13
|
+
* Busy parents receive a steer into the next model step. Idle parents wait until all registered
|
|
14
|
+
* background work finishes; decision requests and blockers bypass that wait. The shared gate
|
|
15
|
+
* coordinates this channel with Task/Bash results so only one message requests a new idle turn.
|
|
16
|
+
*/
|
|
17
|
+
export declare function createParentChannel(pi: Pick<ExtensionAPI, "sendMessage" | "on">, deps: {
|
|
18
|
+
readonly isIdle: () => boolean;
|
|
19
|
+
readonly delivery: BackgroundDelivery;
|
|
20
|
+
readonly signal?: AbortSignal;
|
|
21
|
+
}): ParentChannelHandle;
|
|
22
|
+
/**
|
|
23
|
+
* Pi publishes the skills it loaded with every prompt; a definition names them the same way, so
|
|
24
|
+
* the launch path needs exactly the name and the file path from that list.
|
|
25
|
+
*/
|
|
26
|
+
export declare function skillCatalogFromLoaded(skills: readonly Skill[]): readonly SkillCatalogEntry[];
|
|
2
27
|
export default function piSubagentsExtension(pi: ExtensionAPI): void;
|