@hheei/pi-subagents 0.7.0 → 0.8.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 (59) hide show
  1. package/LICENSES/Apache-2.0.txt +202 -0
  2. package/NOTICE +10 -0
  3. package/README.md +13 -13
  4. package/dist/child-control.d.ts +0 -8
  5. package/dist/child-control.js +40 -8
  6. package/dist/commands.js +1 -1
  7. package/dist/controller-ipc.d.ts +38 -0
  8. package/dist/controller-ipc.js +292 -0
  9. package/dist/controller-main.d.ts +1 -0
  10. package/dist/controller-main.js +26 -0
  11. package/dist/controller-role.d.ts +40 -0
  12. package/dist/controller-role.js +37 -0
  13. package/dist/controller-socket.d.ts +6 -0
  14. package/dist/controller-socket.js +43 -0
  15. package/dist/controller.d.ts +25 -0
  16. package/dist/controller.js +217 -0
  17. package/dist/delegation.d.ts +73 -0
  18. package/dist/delegation.js +71 -0
  19. package/dist/domain.d.ts +20 -18
  20. package/dist/domain.js +6 -1
  21. package/dist/extension.js +43 -26
  22. package/dist/host-adapter.js +1 -1
  23. package/dist/launch-spec.d.ts +1 -3
  24. package/dist/launch-spec.js +38 -15
  25. package/dist/lease.d.ts +0 -1
  26. package/dist/lease.js +8 -11
  27. package/dist/mailbox.js +110 -34
  28. package/dist/manager.d.ts +52 -6
  29. package/dist/manager.js +568 -32
  30. package/dist/mcp/app.html +948 -0
  31. package/dist/mcp/delegate.d.ts +30 -0
  32. package/dist/mcp/delegate.js +44 -0
  33. package/dist/mcp/views.d.ts +5 -0
  34. package/dist/mcp/views.js +12 -0
  35. package/dist/mcp-server.d.ts +6 -0
  36. package/dist/mcp-server.js +287 -0
  37. package/dist/mcp-stdio.d.ts +2 -0
  38. package/dist/mcp-stdio.js +111 -0
  39. package/dist/process-lock.d.ts +1 -1
  40. package/dist/process-lock.js +4 -2
  41. package/dist/protocol.d.ts +5 -5
  42. package/dist/protocol.js +3 -4
  43. package/dist/receipts.d.ts +80 -0
  44. package/dist/receipts.js +326 -0
  45. package/dist/runtime.d.ts +5 -26
  46. package/dist/runtime.js +15 -57
  47. package/dist/session-bootstrap.js +2 -6
  48. package/dist/state-db.d.ts +11 -4
  49. package/dist/state-db.js +102 -24
  50. package/dist/state.js +12 -3
  51. package/dist/tool-activation.d.ts +2 -2
  52. package/dist/tool-activation.js +5 -5
  53. package/dist/tool-presentation.d.ts +2 -6
  54. package/dist/tool-presentation.js +75 -122
  55. package/dist/tools.d.ts +0 -3
  56. package/dist/tools.js +104 -132
  57. package/package.json +12 -5
  58. package/dist/json-lines.d.ts +0 -14
  59. package/dist/json-lines.js +0 -95
