@hheei/pi-subagents 0.2.3 → 0.3.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.
Files changed (44) hide show
  1. package/README.md +32 -17
  2. package/dist/agent-resolver.d.ts +9 -0
  3. package/dist/agent-resolver.js +278 -51
  4. package/dist/bridge-client.d.ts +5 -0
  5. package/dist/bridge-client.js +6 -0
  6. package/dist/builtin-agents.js +50 -37
  7. package/dist/child-bridge.d.ts +0 -8
  8. package/dist/child-bridge.js +60 -43
  9. package/dist/child-control.d.ts +5 -3
  10. package/dist/child-control.js +176 -35
  11. package/dist/commands.d.ts +1 -4
  12. package/dist/commands.js +1 -52
  13. package/dist/domain.d.ts +12 -12
  14. package/dist/domain.js +2 -1
  15. package/dist/extension.d.ts +5 -3
  16. package/dist/extension.js +42 -44
  17. package/dist/host-adapter.js +1 -1
  18. package/dist/info.d.ts +9 -0
  19. package/dist/info.js +54 -0
  20. package/dist/launch-spec.d.ts +0 -1
  21. package/dist/launch-spec.js +38 -18
  22. package/dist/manager.d.ts +4 -2
  23. package/dist/manager.js +299 -114
  24. package/dist/registry.js +19 -5
  25. package/dist/rpc-events.d.ts +1 -1
  26. package/dist/rpc-events.js +0 -1
  27. package/dist/runtime.js +8 -0
  28. package/dist/session-bootstrap.d.ts +2 -1
  29. package/dist/session-bootstrap.js +22 -14
  30. package/dist/state.d.ts +13 -0
  31. package/dist/state.js +59 -16
  32. package/dist/status.d.ts +14 -0
  33. package/dist/status.js +36 -0
  34. package/dist/tool-activation.d.ts +3 -4
  35. package/dist/tool-activation.js +13 -215
  36. package/dist/tool-presentation.d.ts +17 -0
  37. package/dist/tool-presentation.js +160 -0
  38. package/dist/tools.d.ts +6 -1
  39. package/dist/tools.js +133 -180
  40. package/package.json +6 -6
  41. package/dist/session-fork.d.ts +0 -16
  42. package/dist/session-fork.js +0 -67
  43. package/dist/widget.d.ts +0 -12
  44. package/dist/widget.js +0 -149
