@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.
Files changed (65) hide show
  1. package/README.md +45 -137
  2. package/dist/agent-resolver.d.ts +20 -0
  3. package/dist/agent-resolver.js +155 -21
  4. package/dist/bridge-client.d.ts +55 -0
  5. package/dist/bridge-client.js +453 -0
  6. package/dist/bridge-server.d.ts +72 -0
  7. package/dist/bridge-server.js +490 -0
  8. package/dist/builtin-agents.d.ts +24 -0
  9. package/dist/builtin-agents.js +152 -0
  10. package/dist/child-bridge.d.ts +31 -15
  11. package/dist/child-bridge.js +95 -117
  12. package/dist/child-control.d.ts +58 -0
  13. package/dist/child-control.js +176 -0
  14. package/dist/child-process.d.ts +21 -0
  15. package/dist/child-process.js +93 -0
  16. package/dist/commands.d.ts +2 -2
  17. package/dist/commands.js +52 -60
  18. package/dist/diagnostics.d.ts +0 -2
  19. package/dist/diagnostics.js +0 -8
  20. package/dist/domain.d.ts +57 -27
  21. package/dist/domain.js +60 -11
  22. package/dist/extension.d.ts +26 -1
  23. package/dist/extension.js +243 -46
  24. package/dist/host-adapter.d.ts +37 -12
  25. package/dist/host-adapter.js +214 -63
  26. package/dist/json-lines.js +2 -1
  27. package/dist/launch-spec.d.ts +8 -6
  28. package/dist/launch-spec.js +61 -18
  29. package/dist/manager.d.ts +93 -49
  30. package/dist/manager.js +1175 -528
  31. package/dist/markdown.d.ts +8 -0
  32. package/dist/markdown.js +21 -0
  33. package/dist/protocol.d.ts +33 -52
  34. package/dist/protocol.js +41 -52
  35. package/dist/registry.d.ts +3 -8
  36. package/dist/registry.js +41 -163
  37. package/dist/rpc-events.d.ts +3 -5
  38. package/dist/rpc-events.js +5 -9
  39. package/dist/runtime.d.ts +66 -11
  40. package/dist/runtime.js +182 -208
  41. package/dist/session-bootstrap.d.ts +23 -7
  42. package/dist/session-bootstrap.js +79 -15
  43. package/dist/session-fork.d.ts +16 -0
  44. package/dist/session-fork.js +67 -0
  45. package/dist/state.d.ts +7 -0
  46. package/dist/state.js +60 -25
  47. package/dist/tool-activation.d.ts +18 -0
  48. package/dist/tool-activation.js +227 -0
  49. package/dist/tools.d.ts +6 -3
  50. package/dist/tools.js +213 -72
  51. package/dist/widget.d.ts +4 -8
  52. package/dist/widget.js +55 -64
  53. package/package.json +51 -52
  54. package/dist/child-nudge.d.ts +0 -24
  55. package/dist/child-nudge.js +0 -102
  56. package/dist/connector.d.ts +0 -53
  57. package/dist/connector.js +0 -283
  58. package/dist/pause-gate.d.ts +0 -14
  59. package/dist/pause-gate.js +0 -64
  60. package/dist/rpc-adapter.d.ts +0 -40
  61. package/dist/rpc-adapter.js +0 -194
  62. package/dist/runner-entry.d.ts +0 -24
  63. package/dist/runner-entry.js +0 -166
  64. package/dist/runner.d.ts +0 -52
  65. 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
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
- import type { PublicSubagent } from "./domain.js";
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 !== "stopped");
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.mode === "tui" ? "tui" : child.state;
10
- const flag = child.interrupted !== undefined ? " interrupted" : "";
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 `${name} ${child.id} ${child.state}/${child.mode}`;
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 !== "stopped");
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: "List, inspect, attach, send to, or stop an owned subagent",
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 spawn_subagent / send_subagent / get_subagent / stop_subagent in this mode");
64
+ ctx.ui.notify("Use spawn_agent / send_agent / get_agent / stop_agent in this mode");
67
65
  return;
68
66
  }
69
- const action = args.trim() ||
70
- (await ctx.ui.select("Subagents", ["list", "inspect", "attach", "send", "stop"]));
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.mode === "rpc" && (child.state === "idle" || child.state === "running"));
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,48 +103,45 @@ 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
- return;
131
- return attachSelected(ctx, manager);
132
- },
133
- });
134
- pi.registerShortcut("ctrl+shift+s", {
135
- description: "Stop subagent",
136
- handler: (ctx) => {
137
- if (ctx.mode !== "tui")
138
106
  return;
139
- return stopSelected(ctx, manager);
107
+ }
108
+ ctx.ui.notify(`Unknown subcommand "${action}". ${SUBAGENT_COMMAND_USAGE}`, "warning");
140
109
  },
141
110
  });
142
111
  }
143
- export function bindParentStatus(_pi, context, manager) {
112
+ export function bindParentStatus(_pi, context, manager, signal) {
113
+ let disposed = false;
144
114
  const refresh = () => {
145
- void manager.list().then((children) => {
146
- context.ui.setStatus(STATUS_KEY, formatStatusLine(children));
147
- });
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(() => { });
148
130
  };
149
131
  const unsubscribe = manager.onChange(refresh);
150
132
  refresh();
151
133
  return () => {
134
+ if (disposed)
135
+ return;
136
+ disposed = true;
152
137
  unsubscribe();
153
- context.ui.setStatus(STATUS_KEY, undefined);
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
+ }
154
146
  };
155
147
  }
@@ -6,5 +6,3 @@
6
6
  */
7
7
  export declare function writeDiagnostic(scope: string, message: string): void;
8
8
  export declare function toError(value: unknown): Error;
9
- export declare function errorMessage(value: unknown): string;
10
- export declare function abortError(): Error;
@@ -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 = "starting" | "running" | "idle" | "done" | "stopped" | "failed";
4
- export type ExecutionMode = "rpc" | "tui";
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 ExtensionSelection {
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 mode: ExecutionMode;
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 mode: ExecutionMode;
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 isExecutionMode(value: unknown): value is ExecutionMode;
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
- stopped: true,
40
- failed: true,
90
+ blocked: true,
91
+ error: true,
41
92
  };
42
93
  export function isSubagentState(value) {
43
- return typeof value === "string" && SUBAGENT_STATES[value] === true;
94
+ return (typeof value === "string" &&
95
+ (SUBAGENT_STATES[value] === true || normalizeSubagentState(value) !== undefined));
44
96
  }
45
- const EXECUTION_MODES = { rpc: true, tui: true };
46
- export function isExecutionMode(value) {
47
- return typeof value === "string" && EXECUTION_MODES[value] === true;
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
- }
@@ -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;