package/dist/state-db.js CHANGED
@@ -2,11 +2,17 @@ import { chmodSync, mkdirSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { DatabaseSync } from "node:sqlite";
4
4
  import { getAgentDir } from "@earendil-works/pi-coding-agent";
5
- /** The one durable file for child records, commands, and tokens. */
5
+ /** The one durable file for child records, the mailbox, and delegation receipts. */
6
6
  export function defaultStatePath() {
7
7
  return join(getAgentDir(), "pi-subagents", "state.sqlite");
8
8
  }
9
- const SCHEMA = `
9
+ /**
10
+ * Bumped only when an existing ledger needs work. A ledger written by a newer version is refused
11
+ * instead of being rewritten: this file is runtime state, but silently discarding another build's
12
+ * records is worse than failing to start.
13
+ */
14
+ const SCHEMA_VERSION = 2;
15
+ const TABLES = `
10
16
  CREATE TABLE IF NOT EXISTS children (
11
17
  parent_session_id TEXT NOT NULL,
12
18
  subagent_id TEXT NOT NULL,
@@ -33,27 +39,91 @@ CREATE TABLE IF NOT EXISTS commands (
33
39
  error TEXT,
34
40
  created_at INTEGER NOT NULL
35
41
  );
36
- CREATE INDEX IF NOT EXISTS commands_pending
37
- ON commands (parent_session_id, subagent_id, direction, status, created_at);
38
- CREATE TABLE IF NOT EXISTS tokens (
42
+ CREATE TABLE IF NOT EXISTS delegations (
39
43
  parent_session_id TEXT NOT NULL,
40
- runtime_identity TEXT NOT NULL,
41
- token TEXT NOT NULL,
42
- PRIMARY KEY (parent_session_id, runtime_identity)
44
+ delegation_id TEXT NOT NULL,
45
+ child_id TEXT,
46
+ delegation_revision INTEGER,
47
+ task TEXT NOT NULL,
48
+ input_checkpoint TEXT,
49
+ run_boundary INTEGER,
50
+ control_id TEXT,
51
+ status TEXT NOT NULL,
52
+ delivery TEXT NOT NULL,
53
+ result_text TEXT,
54
+ safe_to_retry INTEGER,
55
+ side_effects TEXT,
56
+ created_at INTEGER NOT NULL,
57
+ updated_at INTEGER NOT NULL,
58
+ PRIMARY KEY (parent_session_id, delegation_id)
43
59
  );
44
60
  `;
61
+ /**
62
+ * Indexes are created after the column repair below, because a ledger that predates
63
+ * `delegation_revision` cannot index it yet.
64
+ */
65
+ const INDEXES = `
66
+ CREATE INDEX IF NOT EXISTS commands_pending
67
+ ON commands (parent_session_id, subagent_id, direction, status, created_at);
68
+ CREATE INDEX IF NOT EXISTS delegations_child
69
+ ON delegations (parent_session_id, child_id, status);
70
+ CREATE UNIQUE INDEX IF NOT EXISTS delegations_revision
71
+ ON delegations (parent_session_id, child_id, delegation_revision);
72
+ `;
73
+ /**
74
+ * Columns added to a table that already exists. `CREATE TABLE IF NOT EXISTS` leaves an older
75
+ * table untouched, so an 11-column `delegations` would keep failing the indexes above.
76
+ */
77
+ const ADDED_COLUMNS = [
78
+ ["delegations", "delegation_revision", "INTEGER"],
79
+ ["delegations", "control_id", "TEXT"],
80
+ ["delegations", "safe_to_retry", "INTEGER"],
81
+ ["delegations", "side_effects", "TEXT"],
82
+ ];
45
83
  const connections = new Map();
84
+ function schemaVersion(db) {
85
+ return Number(db.prepare("PRAGMA user_version").get()?.user_version ?? 0);
86
+ }
87
+ /** Brings any older ledger to {@link SCHEMA_VERSION} in place, keeping every row. */
88
+ function migrate(db) {
89
+ const version = schemaVersion(db);
90
+ if (version > SCHEMA_VERSION) {
91
+ throw new Error(`the subagent ledger was written by a newer version (schema ${version}); this version understands schema ${SCHEMA_VERSION}`);
92
+ }
93
+ if (version === SCHEMA_VERSION)
94
+ return;
95
+ withWrite(db, () => {
96
+ if (schemaVersion(db) === SCHEMA_VERSION)
97
+ return;
98
+ db.exec(TABLES);
99
+ for (const [table, column, type] of ADDED_COLUMNS) {
100
+ const columns = db.prepare(`PRAGMA table_info(${table})`).all();
101
+ if (columns.some((existing) => existing.name === column))
102
+ continue;
103
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${type}`);
104
+ }
105
+ // The bridge token had no reader once the mailbox replaced the socket handshake.
106
+ db.exec("DROP TABLE IF EXISTS tokens");
107
+ db.exec(INDEXES);
108
+ db.exec(`PRAGMA user_version = ${SCHEMA_VERSION}`);
109
+ });
110
+ }
46
111
  export function openStateDb(filePath) {
47
112
  const cached = connections.get(filePath);
48
113
  if (cached !== undefined)
49
114
  return cached;
50
115
  mkdirSync(dirname(filePath), { recursive: true, mode: 0o700 });
51
116
  const db = new DatabaseSync(filePath);
52
- db.exec("PRAGMA journal_mode = WAL");
53
- db.exec("PRAGMA busy_timeout = 5000");
54
- db.exec(SCHEMA);
55
- db.exec("DROP TABLE IF EXISTS presence");
56
- chmodSync(filePath, 0o600);
117
+ try {
118
+ db.exec("PRAGMA busy_timeout = 5000");
119
+ db.exec("PRAGMA journal_mode = WAL");
120
+ migrate(db);
121
+ chmodSync(filePath, 0o600);
122
+ }
123
+ catch (error) {
124
+ db.close();
125
+ throw error;
126
+ }
57
127
  connections.set(filePath, db);
58
128
  return db;
59
129
  }