@@ -0,0 +1,160 @@
1
+ import { agentResultText, createToolPresentationRenderers, isRecord, } from "@hheei/pi-ext-core";
2
+ function stringField(source, key) {
3
+ const value = source?.[key];
4
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
5
+ }
6
+ function numberField(source, key) {
7
+ const value = source[key];
8
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
9
+ }
10
+ function spawnPresentation(args, result) {
11
+ const record = isRecord(args) ? args : undefined;
12
+ const agent = stringField(record, "agent") ?? "agent";
13
+ const title = stringField(record, "title");
14
+ const task = stringField(record, "task");
15
+ const details = result !== undefined && isRecord(result.details) ? result.details : undefined;
16
+ const id = stringField(details, "id");
17
+ const path = stringField(details, "path");
18
+ const resultText = result === undefined ? undefined : agentResultText(result);
19
+ const rows = [
20
+ ...(id === undefined ? [] : [`id: ${id}`]),
21
+ ...(path === undefined ? [] : [`path: ${path}`]),
22
+ ];
23
+ const summaryParts = [agent];
24
+ if (title !== undefined && title.trim() !== "") {
25
+ summaryParts.push(title.trim());
26
+ }
27
+ if (id !== undefined && id.trim() !== "") {
28
+ summaryParts.push(id.trim());
29
+ }
30
+ return {
31
+ summary: summaryParts.join(" · "),
32
+ ...(task === undefined ? {} : { request: task }),
33
+ ...(task === undefined
34
+ ? {}
35
+ : {
36
+ copy: {
37
+ primary: task,
38
+ full: resultText === undefined ? task : `${task}\n---\n${resultText}`,
39
+ },
40
+ }),
41
+ ...(rows.length === 0 ? {} : { output: rows.join("\n") }),
42
+ };
43
+ }
44
+ function sendPresentation(args, result) {
45
+ const record = isRecord(args) ? args : undefined;
46
+ const id = stringField(record, "id") ?? "agent";
47
+ const mode = stringField(record, "mode") ?? "auto";
48
+ const message = stringField(record, "message");
49
+ const confirmation = result === undefined ? undefined : agentResultText(result);
50
+ const copy = message === undefined
51
+ ? undefined
52
+ : {
53
+ primary: message,
54
+ ...(confirmation === undefined ? {} : { full: `${message}\n${confirmation}` }),
55
+ };
56
+ return {
57
+ summary: `${id} · ${mode}`,
58
+ ...(message === undefined ? {} : { request: message }),
59
+ ...(confirmation === undefined ? {} : { output: confirmation }),
60
+ ...(copy === undefined ? {} : { copy }),
61
+ };
62
+ }
63
+ const PROGRESS_TEXT_KEYS = ["agent", "path", "presentation", "freshness", "activeTool"];
64
+ const PROGRESS_NUMBER_KEYS = ["turns", "tokens"];
65
+ /** Readable selected fields only; never restates the task or a terminal answer. */
66
+ function detailLines(details) {
67
+ const lines = [];
68
+ const status = stringField(details, "status");
69
+ if (status !== undefined)
70
+ lines.push(`status: ${status}`);
71
+ const progress = isRecord(details.progress) ? details.progress : undefined;
72
+ if (progress !== undefined) {
73
+ for (const key of PROGRESS_TEXT_KEYS) {
74
+ const value = stringField(progress, key);
75
+ if (value !== undefined)
76
+ lines.push(`${key}: ${value}`);
77
+ }
78
+ for (const key of PROGRESS_NUMBER_KEYS) {
79
+ const value = numberField(progress, key);
80
+ if (value !== undefined)
81
+ lines.push(`${key}: ${value}`);
82
+ }
83
+ const summary = stringField(progress, "summary");
84
+ if (summary !== undefined)
85
+ lines.push(`summary: ${summary}`);
86
+ }
87
+ const diagnostic = stringField(details, "diagnostic");
88
+ if (diagnostic !== undefined)
89
+ lines.push(`diagnostic: ${diagnostic}`);
90
+ return lines;
91
+ }
92
+ function getCollapsedRow(details) {
93
+ const parts = [];
94
+ const status = stringField(details, "status");
95
+ if (status !== undefined)
96
+ parts.push(status);
97
+ const progress = isRecord(details.progress) ? details.progress : undefined;
98
+ if (progress !== undefined) {
99
+ const agent = stringField(progress, "agent");
100
+ if (agent !== undefined)
101
+ parts.push(agent);
102
+ const turns = numberField(progress, "turns");
103
+ if (turns !== undefined)
104
+ parts.push(`${turns} turn${turns === 1 ? "" : "s"}`);
105
+ const tokens = numberField(progress, "tokens");
106
+ if (tokens !== undefined) {
107
+ const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1).replace(/\.0$/u, "")}k` : String(tokens);
108
+ parts.push(`${tokStr} tok`);
109
+ }
110
+ const activeTool = stringField(progress, "activeTool");
111
+ if (activeTool !== undefined) {
112
+ parts.push(activeTool);
113
+ }
114
+ else {
115
+ const summary = stringField(progress, "summary");
116
+ if (summary !== undefined)
117
+ parts.push(summary.replace(/\s+/gu, " ").trim());
118
+ }
119
+ }
120
+ const diagnostic = stringField(details, "diagnostic");
121
+ if (diagnostic !== undefined) {
122
+ parts.push(`diagnostic: ${diagnostic.replace(/\s+/gu, " ").trim()}`);
123
+ }
124
+ return parts.length > 0 ? parts.join(" · ") : undefined;
125
+ }
126
+ function getPresentation(args, result) {
127
+ const record = isRecord(args) ? args : undefined;
128
+ const id = stringField(record, "id") ?? "agent";
129
+ const keys = stringField(record, "keys") ?? "status, progress";
130
+ const details = result !== undefined && isRecord(result.details) ? result.details : undefined;
131
+ const lines = details === undefined ? [] : detailLines(details);
132
+ const collapsedRow = details === undefined ? undefined : getCollapsedRow(details);
133
+ const resultText = result === undefined ? undefined : agentResultText(result);
134
+ return {
135
+ summary: id,
136
+ request: `keys: ${keys}`,
137
+ ...(lines.length === 0 ? {} : { output: lines.join("\n") }),
138
+ ...(collapsedRow === undefined ? {} : { collapsedRows: [collapsedRow] }),
139
+ ...(resultText === undefined ? {} : { copy: { primary: resultText } }),
140
+ };
141
+ }
142
+ function stopPresentation(args, result) {
143
+ const record = isRecord(args) ? args : undefined;
144
+ const id = stringField(record, "id") ?? "agent";
145
+ const confirmation = result === undefined ? undefined : agentResultText(result);
146
+ return {
147
+ summary: id,
148
+ ...(confirmation === undefined ? {} : { output: confirmation }),
149
+ ...(confirmation === undefined ? {} : { copy: { primary: confirmation } }),
150
+ };
151
+ }
152
+ /** One renderer set per tool, named exactly after its tool so nested calls resolve too. */
153
+ export function parentToolRenderers(pi) {
154
+ return {
155
+ spawn: createToolPresentationRenderers(pi, "agent_spawn", spawnPresentation),
156
+ send: createToolPresentationRenderers(pi, "agent_send", sendPresentation),
157
+ get: createToolPresentationRenderers(pi, "agent_get", getPresentation),
158
+ stop: createToolPresentationRenderers(pi, "agent_stop", stopPresentation),
159
+ };
160
+ }
package/dist/tools.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+ import type { DiscoveredAgent } from "./agent-resolver.js";
2
3
  import { type ChildIdentity } from "./domain.js";
3
4
  import type { SubagentManager } from "./manager.js";
4
- export declare function registerParentTools(pi: ExtensionAPI, manager: SubagentManager): void;
5
+ export declare function formatAvailableAgents(agents: readonly DiscoveredAgent[]): string;
6
+ declare const GET_KEYS: readonly ["status", "progress", "diagnostic"];
7
+ export type AgentGetKey = (typeof GET_KEYS)[number];
8
+ export declare function registerParentTools(pi: ExtensionAPI, manager: SubagentManager, agents: readonly DiscoveredAgent[]): void;
5
9
  /** Sends a child-initiated request over the bridge and resolves with the parent's answer. */
6
10
  export type ChildReport = (operation: string, payload: unknown, signal?: AbortSignal) => Promise<unknown>;
7
11
  export interface RegisterChildToolsOptions {
@@ -10,3 +14,4 @@ export interface RegisterChildToolsOptions {
10
14
  readonly isBound?: (sessionId: string) => boolean;
11
15
  }
12
16
  export declare function registerChildTools(pi: ExtensionAPI, identity: ChildIdentity, options: RegisterChildToolsOptions): void;
17
+ export {};
package/dist/tools.js CHANGED
@@ -1,26 +1,20 @@
1
- import { getToolTui, isRecord, registerToolTuiTrace, textToolResult } from "@hheei/pi-ext-core";
1
+ import { textToolResult } from "@hheei/pi-ext-core";
2
2
  import { Type } from "typebox";
3
- import { discoverAgents } from "./agent-resolver.js";
4
- import { builtinAgents } from "./builtin-agents.js";
3
+ import { Value } from "typebox/value";
5
4
  import { isOperationError } from "./domain.js";
5
+ import { parentToolRenderers } from "./tool-presentation.js";
6
6
  const spawnSchema = Type.Object({
7
- id: Type.Optional(Type.String({
8
- description: "Optional subagent id (e.g. 'agent-1') to re-awaken an idle or completed subagent with a new task instead of creating a new one.",
9
- })),
10
7
  task: Type.String({ minLength: 1 }),
11
8
  agent: Type.String({
12
9
  minLength: 1,
13
- description: "Name of an agent defined in .pi/agents/*.md or ~/.pi/agent/agents/*.md",
10
+ description: "Agent name from <available_agents> in this tool description.",
14
11
  }),
15
12
  cwd: Type.Optional(Type.String()),
16
13
  title: Type.Optional(Type.String({
17
14
  maxLength: 60,
18
15
  description: "Short session title for this child, e.g. 'OVITO properties editor'. It becomes the child's Pi session title, prefixed with a robot marker so delegated sessions stay recognizable; omit it — or leave it blank — to derive one from the agent name and child id.",
19
16
  })),
20
- forkFrom: Type.Optional(Type.String({
21
- description: "Optional session or subagent id to reference context from: 'parent' (parent session), or a subagent id like 'agent-1' (including subagents that ended in 'error'). Does not inject past conversation into the prompt; provides the referenced session JSONL path in instructions so the subagent can inspect prior context on demand via read or grep. Cannot be 'current'.",
22
- })),
23
- });
17
+ }, { additionalProperties: false });
24
18
  const sendSchema = Type.Object({
25
19
  id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
26
20
  message: Type.String({ minLength: 1 }),
@@ -29,241 +23,203 @@ const sendSchema = Type.Object({
29
23
  const idSchema = Type.Object({
30
24
  id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
31
25
  });
26
+ const getSchema = Type.Object({
27
+ id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
28
+ keys: Type.Optional(Type.String({
29
+ description: "Comma-separated fields to return. Defaults to 'status,progress'. Allowed: status, progress, diagnostic.",
30
+ })),
31
+ });
32
32
  const contactSchema = Type.Object({
33
33
  message: Type.String({
34
34
  minLength: 1,
35
35
  description: "Explanation of what is blocking progress or the error encountered.",
36
36
  }),
37
37
  reason: Type.Optional(Type.Union([Type.Literal("blocked"), Type.Literal("error")], {
38
- description: "Reason: 'blocked' if waiting for input/guidance (can be resumed via send_agent), or 'error' if fatal unrecoverable failure (cannot continue). Defaults to 'blocked'.",
38
+ description: "Reason: 'blocked' if waiting for input/guidance (can be resumed via agent_send), or 'error' if the turn failed (you may retry via agent_send or create a new child). Defaults to 'blocked'.",
39
39
  })),
40
40
  });
41
- const emptySchema = Type.Object({});
42
- function result(value) {
43
- if (isOperationError(value))
44
- throw new Error(JSON.stringify(value));
45
- return textToolResult(typeof value === "string" ? value : JSON.stringify(value), value);
46
- }
47
41
  const SUBAGENTS_NAMESPACE = {
48
- name: "subagents",
42
+ name: "agent",
43
+ aliases: ["agent", "agents", "subagent", "subagents"],
49
44
  description: "Subagent orchestration and delegation tools",
50
45
  };
51
- const SPAWN_DESCRIPTION = "Start an independent background RPC agent. PREFER REUSING EXISTING SUBAGENTS: For subtasks with identical or related functionality, prefer reusing an existing subagent via send_agent (even if in 'done' or 'blocked' state) rather than spawning a fresh subagent each time. Reusing subagents avoids redundant session initialization, preserves ongoing context, and maximizes prompt cache and pi-ext-memory efficiency. Only spawn a new agent when a distinct role or isolated fresh context is genuinely needed. Do NOT freeze, pause, or stop other sub-workers before spawning: idle subagents do not consume compute or interfere, so do not waste an agent turn freezing them. Do NOT poll get_agent or list_agents to wait for the child's work. Results are automatically captured when the child settles, and delivered as a subagent-report message to start your next turn; while you are idle, reports from several children may arrive together. After this tool returns, either end your turn or work on other independent tasks, including spawning more agents in parallel. Do not fabricate or assume the child's results.";
52
- const SPAWN_SNIPPET = "Start a background RPC agent. Prefer reusing existing subagents via send_agent for related work to preserve context and maximize memory/cache efficiency. No need to freeze/stop other workers. Results arrive as subagent-report; do not poll.";
53
- const SPAWN_GUIDELINES = [
54
- "Prefer reusing existing subagents: for subtasks that have the same or related functionality, reuse an existing subagent via send_agent (which auto-resumes 'done' or 'blocked' agents) instead of spawning a new one. This preserves conversational continuity and maximizes prompt cache and memory efficiency.",
55
- "Do not freeze, pause, or stop other subagents: idle subagents consume no compute and do not interfere. Do not waste a turn stopping them.",
56
- "Do not poll get_agent or list_agents waiting for the child to finish.",
57
- "Do not sleep, wait, or tail session/log files to detect completion. The harness delivers reports automatically.",
58
- "After spawn returns, end your turn or do other independent work, including spawning more agents in parallel.",
59
- "Do not fabricate, assume, or summarize the child's results before a report arrives.",
60
- ];
61
- const SEND_DESCRIPTION = "Send a steer or follow-up message to one owned child. PREFERRED FOR SUBSEQUENT/RELATED TASKS: Prefer reusing existing subagents in 'done' or 'blocked' states via send_agent instead of spawning new subagents every time; 'blocked' and 'done' children automatically rebuild and resume with their conversation context intact, maximizing prompt cache and memory efficiency. Subagents in 'error' cannot be continued; use spawn_agent with forkFrom to reference their session context. Do NOT wait for or ask workers to 'freeze': idle subagents are completely dormant and touch nothing until sent to. Dispatch reviewers or follow-up tasks immediately without wasting turns on freeze ceremonies. Do NOT poll get_agent or list_agents afterwards. Child reports arrive as subagent-report messages that start your next turn.";
62
- const SEND_SNIPPET = "Send a message to one owned child (auto-resumes if finished/blocked). Prefer reusing existing agents for related tasks. Reports arrive as subagent-report; do not poll afterwards.";
63
- const GET_DESCRIPTION = "Inspect one owned child: state, presentation (panel or background), summary, usage, and whether the view is live or last-known. Use this when you need current identity or state, not to wait for the child to finish.";
64
- const LIST_DESCRIPTION = "List available interactive agent definitions and running or past subagents owned by this parent session with their state ('running', 'done', 'blocked', 'error'). Use this to inspect existing subagents that can be reused via send_agent before deciding to spawn a new agent, not to wait for work to finish. Reports still arrive as subagent-report messages.";
65
- const LIST_SNIPPET = "List available interactive agent definitions and running subagents.";
66
- const STOP_DESCRIPTION = "Persist a stopped intent, then end that child's runtime.";
46
+ export function formatAvailableAgents(agents) {
47
+ const available = agents
48
+ .filter((agent) => agent.enabled !== false && agent.frontmatter.hidden !== true)
49
+ .sort((a, b) => a.name.localeCompare(b.name))
50
+ .map((agent) => {
51
+ const description = agent.frontmatter.description;
52
+ return `- ${agent.name}: ${typeof description === "string" && description.trim() !== "" ? description.trim() : "No description provided."}`;
53
+ });
54
+ return [
55
+ "<available_agents>",
56
+ ...(available.length > 0 ? available : ["None."]),
57
+ "</available_agents>",
58
+ ].join("\n");
59
+ }
60
+ function getSpawnDescription(agents) {
61
+ return [
62
+ "Spawn an independent background subagent task to work on a goal asynchronously. Choose an agent from <available_agents> below.",
63
+ "Returns JSON { id, path }, where path is the absolute child session file path; the first write may still be pending.",
64
+ "Results are returned automatically to this parent session when the task finishes. After spawning, do independent work or end your turn; do not poll agent_get.",
65
+ "Related subagent tools: agent_send, agent_get, agent_stop.",
66
+ formatAvailableAgents(agents),
67
+ ].join("\n");
68
+ }
69
+ const SEND_DESCRIPTION = "Send input, steer, or follow-up instructions to an existing subagent (e.g. 'agent-1'), resuming its session when it is idle, blocked, done, or errored. Results are returned automatically when the task finishes; do not poll agent_get. Related subagent tools: agent_spawn, agent_get, agent_stop.";
70
+ const GET_DESCRIPTION = "Inspect one subagent's status, progress, or diagnostic details by id (e.g. 'agent-1'). Use keys to choose fields; defaults to 'status,progress'. Results are returned automatically; use this for inspection, not completion polling. Related subagent tools: agent_spawn, agent_send, agent_stop.";
71
+ const STOP_DESCRIPTION = "Stop a subagent's runtime by id (e.g. 'agent-1'). The child keeps its session, so a later agent_send can resume it. Related subagent tools: agent_spawn, agent_send, agent_get.";
67
72
  const CONTACT_DESCRIPTION = "Notify the parent session when you are BLOCKED and cannot continue without parent decision or intervention. Do NOT call this on success or task completion: simply output your final answer directly as normal text in your response, and the harness will automatically capture and report your result to the parent.";
68
73
  const CONTACT_SNIPPET = "Report a blocker to the parent. Do NOT call on success: write normal text to finish.";
69
74
  const CONTACT_GUIDELINES = [
70
75
  "Use contact_parent ONLY if you are blocked or urgently require a parent decision midway.",
71
76
  "Do NOT use contact_parent to deliver final findings or report success: output normal text instead.",
72
- "After reporting a blocker, wait for a parent send_agent instruction.",
77
+ "After reporting a blocker, wait for a parent agent_send instruction.",
73
78
  ];
74
- function spawnFooter(result) {
75
- const details = result.details;
76
- const child = details?.child;
77
- if (child !== undefined && typeof child.id === "string") {
78
- const agent = child.displayName ?? child.agent;
79
- return `#${child.id} · ${agent}`;
79
+ const GET_KEYS = ["status", "progress", "diagnostic"];
80
+ /** Parses the comma-separated `keys` argument; empty means the default view. */
81
+ function parseAgentGetKeys(raw) {
82
+ if (raw === undefined)
83
+ return ["status", "progress"];
84
+ const keys = [];
85
+ for (const part of raw.split(",")) {
86
+ const key = part.trim();
87
+ if (key === "")
88
+ continue;
89
+ if (!GET_KEYS.includes(key)) {
90
+ throw new Error(`Unknown agent_get key "${key}". Allowed keys: ${GET_KEYS.join(", ")}.`);
91
+ }
92
+ if (!keys.includes(key))
93
+ keys.push(key);
80
94
  }
81
- return undefined;
95
+ return keys.length > 0 ? keys : ["status", "progress"];
82
96
  }
83
- function childStateFooter(result) {
84
- const details = result.details;
85
- const child = details?.child;
86
- if (child !== undefined && typeof child.id === "string") {
87
- return `#${child.id} · ${child.state}`;
97
+ function childAgentDetails(child, keys) {
98
+ const view = {};
99
+ if (keys.includes("status"))
100
+ view.status = child.state;
101
+ if (keys.includes("progress")) {
102
+ view.progress = {
103
+ agent: child.agent,
104
+ ...(child.path === undefined ? {} : { path: child.path }),
105
+ presentation: child.presentation,
106
+ freshness: child.freshness,
107
+ ...(child.activeTool === undefined ? {} : { activeTool: child.activeTool.slice(0, 200) }),
108
+ ...(child.state !== "running" || child.summary === undefined
109
+ ? {}
110
+ : { summary: child.summary.slice(0, 2000) }),
111
+ ...(child.usage === undefined
112
+ ? {}
113
+ : { turns: child.usage.turns, tokens: child.usage.inputTokens + child.usage.outputTokens }),
114
+ };
88
115
  }
89
- return undefined;
90
- }
91
- function listFooter(result) {
92
- const details = result.details;
93
- if (isRecord(details) && Array.isArray(details.runningAgents)) {
94
- const count = details.runningAgents.length;
95
- if (count === 0)
96
- return undefined;
97
- return count === 1 ? "1 child" : `${count} children`;
98
- }
99
- if (Array.isArray(details)) {
100
- const count = details.length;
101
- if (count === 0)
102
- return undefined;
103
- return count === 1 ? "1 child" : `${count} children`;
116
+ if (keys.includes("diagnostic")) {
117
+ const diagnostic = child.interrupted ??
118
+ (child.state === "blocked" || child.state === "error" ? child.summary : undefined) ??
119
+ (child.freshness === "last_known" ? "Runtime disconnected; state is last known" : undefined);
120
+ if (diagnostic !== undefined)
121
+ view.diagnostic = diagnostic.slice(0, 2000);
104
122
  }
105
- return undefined;
123
+ return view;
106
124
  }
107
- export function registerParentTools(pi, manager) {
108
- registerToolTuiTrace(pi);
109
- const tui = getToolTui(pi);
125
+ export function registerParentTools(pi, manager, agents) {
126
+ const renderers = parentToolRenderers(pi);
127
+ const inspectedTasks = new Set();
128
+ function inspectionResult(id, text, details) {
129
+ const repeated = inspectedTasks.has(id);
130
+ inspectedTasks.add(id);
131
+ return textToolResult(repeated ? `${text}\nResults are returned automatically; do not poll agent_get.` : text, details);
132
+ }
110
133
  const spawn = {
111
- name: "spawn_agent",
134
+ name: "agent_spawn",
112
135
  namespace: SUBAGENTS_NAMESPACE,
113
136
  label: "Spawn agent",
114
- description: SPAWN_DESCRIPTION,
115
- promptSnippet: SPAWN_SNIPPET,
116
- promptGuidelines: [...SPAWN_GUIDELINES],
137
+ description: getSpawnDescription(agents),
117
138
  parameters: spawnSchema,
139
+ exposure: "deferred",
118
140
  defaultActive: false,
119
141
  annotations: {
120
142
  openWorldHint: true,
121
143
  },
122
144
  async execute(_id, params) {
123
- return result(await manager.spawn(params));
145
+ if (!Value.Check(spawnSchema, params))
146
+ throw new Error("Invalid agent_spawn parameters");
147
+ const spawned = await manager.spawn(params);
148
+ if (isOperationError(spawned))
149
+ throw new Error(spawned.reason);
150
+ const { id } = spawned.child;
151
+ const { path } = spawned;
152
+ const details = { id, path };
153
+ return textToolResult(JSON.stringify(details), details);
124
154
  },
155
+ ...renderers.spawn,
125
156
  };
126
157
  const send = {
127
- name: "send_agent",
158
+ name: "agent_send",
128
159
  namespace: SUBAGENTS_NAMESPACE,
129
160
  label: "Send to agent",
130
161
  description: SEND_DESCRIPTION,
131
- promptSnippet: SEND_SNIPPET,
132
- promptGuidelines: [
133
- "Prefer reusing an existing subagent in 'done' or 'blocked' states via send_agent for subsequent or functionally identical/related subtasks instead of spawning a new subagent each time. This preserves context and maximizes memory and prompt cache efficiency.",
134
- "Do not wait for or ask workers to 'freeze': idle subagents are dormant and touch nothing. Dispatch reviewers or follow-ups immediately.",
135
- "Do not poll get_agent or list_agents afterwards.",
136
- "Child reports arrive as subagent-report messages that start your next turn.",
137
- ],
138
162
  parameters: sendSchema,
163
+ exposure: "deferred",
139
164
  defaultActive: false,
140
165
  annotations: {
141
166
  openWorldHint: true,
142
167
  },
143
168
  async execute(_id, params, signal) {
144
- return result(await manager.send(params.id, params.message, params.mode, signal));
169
+ const sent = await manager.send(params.id, params.message, params.mode, signal);
170
+ if (isOperationError(sent))
171
+ throw new Error(sent.reason);
172
+ return textToolResult("Message sent.", undefined);
145
173
  },
174
+ ...renderers.send,
146
175
  };
147
176
  const get = {
148
- name: "get_agent",
177
+ name: "agent_get",
149
178
  namespace: SUBAGENTS_NAMESPACE,
150
179
  label: "Get agent",
151
180
  description: GET_DESCRIPTION,
152
- promptSnippet: GET_DESCRIPTION,
153
- parameters: idSchema,
181
+ parameters: getSchema,
182
+ exposure: "deferred",
154
183
  defaultActive: false,
155
184
  annotations: {
156
185
  readOnlyHint: true,
157
186
  idempotentHint: true,
158
187
  },
159
188
  async execute(_id, params) {
160
- return result(await manager.get(params.id));
161
- },
162
- };
163
- const list = {
164
- name: "list_agents",
165
- namespace: SUBAGENTS_NAMESPACE,
166
- label: "List agents",
167
- description: LIST_DESCRIPTION,
168
- promptSnippet: LIST_SNIPPET,
169
- parameters: emptySchema,
170
- annotations: {
171
- readOnlyHint: true,
172
- idempotentHint: true,
173
- },
174
- async execute(_id, _params, _signal, _onUpdate, ctx) {
175
- const children = await manager.list();
176
- const cwd = ctx !== undefined && typeof ctx === "object" && "cwd" in ctx && typeof ctx.cwd === "string"
177
- ? ctx.cwd
178
- : process.cwd();
179
- let allAgents;
180
- try {
181
- allAgents = await discoverAgents(cwd);
182
- }
183
- catch {
184
- allAgents = builtinAgents();
185
- }
186
- const visible = allAgents.filter((agent) => agent.enabled !== false && agent.frontmatter.hidden !== true);
187
- const interactiveAgents = visible
188
- .filter((agent) => agent.frontmatter.interactive === true)
189
- .map((agent) => ({
190
- name: agent.name,
191
- description: typeof agent.frontmatter.description === "string" &&
192
- agent.frontmatter.description.trim() !== ""
193
- ? agent.frontmatter.description.trim()
194
- : "No description provided.",
195
- }))
196
- .sort((a, b) => a.name.localeCompare(b.name));
197
- const lines = [];
198
- if (interactiveAgents.length > 0) {
199
- lines.push("<interactive_agents>");
200
- for (const agent of interactiveAgents) {
201
- lines.push(`- ${agent.name}: ${agent.description}`);
202
- }
203
- lines.push("</interactive_agents>");
204
- }
205
- if (children.length > 0) {
206
- if (lines.length > 0)
207
- lines.push("");
208
- lines.push("<running_agents>");
209
- for (const child of children) {
210
- const summaryPart = child.summary ? ` | summary: ${child.summary}` : "";
211
- lines.push(`- id: ${child.id} | agent: ${child.agent} | state: ${child.state}${summaryPart}`);
212
- }
213
- lines.push("</running_agents>");
214
- }
215
- if (lines.length === 0) {
216
- lines.push("No active or available interactive subagents.");
217
- }
218
- return textToolResult(lines.join("\n"), {
219
- ...(interactiveAgents.length > 0 ? { interactiveAgents } : {}),
220
- runningAgents: children,
221
- });
189
+ const keys = parseAgentGetKeys(params.keys);
190
+ const child = await manager.get(params.id);
191
+ if (isOperationError(child))
192
+ throw new Error(child.reason);
193
+ const view = childAgentDetails(child, keys);
194
+ return inspectionResult(params.id, JSON.stringify(view), view);
222
195
  },
196
+ ...renderers.get,
223
197
  };
224
198
  const stop = {
225
- name: "stop_agent",
199
+ name: "agent_stop",
226
200
  namespace: SUBAGENTS_NAMESPACE,
227
201
  label: "Stop agent",
228
202
  description: STOP_DESCRIPTION,
229
203
  parameters: idSchema,
204
+ exposure: "deferred",
230
205
  defaultActive: false,
231
206
  annotations: {
232
207
  destructiveHint: true,
233
208
  },
234
209
  async execute(_id, params) {
235
- return result(await manager.stop(params.id));
210
+ const stopped = await manager.stop(params.id);
211
+ if (isOperationError(stopped))
212
+ throw new Error(stopped.reason);
213
+ return textToolResult("Agent stopped.", undefined);
236
214
  },
215
+ ...renderers.stop,
237
216
  };
238
- pi.registerTool(tui.frame(spawn, {
239
- summary: (args) => args.agent,
240
- headerLine: "truncate",
241
- footer: spawnFooter,
242
- }));
243
- pi.registerTool(tui.frame(send, {
244
- summary: (args) => args.id,
245
- headerLine: "truncate",
246
- footer: childStateFooter,
247
- }));
248
- pi.registerTool(tui.frame(get, {
249
- summary: (args) => args.id,
250
- headerLine: "truncate",
251
- footer: childStateFooter,
252
- }));
253
- pi.registerTool(tui.frame(list, {
254
- summary: () => "owned children",
255
- headerLine: "truncate",
256
- footer: listFooter,
257
- }));
258
- pi.registerTool(tui.frame(stop, {
259
- summary: (args) => args.id,
260
- headerLine: "truncate",
261
- footer: childStateFooter,
262
- }));
217
+ pi.registerTool(spawn);
218
+ pi.registerTool(send);
219
+ pi.registerTool(get);
220
+ pi.registerTool(stop);
263
221
  }
264
222
  export function registerChildTools(pi, identity, options) {
265
- registerToolTuiTrace(pi);
266
- const tui = getToolTui(pi);
267
223
  const tool = {
268
224
  name: "contact_parent",
269
225
  label: "Contact parent",
@@ -289,11 +245,8 @@ export function registerChildTools(pi, identity, options) {
289
245
  await options.report("contact_parent", details, signal);
290
246
  return textToolResult(reason === "error"
291
247
  ? "Error report delivered to the parent."
292
- : "Blocker report queued for the parent.", details);
248
+ : "Blocker report queued for the parent.", undefined);
293
249
  },
294
250
  };
295
- pi.registerTool(tui.frame(tool, {
296
- summary: (args) => args.message,
297
- headerLine: "truncate",
298
- }));
251
+ pi.registerTool(tool);
299
252
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hheei/pi-subagents",
3
- "version": "0.2.3",
3
+ "version": "0.3.0",
4
4
  "description": "Independent background Pi subagent sessions with durable identity and RPC control.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -35,16 +35,16 @@
35
35
  "access": "public"
36
36
  },
37
37
  "dependencies": {
38
- "@hheei/pi-ext-core": "^0.2.1"
38
+ "@hheei/pi-ext-core": "^0.3.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@earendil-works/pi-coding-agent": ">=1.0.0",
42
- "@earendil-works/pi-tui": ">=1.0.0",
41
+ "@earendil-works/pi-coding-agent": ">=1.2.0",
42
+ "@earendil-works/pi-tui": ">=1.2.0",
43
43
  "typebox": "*"
44
44
  },
45
45
  "devDependencies": {
46
- "@earendil-works/pi-coding-agent": "1.0.0",
47
- "@earendil-works/pi-tui": "1.0.0"
46
+ "@earendil-works/pi-coding-agent": "1.2.0",
47
+ "@earendil-works/pi-tui": "1.2.0"
48
48
  },
49
49
  "scripts": {
50
50
  "build": "node -e \"import { rmSync } from 'node:fs'; rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json"
@@ -1,16 +0,0 @@
1
- import type { SubagentRegistry } from "./registry.js";
2
- /** Reads all JSON entries from a session file on disk. */
3
- export declare function readSessionJsonlEntries(sessionPath: string): Promise<unknown[]>;
4
- export interface ResolveForkSessionReferenceOptions {
5
- readonly forkFrom: string;
6
- readonly parentSessionPath?: string | undefined;
7
- readonly registry?: Pick<SubagentRegistry, "get"> | undefined;
8
- readonly onWarning?: ((message: string) => void) | undefined;
9
- }
10
- /**
11
- * Resolves the source session JSONL file path for on-demand context inspection.
12
- * Rejects 'current' to prevent ambiguous self-references.
13
- */
14
- export declare function resolveForkSessionReference(options: ResolveForkSessionReferenceOptions): Promise<string>;
15
- /** Formats instructions explaining where the referenced session file is and how to inspect it on demand. */
16
- export declare function formatSessionReferencePrompt(sessionPath: string): string;