@hheei/pi-subagents 0.1.2 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -137
- package/dist/agent-resolver.d.ts +20 -0
- package/dist/agent-resolver.js +155 -21
- package/dist/bridge-client.d.ts +55 -0
- package/dist/bridge-client.js +453 -0
- package/dist/bridge-server.d.ts +72 -0
- package/dist/bridge-server.js +490 -0
- package/dist/builtin-agents.d.ts +24 -0
- package/dist/builtin-agents.js +152 -0
- package/dist/child-bridge.d.ts +31 -15
- package/dist/child-bridge.js +95 -117
- package/dist/child-control.d.ts +58 -0
- package/dist/child-control.js +176 -0
- package/dist/child-process.d.ts +21 -0
- package/dist/child-process.js +93 -0
- package/dist/commands.d.ts +2 -2
- package/dist/commands.js +52 -60
- package/dist/diagnostics.d.ts +0 -2
- package/dist/diagnostics.js +0 -8
- package/dist/domain.d.ts +57 -27
- package/dist/domain.js +60 -11
- package/dist/extension.d.ts +26 -1
- package/dist/extension.js +243 -46
- package/dist/host-adapter.d.ts +37 -12
- package/dist/host-adapter.js +214 -63
- package/dist/json-lines.js +2 -1
- package/dist/launch-spec.d.ts +8 -6
- package/dist/launch-spec.js +61 -18
- package/dist/manager.d.ts +93 -49
- package/dist/manager.js +1175 -528
- package/dist/markdown.d.ts +8 -0
- package/dist/markdown.js +21 -0
- package/dist/protocol.d.ts +33 -52
- package/dist/protocol.js +41 -52
- package/dist/registry.d.ts +3 -8
- package/dist/registry.js +41 -163
- package/dist/rpc-events.d.ts +3 -5
- package/dist/rpc-events.js +5 -9
- package/dist/runtime.d.ts +66 -11
- package/dist/runtime.js +182 -208
- package/dist/session-bootstrap.d.ts +23 -7
- package/dist/session-bootstrap.js +79 -15
- package/dist/session-fork.d.ts +16 -0
- package/dist/session-fork.js +67 -0
- package/dist/state.d.ts +7 -0
- package/dist/state.js +60 -25
- package/dist/tool-activation.d.ts +18 -0
- package/dist/tool-activation.js +227 -0
- package/dist/tools.d.ts +6 -3
- package/dist/tools.js +213 -72
- package/dist/widget.d.ts +4 -8
- package/dist/widget.js +55 -64
- package/package.json +51 -52
- package/dist/child-nudge.d.ts +0 -24
- package/dist/child-nudge.js +0 -102
- package/dist/connector.d.ts +0 -53
- package/dist/connector.js +0 -283
- package/dist/pause-gate.d.ts +0 -14
- package/dist/pause-gate.js +0 -64
- package/dist/rpc-adapter.d.ts +0 -40
- package/dist/rpc-adapter.js +0 -194
- package/dist/runner-entry.d.ts +0 -24
- package/dist/runner-entry.js +0 -166
- package/dist/runner.d.ts +0 -52
- package/dist/runner.js +0 -930
package/dist/tools.js
CHANGED
|
@@ -1,130 +1,269 @@
|
|
|
1
|
-
import { getToolTui, registerToolTuiTrace } from "@hheei/pi-ext-core";
|
|
1
|
+
import { getToolTui, isRecord, registerToolTuiTrace, textToolResult } from "@hheei/pi-ext-core";
|
|
2
2
|
import { Type } from "typebox";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { discoverAgents } from "./agent-resolver.js";
|
|
4
|
+
import { builtinAgents } from "./builtin-agents.js";
|
|
5
|
+
import { isOperationError } from "./domain.js";
|
|
5
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
|
+
})),
|
|
6
10
|
task: Type.String({ minLength: 1 }),
|
|
7
|
-
agent: Type.String({
|
|
11
|
+
agent: Type.String({
|
|
12
|
+
minLength: 1,
|
|
13
|
+
description: "Name of an agent defined in .pi/agents/*.md or ~/.pi/agent/agents/*.md",
|
|
14
|
+
}),
|
|
8
15
|
cwd: Type.Optional(Type.String()),
|
|
16
|
+
title: Type.Optional(Type.String({
|
|
17
|
+
maxLength: 60,
|
|
18
|
+
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
|
+
})),
|
|
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
|
+
})),
|
|
9
23
|
});
|
|
10
24
|
const sendSchema = Type.Object({
|
|
11
|
-
id: Type.String({ minLength: 1 }),
|
|
25
|
+
id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
|
|
12
26
|
message: Type.String({ minLength: 1 }),
|
|
13
27
|
mode: Type.Optional(Type.Union([Type.Literal("steer"), Type.Literal("follow_up"), Type.Literal("auto")])),
|
|
14
28
|
});
|
|
15
|
-
const idSchema = Type.Object({
|
|
29
|
+
const idSchema = Type.Object({
|
|
30
|
+
id: Type.String({ minLength: 1, description: "Subagent id, e.g. agent-1" }),
|
|
31
|
+
});
|
|
16
32
|
const contactSchema = Type.Object({
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
message: Type.String({
|
|
34
|
+
minLength: 1,
|
|
35
|
+
description: "Explanation of what is blocking progress or the error encountered.",
|
|
36
|
+
}),
|
|
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'.",
|
|
39
|
+
})),
|
|
24
40
|
});
|
|
25
41
|
const emptySchema = Type.Object({});
|
|
26
|
-
function isOperationError(value) {
|
|
27
|
-
return (isRecord(value) &&
|
|
28
|
-
typeof value.operation === "string" &&
|
|
29
|
-
typeof value.reason === "string" &&
|
|
30
|
-
Array.isArray(value.sideEffects));
|
|
31
|
-
}
|
|
32
42
|
function result(value) {
|
|
33
43
|
if (isOperationError(value))
|
|
34
44
|
throw new Error(JSON.stringify(value));
|
|
35
|
-
return
|
|
36
|
-
content: [{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value) }],
|
|
37
|
-
details: value,
|
|
38
|
-
};
|
|
45
|
+
return textToolResult(typeof value === "string" ? value : JSON.stringify(value), value);
|
|
39
46
|
}
|
|
40
|
-
const
|
|
41
|
-
|
|
47
|
+
const SUBAGENTS_NAMESPACE = {
|
|
48
|
+
name: "subagents",
|
|
49
|
+
description: "Subagent orchestration and delegation tools",
|
|
50
|
+
};
|
|
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.";
|
|
42
53
|
const SPAWN_GUIDELINES = [
|
|
43
|
-
"
|
|
44
|
-
"Do not
|
|
45
|
-
"
|
|
46
|
-
"
|
|
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.",
|
|
47
59
|
"Do not fabricate, assume, or summarize the child's results before a report arrives.",
|
|
48
60
|
];
|
|
49
|
-
const SEND_DESCRIPTION = "Send a steer or follow-up message to one owned child. Do NOT poll
|
|
50
|
-
const SEND_SNIPPET = "Send a message to one owned child. Reports arrive as
|
|
51
|
-
const GET_DESCRIPTION = "Inspect one owned child: state,
|
|
52
|
-
const LIST_DESCRIPTION = "List
|
|
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.";
|
|
53
66
|
const STOP_DESCRIPTION = "Persist a stopped intent, then end that child's runtime.";
|
|
54
|
-
const CONTACT_DESCRIPTION = "
|
|
55
|
-
const CONTACT_SNIPPET = "Report to the parent.
|
|
67
|
+
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
|
+
const CONTACT_SNIPPET = "Report a blocker to the parent. Do NOT call on success: write normal text to finish.";
|
|
56
69
|
const CONTACT_GUIDELINES = [
|
|
57
|
-
"
|
|
58
|
-
"Do
|
|
59
|
-
"After
|
|
70
|
+
"Use contact_parent ONLY if you are blocked or urgently require a parent decision midway.",
|
|
71
|
+
"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.",
|
|
60
73
|
];
|
|
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}`;
|
|
80
|
+
}
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
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}`;
|
|
88
|
+
}
|
|
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`;
|
|
104
|
+
}
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
61
107
|
export function registerParentTools(pi, manager) {
|
|
62
108
|
registerToolTuiTrace(pi);
|
|
63
109
|
const tui = getToolTui(pi);
|
|
64
110
|
const spawn = {
|
|
65
|
-
name: "
|
|
66
|
-
|
|
111
|
+
name: "spawn_agent",
|
|
112
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
113
|
+
label: "Spawn agent",
|
|
67
114
|
description: SPAWN_DESCRIPTION,
|
|
68
115
|
promptSnippet: SPAWN_SNIPPET,
|
|
69
116
|
promptGuidelines: [...SPAWN_GUIDELINES],
|
|
70
117
|
parameters: spawnSchema,
|
|
118
|
+
defaultActive: false,
|
|
119
|
+
annotations: {
|
|
120
|
+
openWorldHint: true,
|
|
121
|
+
},
|
|
71
122
|
async execute(_id, params) {
|
|
72
123
|
return result(await manager.spawn(params));
|
|
73
124
|
},
|
|
74
125
|
};
|
|
75
126
|
const send = {
|
|
76
|
-
name: "
|
|
77
|
-
|
|
127
|
+
name: "send_agent",
|
|
128
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
129
|
+
label: "Send to agent",
|
|
78
130
|
description: SEND_DESCRIPTION,
|
|
79
131
|
promptSnippet: SEND_SNIPPET,
|
|
80
132
|
promptGuidelines: [
|
|
81
|
-
"
|
|
82
|
-
"
|
|
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.",
|
|
83
137
|
],
|
|
84
138
|
parameters: sendSchema,
|
|
139
|
+
defaultActive: false,
|
|
140
|
+
annotations: {
|
|
141
|
+
openWorldHint: true,
|
|
142
|
+
},
|
|
85
143
|
async execute(_id, params, signal) {
|
|
86
144
|
return result(await manager.send(params.id, params.message, params.mode, signal));
|
|
87
145
|
},
|
|
88
146
|
};
|
|
89
147
|
const get = {
|
|
90
|
-
name: "
|
|
91
|
-
|
|
148
|
+
name: "get_agent",
|
|
149
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
150
|
+
label: "Get agent",
|
|
92
151
|
description: GET_DESCRIPTION,
|
|
93
152
|
promptSnippet: GET_DESCRIPTION,
|
|
94
153
|
parameters: idSchema,
|
|
154
|
+
defaultActive: false,
|
|
155
|
+
annotations: {
|
|
156
|
+
readOnlyHint: true,
|
|
157
|
+
idempotentHint: true,
|
|
158
|
+
},
|
|
95
159
|
async execute(_id, params) {
|
|
96
160
|
return result(await manager.get(params.id));
|
|
97
161
|
},
|
|
98
162
|
};
|
|
99
163
|
const list = {
|
|
100
|
-
name: "
|
|
101
|
-
|
|
164
|
+
name: "list_agents",
|
|
165
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
166
|
+
label: "List agents",
|
|
102
167
|
description: LIST_DESCRIPTION,
|
|
103
|
-
promptSnippet:
|
|
168
|
+
promptSnippet: LIST_SNIPPET,
|
|
104
169
|
parameters: emptySchema,
|
|
105
|
-
|
|
106
|
-
|
|
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
|
+
});
|
|
107
222
|
},
|
|
108
223
|
};
|
|
109
224
|
const stop = {
|
|
110
|
-
name: "
|
|
111
|
-
|
|
225
|
+
name: "stop_agent",
|
|
226
|
+
namespace: SUBAGENTS_NAMESPACE,
|
|
227
|
+
label: "Stop agent",
|
|
112
228
|
description: STOP_DESCRIPTION,
|
|
113
229
|
parameters: idSchema,
|
|
114
|
-
|
|
115
|
-
|
|
230
|
+
defaultActive: false,
|
|
231
|
+
annotations: {
|
|
232
|
+
destructiveHint: true,
|
|
233
|
+
},
|
|
234
|
+
async execute(_id, params) {
|
|
235
|
+
return result(await manager.stop(params.id));
|
|
116
236
|
},
|
|
117
237
|
};
|
|
118
|
-
pi.registerTool(tui.frame(spawn, {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
+
}));
|
|
123
263
|
}
|
|
124
|
-
export function registerChildTools(pi, identity,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
: (onReportOrOptions ?? {});
|
|
264
|
+
export function registerChildTools(pi, identity, options) {
|
|
265
|
+
registerToolTuiTrace(pi);
|
|
266
|
+
const tui = getToolTui(pi);
|
|
128
267
|
const tool = {
|
|
129
268
|
name: "contact_parent",
|
|
130
269
|
label: "Contact parent",
|
|
@@ -137,22 +276,24 @@ export function registerChildTools(pi, identity, onReportOrOptions) {
|
|
|
137
276
|
if (options.isBound !== undefined && !options.isBound(sessionId)) {
|
|
138
277
|
throw new Error("This Pi session is not the bound subagent session; contact_parent is disabled.");
|
|
139
278
|
}
|
|
279
|
+
const reason = params.reason ?? "blocked";
|
|
140
280
|
const details = {
|
|
141
281
|
type: "pi_subagent_report",
|
|
142
282
|
parentSessionId: identity.parentSessionId,
|
|
143
283
|
childId: identity.subagentId,
|
|
144
284
|
runtimeIdentity: identity.runtimeIdentity,
|
|
145
|
-
reason
|
|
285
|
+
reason,
|
|
146
286
|
message: params.message,
|
|
147
287
|
sessionId,
|
|
148
288
|
};
|
|
149
|
-
await
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
details,
|
|
154
|
-
};
|
|
289
|
+
await options.report("contact_parent", details, signal);
|
|
290
|
+
return textToolResult(reason === "error"
|
|
291
|
+
? "Error report delivered to the parent."
|
|
292
|
+
: "Blocker report queued for the parent.", details);
|
|
155
293
|
},
|
|
156
294
|
};
|
|
157
|
-
pi.registerTool(tool
|
|
295
|
+
pi.registerTool(tui.frame(tool, {
|
|
296
|
+
summary: (args) => args.message,
|
|
297
|
+
headerLine: "truncate",
|
|
298
|
+
}));
|
|
158
299
|
}
|
package/dist/widget.d.ts
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import type
|
|
2
|
+
import { type PublicSubagent, type SubagentState } from "./domain.js";
|
|
3
|
+
export declare const SUBAGENT_TERMINAL_DISPLAY_DURATION_MS = 15000;
|
|
3
4
|
export interface SubagentWidget {
|
|
4
5
|
refresh(children: readonly PublicSubagent[]): void;
|
|
5
6
|
dispose(): void;
|
|
6
7
|
}
|
|
7
|
-
export declare function
|
|
8
|
+
export declare function isTerminalState(state: SubagentState): boolean;
|
|
9
|
+
export declare function isWidgetVisibleChild(child: PublicSubagent, nowMs?: number): boolean;
|
|
8
10
|
export declare function formatElapsed(fromIso: string, nowMs?: number): string;
|
|
9
11
|
export declare function renderSubagentWidget(children: readonly PublicSubagent[], width: number, theme: Theme, nowMs?: number): string[];
|
|
10
12
|
export declare function createSubagentWidget(pi: ExtensionAPI, context: ExtensionContext, signal: AbortSignal, initial?: readonly PublicSubagent[]): SubagentWidget | undefined;
|
|
11
|
-
export interface ChildIdentityWidgetOptions {
|
|
12
|
-
readonly agent: string;
|
|
13
|
-
readonly toolCount: number;
|
|
14
|
-
}
|
|
15
|
-
export declare function renderChildIdentityWidget(options: ChildIdentityWidgetOptions, width: number, theme: Theme): string[];
|
|
16
|
-
export declare function createChildIdentityWidget(pi: ExtensionAPI, context: ExtensionContext, signal: AbortSignal, options: ChildIdentityWidgetOptions): SubagentWidget | undefined;
|
package/dist/widget.js
CHANGED
|
@@ -1,32 +1,26 @@
|
|
|
1
1
|
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
2
2
|
import { registerWidget } from "@hheei/pi-ext-core";
|
|
3
|
+
import { toVisualSubagentState, VISUAL_SUBAGENT_GLYPH, VISUAL_SUBAGENT_TONE, } from "./domain.js";
|
|
3
4
|
const WIDGET_ID = "@hheei/pi-subagents:status";
|
|
4
|
-
const CHILD_WIDGET_ID = "@hheei/pi-subagents:child-identity";
|
|
5
5
|
const MAX_ROWS = 8;
|
|
6
6
|
const ELAPSED_TICK_MS = 1_000;
|
|
7
|
-
export
|
|
8
|
-
|
|
7
|
+
export const SUBAGENT_TERMINAL_DISPLAY_DURATION_MS = 15_000;
|
|
8
|
+
export function isTerminalState(state) {
|
|
9
|
+
return state === "done" || state === "blocked" || state === "error";
|
|
9
10
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
if (state === "running")
|
|
15
|
-
return theme.fg("accent", "◐");
|
|
16
|
-
if (state === "starting")
|
|
17
|
-
return theme.fg("muted", "○");
|
|
18
|
-
return theme.fg("dim", "○");
|
|
19
|
-
}
|
|
20
|
-
function stateLabel(child, theme) {
|
|
21
|
-
const mode = child.mode === "tui" ? ` ${child.state} tui` : ` ${child.state}`;
|
|
22
|
-
const freshness = child.freshness === "last_known" ? " last known" : "";
|
|
23
|
-
const label = `${mode.trim()}${freshness}`;
|
|
24
|
-
if (child.state === "running" && child.freshness === "live")
|
|
25
|
-
return theme.fg("accent", label);
|
|
26
|
-
if (child.interrupted !== undefined || child.freshness === "last_known") {
|
|
27
|
-
return theme.fg("warning", label);
|
|
11
|
+
export function isWidgetVisibleChild(child, nowMs = Date.now()) {
|
|
12
|
+
const visual = toVisualSubagentState(child.state, child.interrupted);
|
|
13
|
+
if (visual === "running") {
|
|
14
|
+
return true;
|
|
28
15
|
}
|
|
29
|
-
|
|
16
|
+
const updated = Date.parse(child.updatedAt);
|
|
17
|
+
if (!Number.isNaN(updated) && nowMs - updated < SUBAGENT_TERMINAL_DISPLAY_DURATION_MS) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
return false;
|
|
21
|
+
}
|
|
22
|
+
function visibleChildren(children, nowMs = Date.now()) {
|
|
23
|
+
return children.filter((c) => isWidgetVisibleChild(c, nowMs));
|
|
30
24
|
}
|
|
31
25
|
export function formatElapsed(fromIso, nowMs = Date.now()) {
|
|
32
26
|
const from = Date.parse(fromIso);
|
|
@@ -43,30 +37,42 @@ export function formatElapsed(fromIso, nowMs = Date.now()) {
|
|
|
43
37
|
return `${minutes}m ${seconds}s`;
|
|
44
38
|
}
|
|
45
39
|
export function renderSubagentWidget(children, width, theme, nowMs = Date.now()) {
|
|
46
|
-
const visible = visibleChildren(children);
|
|
40
|
+
const visible = visibleChildren(children, nowMs);
|
|
47
41
|
if (visible.length === 0)
|
|
48
42
|
return [];
|
|
49
|
-
const
|
|
50
|
-
const
|
|
51
|
-
const heading = `${theme.fg(headingColor, running > 0 ? "●" : "○")} ${theme.fg("text", `Subagents (${visible.length})`)}`;
|
|
52
|
-
const lines = [truncateToWidth(heading, width, "...")];
|
|
43
|
+
const heading = `🤖 ${theme.fg("text", "Agents:")}`;
|
|
44
|
+
const lines = [truncateToWidth(heading, width, theme.fg("dim", "…"))];
|
|
53
45
|
const rows = visible.slice(0, MAX_ROWS);
|
|
46
|
+
const sep = theme.fg("dim", " · ");
|
|
54
47
|
for (let index = 0; index < rows.length; index++) {
|
|
55
48
|
const child = rows[index];
|
|
56
49
|
if (child === undefined)
|
|
57
50
|
continue;
|
|
58
|
-
const
|
|
59
|
-
const
|
|
60
|
-
const
|
|
51
|
+
const visual = toVisualSubagentState(child.state, child.interrupted);
|
|
52
|
+
const glyph = VISUAL_SUBAGENT_GLYPH[visual];
|
|
53
|
+
const tone = VISUAL_SUBAGENT_TONE[visual];
|
|
54
|
+
const glyphStyled = theme.fg(tone, glyph);
|
|
55
|
+
const idStyled = theme.fg("dim", `#${child.id}`);
|
|
56
|
+
const nameStyled = theme.fg("text", child.displayName ?? child.agent);
|
|
61
57
|
const elapsed = formatElapsed(child.createdAt, nowMs);
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const
|
|
65
|
-
const
|
|
66
|
-
|
|
58
|
+
const timeStyled = theme.fg("dim", elapsed === "" ? "0s" : elapsed);
|
|
59
|
+
const stateStyled = theme.fg(tone, visual);
|
|
60
|
+
const turnsCount = child.usage?.turns ?? 0;
|
|
61
|
+
const turnsLabel = `${turnsCount} turn${turnsCount === 1 ? "" : "s"}`;
|
|
62
|
+
const turnsStyled = theme.fg("dim", turnsLabel);
|
|
63
|
+
let detailText = "";
|
|
64
|
+
if (child.activeTool !== undefined && child.activeTool.trim() !== "") {
|
|
65
|
+
detailText = child.activeTool.trim();
|
|
66
|
+
}
|
|
67
|
+
else if (child.summary !== undefined && child.summary.trim() !== "") {
|
|
68
|
+
detailText = child.summary.replace(/\s+/g, " ").trim();
|
|
69
|
+
}
|
|
70
|
+
const detailStyled = detailText === "" ? "" : `${sep}${theme.fg("dim", detailText)}`;
|
|
71
|
+
const rowContent = `${glyphStyled} ${idStyled} ${nameStyled}${sep}${timeStyled}${sep}${stateStyled}${sep}${turnsStyled}${detailStyled}`;
|
|
72
|
+
lines.push(truncateToWidth(rowContent, width, theme.fg("dim", "…")));
|
|
67
73
|
}
|
|
68
74
|
if (visible.length > MAX_ROWS) {
|
|
69
|
-
lines.push(truncateToWidth(theme.fg("dim",
|
|
75
|
+
lines.push(truncateToWidth(theme.fg("dim", `+${visible.length - MAX_ROWS} more`), width, theme.fg("dim", "…")));
|
|
70
76
|
}
|
|
71
77
|
lines.push("");
|
|
72
78
|
return lines;
|
|
@@ -99,10 +105,22 @@ export function createSubagentWidget(pi, context, signal, initial = []) {
|
|
|
99
105
|
stopTick();
|
|
100
106
|
return;
|
|
101
107
|
}
|
|
108
|
+
// 仅在有进行中任务或待淡出任务时维持 tick,纯 idle 时静默
|
|
109
|
+
const hasActiveOrExpiring = children.some((child) => isWidgetVisibleChild(child));
|
|
110
|
+
if (!hasActiveOrExpiring) {
|
|
111
|
+
stopTick();
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
102
114
|
if (tick !== undefined)
|
|
103
115
|
return;
|
|
104
116
|
tick = setInterval(() => {
|
|
105
|
-
|
|
117
|
+
if (!hasRows()) {
|
|
118
|
+
widget.setVisible(false);
|
|
119
|
+
stopTick();
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
widget.requestRender();
|
|
123
|
+
}
|
|
106
124
|
}, ELAPSED_TICK_MS);
|
|
107
125
|
};
|
|
108
126
|
syncTick();
|
|
@@ -129,30 +147,3 @@ export function createSubagentWidget(pi, context, signal, initial = []) {
|
|
|
129
147
|
},
|
|
130
148
|
};
|
|
131
149
|
}
|
|
132
|
-
export function renderChildIdentityWidget(options, width, theme) {
|
|
133
|
-
const label = options.agent.trim() === "" ? "child" : options.agent.trim();
|
|
134
|
-
const line = `${theme.fg("accent", `[${label}]`)} ${theme.fg("dim", "child")} ${theme.fg("muted", "report via contact_parent")} ${theme.fg("dim", `${options.toolCount} tools`)}`;
|
|
135
|
-
return [truncateToWidth(line, width, "..."), ""];
|
|
136
|
-
}
|
|
137
|
-
export function createChildIdentityWidget(pi, context, signal, options) {
|
|
138
|
-
if (context.mode !== "tui")
|
|
139
|
-
return undefined;
|
|
140
|
-
const widget = registerWidget(pi, context, signal, {
|
|
141
|
-
id: CHILD_WIDGET_ID,
|
|
142
|
-
placement: "aboveEditor",
|
|
143
|
-
visible: true,
|
|
144
|
-
create: (_tui, theme) => ({
|
|
145
|
-
render: (width) => renderChildIdentityWidget(options, width, theme),
|
|
146
|
-
invalidate: () => undefined,
|
|
147
|
-
dispose: () => undefined,
|
|
148
|
-
}),
|
|
149
|
-
});
|
|
150
|
-
return {
|
|
151
|
-
refresh(_next) {
|
|
152
|
-
widget.requestRender();
|
|
153
|
-
},
|
|
154
|
-
dispose() {
|
|
155
|
-
widget.dispose();
|
|
156
|
-
},
|
|
157
|
-
};
|
|
158
|
-
}
|