@@ -194,16 +264,24 @@ export function finishCommand(db, id, outcome) {
194
264
  }
195
265
  db.prepare("UPDATE commands SET status = 'failed', error_code = ?, error = ? WHERE id = ?").run(outcome.errorCode, outcome.error, id);
196
266
  }
197
- export function readToken(db, parentSessionId, runtimeIdentity) {
198
- const row = db
199
- .prepare("SELECT token FROM tokens WHERE parent_session_id = ? AND runtime_identity = ?")
200
- .get(parentSessionId, runtimeIdentity);
201
- return row?.token;
202
- }
203
- export function writeToken(db, parentSessionId, runtimeIdentity, token) {
204
- db.prepare(`INSERT INTO tokens (parent_session_id, runtime_identity, token) VALUES (?, ?, ?)
205
- ON CONFLICT (parent_session_id, runtime_identity) DO UPDATE SET token = excluded.token`).run(parentSessionId, runtimeIdentity, token);
267
+ /**
268
+ * A settled command whose waiter died with its process has no reader left. Only a parent that is
269
+ * taking over the mailbox may run this: a live waiter deletes its own row when it reads it.
270
+ */
271
+ export function deleteSettledCommands(db, parentSessionId) {
272
+ db.prepare(`DELETE FROM commands
273
+ WHERE parent_session_id = ? AND status IN ('done', 'failed')`).run(parentSessionId);
206
274
  }
207
- export function deleteToken(db, parentSessionId, runtimeIdentity) {
208
- db.prepare("DELETE FROM tokens WHERE parent_session_id = ? AND runtime_identity = ?").run(parentSessionId, runtimeIdentity);
275
+ /**
276
+ * Keeps only the newest `keep` queued events for one child. Events are advisory snapshots, so a
277
+ * parent that stopped reading must not leave an unbounded backlog behind.
278
+ */
279
+ export function trimEventBacklog(db, parentSessionId, subagentId, keep) {
280
+ db.prepare(`DELETE FROM commands WHERE id IN (
281
+ SELECT id FROM commands
282
+ WHERE parent_session_id = ? AND subagent_id = ? AND direction = 'to_parent'
283
+ AND operation = 'event' AND status = 'pending'
284
+ ORDER BY created_at DESC
285
+ LIMIT -1 OFFSET ?
286
+ )`).run(parentSessionId, subagentId, keep);
209
287
  }
package/dist/state.js CHANGED
@@ -32,6 +32,14 @@ function text(message) {
32
32
  .trim();
33
33
  return value === "" ? undefined : value;
34
34
  }
35
+ /** Activity lines stay short. The full answer lives on the receipt, not in the dashboard summary. */
36
+ const ACTIVITY_SUMMARY_LIMIT = 240;
37
+ function activitySummary(value) {
38
+ const flat = value.replace(/\s+/g, " ").trim();
39
+ if (flat.length <= ACTIVITY_SUMMARY_LIMIT)
40
+ return flat;
41
+ return `${flat.slice(0, ACTIVITY_SUMMARY_LIMIT - 1)}…`;
42
+ }
35
43
  function completed(message) {
36
44
  return message.stopReason !== "pending" && message.stopReason !== "deferred";
37
45
  }
@@ -83,7 +91,7 @@ export function summarizeCurrentBranch(entries) {
83
91
  continue;
84
92
  const value = text(item.message);
85
93
  if (value !== undefined)
86
- return value;
94
+ return activitySummary(value);
87
95
  }
88
96
  return undefined;
89
97
  }
@@ -181,7 +189,7 @@ export function parseTranscriptFacts(value) {
181
189
  costUsd === undefined) {
182
190
  return undefined;
183
191
  }
184
- const summary = typeof value.summary === "string" ? value.summary : undefined;
192
+ const summary = typeof value.summary === "string" ? activitySummary(value.summary) : undefined;
185
193
  const stopReason = typeof value.stopReason === "string" ? value.stopReason : undefined;
186
194
  const interrupted = typeof value.interrupted === "string" ? value.interrupted : undefined;
187
195
  const contextTokens = usageNumber(value.contextTokens);
@@ -297,7 +305,8 @@ export function createStateProjector(initialState = "running") {
297
305
  const messages = Array.isArray(value.messages) ? value.messages : [];
298
306
  const message = record(messages.length > 0 ? messages[messages.length - 1] : value.message);
299
307
  if (message?.role === "assistant") {
300
- summary = text(message) ?? summary;
308
+ const spoken = text(message);
309
+ summary = spoken === undefined ? summary : activitySummary(spoken);
301
310
  if (value.type === "agent_end" && message.stopReason === "error") {
302
311
  state = "error";
303
312
  interrupted = diagnostic(message);
@@ -1,10 +1,10 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
2
2
  import type { SubagentManager } from "./manager.js";
3
3
  /** Interactive agent tools managed by dynamic activation. */
4
- export declare const INTERACTIVE_TOOL_NAMES: readonly ["agent_spawn", "agent_send", "agent_get", "agent_stop"];
4
+ export declare const INTERACTIVE_TOOL_NAMES: readonly ["agents_delegate", "agents_get", "agents_stop", "agents_dashboard"];
5
5
  export type InteractiveToolName = (typeof INTERACTIVE_TOOL_NAMES)[number];
6
6
  export declare const SUBAGENT_OPTIONAL_TOOL_KEY = "subagents";
7
- export declare const SUBAGENT_OPTIONAL_TOOL_BULLET = "subagents (query: \"agent\"): Subagent orchestration and delegation (agent_spawn, agent_send, agent_get, agent_stop).";
7
+ export declare const SUBAGENT_OPTIONAL_TOOL_BULLET = "subagents (query: \"agent\"): Agent orchestration and delegation (agents_delegate, agents_get, agents_stop, agents_dashboard).";
8
8
  export interface RegisterInteractiveToolActivationOptions {
9
9
  readonly pi: ExtensionAPI;
10
10
  readonly manager: SubagentManager;
@@ -1,13 +1,13 @@
1
1
  import { setOptionalToolBullet } from "@hheei/pi-ext-core";
2
2
  /** Interactive agent tools managed by dynamic activation. */
3
3
  export const INTERACTIVE_TOOL_NAMES = [
4
- "agent_spawn",
5
- "agent_send",
6
- "agent_get",
7
- "agent_stop",
4
+ "agents_delegate",
5
+ "agents_get",
6
+ "agents_stop",
7
+ "agents_dashboard",
8
8
  ];
9
9
  export const SUBAGENT_OPTIONAL_TOOL_KEY = "subagents";
10
- export const SUBAGENT_OPTIONAL_TOOL_BULLET = 'subagents (query: "agent"): Subagent orchestration and delegation (agent_spawn, agent_send, agent_get, agent_stop).';
10
+ export const SUBAGENT_OPTIONAL_TOOL_BULLET = 'subagents (query: "agent"): Agent orchestration and delegation (agents_delegate, agents_get, agents_stop, agents_dashboard).';
11
11
  export function registerInteractiveToolActivation(options) {
12
12
  const { pi } = options;
13
13
  const unbindBeforeAgent = pi.on("before_agent_start", (event) => {
@@ -1,14 +1,10 @@
1
1
  import type { ExtensionAPI, ToolRenderers } from "@earendil-works/pi-coding-agent";
2
- export interface AgentSpawnDetails {
3
- readonly id: string;
4
- readonly path: string;
5
- }
6
2
  /** One renderer set per tool, named exactly after its tool so nested calls resolve too. */
7
3
  export declare function parentToolRenderers(pi: ExtensionAPI): {
8
- readonly spawn: ToolRenderers;
9
- readonly send: ToolRenderers;
4
+ readonly delegate: ToolRenderers;
10
5
  readonly get: ToolRenderers;
11
6
  readonly stop: ToolRenderers;
7
+ readonly dashboard: ToolRenderers;
12
8
  };
13
9
  /** The child's blocker report names the reason in the header and keeps the full message readable. */
14
10
  export declare function contactParentRenderers(pi: ExtensionAPI): ToolRenderers;
@@ -14,150 +14,103 @@ function stringField(source, key) {
14
14
  const value = source?.[key];
15
15
  return typeof value === "string" && value.trim() !== "" ? value : undefined;
16
16
  }
17
- function numberField(source, key) {
18
- const value = source[key];
19
- return typeof value === "number" && Number.isFinite(value) ? value : undefined;
17
+ function delegationDetails(result) {
18
+ const details = result?.details;
19
+ return isRecord(details) && typeof details.id === "string"
20
+ ? details
21
+ : undefined;
20
22
  }
21
- function spawnPresentation(args, result) {
23
+ /** Readable delegation state; the result body is copied, never restated as state. */
24
+ function delegationLines(view) {
25
+ const lines = [`status: ${view.status}`, `revision: ${view.revision}`];
26
+ if (view.progress !== undefined) {
27
+ lines.push(`runtime: ${view.progress.runtimeState}`);
28
+ if (view.progress.activeTool !== undefined)
29
+ lines.push(`activeTool: ${view.progress.activeTool}`);
30
+ if (view.progress.summary !== undefined)
31
+ lines.push(`summary: ${view.progress.summary}`);
32
+ }
33
+ lines.push(`hasOutput: ${String(view.hasOutput)}`);
34
+ if (view.error !== undefined)
35
+ lines.push(`error: ${view.error.reason}`);
36
+ return lines;
37
+ }
38
+ function delegationRow(view) {
39
+ const parts = [view.status];
40
+ if (view.agent !== undefined)
41
+ parts.push(view.agent);
42
+ if (view.progress !== undefined) {
43
+ parts.push(view.progress.activeTool ?? view.progress.runtimeState);
44
+ }
45
+ parts.push(`rev ${view.revision}`);
46
+ return parts.join(" · ");
47
+ }
48
+ function delegatePresentation(args, result) {
22
49
  const record = isRecord(args) ? args : undefined;
23
- const agent = stringField(record, "agent") ?? "agent";
24
- const title = stringField(record, "title");
50
+ const agent = stringField(record, "agent");
51
+ const id = stringField(record, "id");
25
52
  const task = stringField(record, "task");
26
- const details = result !== undefined && isRecord(result.details) ? result.details : undefined;
27
- const id = stringField(details, "id");
28
- const path = stringField(details, "path");
29
- const resultText = result === undefined ? undefined : agentResultText(result);
30
- const rows = [
31
- ...(id === undefined ? [] : [`id: ${id}`]),
32
- ...(path === undefined ? [] : [`path: ${path}`]),
33
- ];
34
- const summaryParts = [agent];
35
- if (title !== undefined && title.trim() !== "") {
36
- summaryParts.push(title.trim());
53
+ const view = delegationDetails(result);
54
+ const resultText = view?.resultText;
55
+ const summaryParts = [view?.agent ?? agent ?? "agent"];
56
+ if (view !== undefined) {
57
+ summaryParts.push(view.id, `rev ${view.revision}`);
37
58
  }
38
- if (id !== undefined && id.trim() !== "") {
39
- summaryParts.push(id.trim());
59
+ else if (id !== undefined) {
60
+ summaryParts.push(id);
40
61
  }
62
+ const output = resultText ?? (view === undefined ? undefined : delegationLines(view).join("\n"));
41
63
  return {
42
64
  summary: summaryParts.join(" · "),
43
65
  ...(task === undefined ? {} : { request: task }),
66
+ ...(output === undefined ? {} : { output }),
44
67
  ...(task === undefined
45
68
  ? {}
46
- : {
47
- copy: {
48
- primary: task,
49
- full: resultText === undefined ? task : `${task}\n---\n${resultText}`,
50
- },
51
- }),
52
- ...(rows.length === 0 ? {} : { output: rows.join("\n") }),
53
- };
54
- }
55
- function sendPresentation(args, result) {
56
- const record = isRecord(args) ? args : undefined;
57
- const id = stringField(record, "id") ?? "agent";
58
- const mode = stringField(record, "mode") ?? "steer";
59
- const message = stringField(record, "message");
60
- const confirmation = result === undefined ? undefined : agentResultText(result);
61
- const copy = message === undefined
62
- ? undefined
63
- : {
64
- primary: message,
65
- ...(confirmation === undefined ? {} : { full: `${message}\n${confirmation}` }),
66
- };
67
- return {
68
- summary: `${id} · ${mode}`,
69
- ...(message === undefined ? {} : { request: message }),
70
- ...(confirmation === undefined ? {} : { output: confirmation }),
71
- ...(copy === undefined ? {} : { copy }),
69
+ : { copy: { primary: task, full: output === undefined ? task : `${task}\n---\n${output}` } }),
72
70
  };
73
71
  }
74
- const PROGRESS_TEXT_KEYS = ["agent", "path", "presentation", "freshness", "activeTool"];
75
- const PROGRESS_NUMBER_KEYS = ["turns", "tokens"];
76
- /** Readable selected fields only; never restates the task or a terminal answer. */
77
- function detailLines(details) {
78
- const lines = [];
79
- const status = stringField(details, "status");
80
- if (status !== undefined)
81
- lines.push(`status: ${status}`);
82
- const progress = isRecord(details.progress) ? details.progress : undefined;
83
- if (progress !== undefined) {
84
- for (const key of PROGRESS_TEXT_KEYS) {
85
- const value = stringField(progress, key);
86
- if (value !== undefined)
87
- lines.push(`${key}: ${value}`);
88
- }
89
- for (const key of PROGRESS_NUMBER_KEYS) {
90
- const value = numberField(progress, key);
91
- if (value !== undefined)
92
- lines.push(`${key}: ${value}`);
93
- }
94
- const summary = stringField(progress, "summary");
95
- if (summary !== undefined)
96
- lines.push(`summary: ${summary}`);
97
- }
98
- const diagnostic = stringField(details, "diagnostic");
99
- if (diagnostic !== undefined)
100
- lines.push(`diagnostic: ${diagnostic}`);
101
- return lines;
102
- }
103
- function getCollapsedRow(details) {
104
- const parts = [];
105
- const status = stringField(details, "status");
106
- if (status !== undefined)
107
- parts.push(status);
108
- const progress = isRecord(details.progress) ? details.progress : undefined;
109
- if (progress !== undefined) {
110
- const agent = stringField(progress, "agent");
111
- if (agent !== undefined)
112
- parts.push(agent);
113
- const turns = numberField(progress, "turns");
114
- if (turns !== undefined)
115
- parts.push(`${turns} turn${turns === 1 ? "" : "s"}`);
116
- const tokens = numberField(progress, "tokens");
117
- if (tokens !== undefined) {
118
- const tokStr = tokens >= 1000 ? `${(tokens / 1000).toFixed(1).replace(/\.0$/u, "")}k` : String(tokens);
119
- parts.push(`${tokStr} tok`);
120
- }
121
- const activeTool = stringField(progress, "activeTool");
122
- if (activeTool !== undefined) {
123
- parts.push(activeTool);
124
- }
125
- else {
126
- const summary = stringField(progress, "summary");
127
- if (summary !== undefined)
128
- parts.push(summary.replace(/\s+/gu, " ").trim());
129
- }
130
- }
131
- const diagnostic = stringField(details, "diagnostic");
132
- if (diagnostic !== undefined) {
133
- parts.push(`diagnostic: ${diagnostic.replace(/\s+/gu, " ").trim()}`);
134
- }
135
- return parts.length > 0 ? parts.join(" · ") : undefined;
136
- }
137
72
  function getPresentation(args, result) {
138
73
  const record = isRecord(args) ? args : undefined;
139
74
  const id = stringField(record, "id") ?? "agent";
140
- const keys = stringField(record, "keys") ?? "status, progress";
141
- const details = result !== undefined && isRecord(result.details) ? result.details : undefined;
142
- const lines = details === undefined ? [] : detailLines(details);
143
- const collapsedRow = details === undefined ? undefined : getCollapsedRow(details);
144
- const resultText = result === undefined ? undefined : agentResultText(result);
75
+ const revision = record?.revision;
76
+ const view = delegationDetails(result);
77
+ const lines = view === undefined ? [] : delegationLines(view);
78
+ const resultText = view?.resultText;
145
79
  return {
146
- summary: id,
147
- request: `keys: ${keys}`,
80
+ summary: typeof revision === "number" ? `${id} · rev ${revision}` : id,
81
+ request: record?.includeOutput === true ? "with output" : "without output",
148
82
  ...(lines.length === 0 ? {} : { output: lines.join("\n") }),
149
- ...(collapsedRow === undefined ? {} : { collapsedRows: [collapsedRow] }),
83
+ ...(view === undefined ? {} : { collapsedRows: [delegationRow(view)] }),
150
84
  ...(resultText === undefined ? {} : { copy: { primary: resultText } }),
151
85
  };
152
86
  }
153
87
  function stopPresentation(args, result) {
154
88
  const record = isRecord(args) ? args : undefined;
155
89
  const id = stringField(record, "id") ?? "agent";
90
+ const view = delegationDetails(result);
156
91
  const confirmation = result === undefined ? undefined : agentResultText(result);
92
+ const output = confirmation ?? (view === undefined ? undefined : `status: ${view.status}`);
157
93
  return {
158
94
  summary: id,
159
- ...(confirmation === undefined ? {} : { output: confirmation }),
160
- ...(confirmation === undefined ? {} : { copy: { primary: confirmation } }),
95
+ ...(output === undefined ? {} : { output }),
96
+ ...(output === undefined ? {} : { copy: { primary: output } }),
97
+ };
98
+ }
99
+ function dashboardPresentation(_args, result) {
100
+ const details = result?.details;
101
+ const view = isRecord(details) && Array.isArray(details.children)
102
+ ? details
103
+ : undefined;
104
+ if (view === undefined)
105
+ return { summary: "agents" };
106
+ const rows = view.children.map((child) => {
107
+ const agent = child.agent === undefined ? "" : ` (${child.agent})`;
108
+ const summary = child.summary === undefined ? "" : ` — ${child.summary}`;
109
+ return `- ${child.id}${agent}: ${child.runtimeState} / ${child.status} · rev ${child.revision}${summary}`;
110
+ });
111
+ return {
112
+ summary: `${view.total} agents · running ${view.running} · blocked ${view.blocked} · completed ${view.completed} · failed ${view.failed}`,
113
+ ...(rows.length === 0 ? {} : { output: rows.join("\n") }),
161
114
  };
162
115
  }
163
116
  function contactPresentation(args, result) {
@@ -181,10 +134,10 @@ function contactPresentation(args, result) {
181
134
  /** One renderer set per tool, named exactly after its tool so nested calls resolve too. */
182
135
  export function parentToolRenderers(pi) {
183
136
  return {
184
- spawn: ownedRenderers(pi, "agent_spawn", spawnPresentation),
185
- send: ownedRenderers(pi, "agent_send", sendPresentation),
186
- get: ownedRenderers(pi, "agent_get", getPresentation),
187
- stop: ownedRenderers(pi, "agent_stop", stopPresentation),
137
+ delegate: ownedRenderers(pi, "agents_delegate", delegatePresentation),
138
+ get: ownedRenderers(pi, "agents_get", getPresentation),
139
+ stop: ownedRenderers(pi, "agents_stop", stopPresentation),
140
+ dashboard: ownedRenderers(pi, "agents_dashboard", dashboardPresentation),
188
141
  };
189
142
  }
190
143
  /** The child's blocker report names the reason in the header and keeps the full message readable. */
package/dist/tools.d.ts CHANGED
@@ -3,8 +3,6 @@ import type { DiscoveredAgent } from "./agent-resolver.js";
3
3
  import { type ChildIdentity } from "./domain.js";
4
4
  import type { SubagentManager } from "./manager.js";
5
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
6
  export declare function registerParentTools(pi: ExtensionAPI, manager: SubagentManager, agents: readonly DiscoveredAgent[]): void;
9
7
  /** Sends a child-initiated request over the bridge and resolves with the parent's answer. */
10
8
  export type ChildReport = (operation: string, payload: unknown, signal?: AbortSignal) => Promise<unknown>;
@@ -14,4 +12,3 @@ export interface RegisterChildToolsOptions {
14
12
  readonly isBound?: (sessionId: string) => boolean;
15
13
  }
16
14
  export declare function registerChildTools(pi: ExtensionAPI, identity: ChildIdentity, options: RegisterChildToolsOptions): void;
17
- export {};