@yandy0725/pi-subagents 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -5
- package/src/lifecycle/subagent-manager.ts +1 -1
- package/src/lifecycle/subagent-state.ts +1 -1
- package/src/observation/notification.ts +2 -2
- package/src/observation/renderer.ts +4 -4
- package/src/service/service-adapter.ts +3 -4
- package/src/session/conversation.ts +1 -1
- package/src/session/prompts.ts +2 -2
- package/src/tools/agent-tool.ts +1 -1
- package/src/tools/result-renderer.ts +13 -13
- package/src/tools/spawn-config.ts +8 -5
- package/src/ui/agent-widget.ts +3 -3
- package/src/ui/display.ts +2 -2
- package/src/ui/session-navigator.ts +1 -1
- package/src/ui/widget-renderer.ts +4 -4
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"publishConfig": {
|
|
4
4
|
"access": "public"
|
|
5
5
|
},
|
|
6
|
-
"version": "0.2.
|
|
6
|
+
"version": "0.2.2",
|
|
7
7
|
"description": "Focused, in-process sub-agent core for pi — autonomous agents plus a typed API and lifecycle events",
|
|
8
8
|
"license": "MIT",
|
|
9
9
|
"repository": {
|
|
@@ -35,10 +35,8 @@
|
|
|
35
35
|
"peerDependencies": {
|
|
36
36
|
"@earendil-works/pi-ai": ">=0.80.2",
|
|
37
37
|
"@earendil-works/pi-coding-agent": ">=0.80.2",
|
|
38
|
-
"@earendil-works/pi-tui": ">=0.80.2"
|
|
39
|
-
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@sinclair/typebox": "^0.34.49"
|
|
38
|
+
"@earendil-works/pi-tui": ">=0.80.2",
|
|
39
|
+
"@sinclair/typebox": "*"
|
|
42
40
|
},
|
|
43
41
|
"devDependencies": {
|
|
44
42
|
"@biomejs/biome": "^2.5.0",
|
|
@@ -134,7 +134,7 @@ export class SubagentManager {
|
|
|
134
134
|
this.observer?.onSubagentStarted(agent);
|
|
135
135
|
},
|
|
136
136
|
onSessionCreated: options.observer?.onSessionCreated
|
|
137
|
-
? (agent) => options.observer
|
|
137
|
+
? (agent) => options.observer?.onSessionCreated?.(agent)
|
|
138
138
|
: undefined,
|
|
139
139
|
onRunFinished: (agent) => {
|
|
140
140
|
if (options.isBackground) {
|
|
@@ -122,7 +122,7 @@ export class SubagentState {
|
|
|
122
122
|
|
|
123
123
|
/** Record a tool starting. Called by record-observer on tool_execution_start. */
|
|
124
124
|
addActiveTool(toolName: string): void {
|
|
125
|
-
this._activeTools.set(toolName
|
|
125
|
+
this._activeTools.set(`${toolName}_${++this._toolKeySeq}`, toolName);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
/** Remove one active tool by name (first match). Called by record-observer on tool_execution_end. */
|
|
@@ -53,7 +53,7 @@ export function formatTaskNotification(record: Subagent, resultMaxLen: number):
|
|
|
53
53
|
|
|
54
54
|
const resultPreview = record.result
|
|
55
55
|
? record.result.length > resultMaxLen
|
|
56
|
-
? record.result.slice(0, resultMaxLen)
|
|
56
|
+
? `${record.result.slice(0, resultMaxLen)}\n...(truncated, use get_subagent_result for full output)`
|
|
57
57
|
: record.result
|
|
58
58
|
: "No output.";
|
|
59
59
|
|
|
@@ -92,7 +92,7 @@ export function buildNotificationDetails(record: Subagent, resultMaxLen: number)
|
|
|
92
92
|
modelName: record.modelName,
|
|
93
93
|
resultPreview: record.result
|
|
94
94
|
? record.result.length > resultMaxLen
|
|
95
|
-
? record.result.slice(0, resultMaxLen)
|
|
95
|
+
? `${record.result.slice(0, resultMaxLen)}…`
|
|
96
96
|
: record.result
|
|
97
97
|
: "No output.",
|
|
98
98
|
};
|
|
@@ -42,21 +42,21 @@ export function createNotificationRenderer() {
|
|
|
42
42
|
if (d.totalTokens > 0) parts.push(formatTokens(d.totalTokens));
|
|
43
43
|
if (d.durationMs > 0) parts.push(formatMs(d.durationMs));
|
|
44
44
|
if (parts.length) {
|
|
45
|
-
line +=
|
|
45
|
+
line += `\n ${parts.map((p) => theme.fg("dim", p)).join(` ${theme.fg("dim", "·")} `)}`;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
// Line 3: result preview (collapsed) or full (expanded)
|
|
49
49
|
if (expanded) {
|
|
50
50
|
const lines = d.resultPreview.split("\n").slice(0, 30);
|
|
51
|
-
for (const l of lines) line +=
|
|
51
|
+
for (const l of lines) line += `\n${theme.fg("dim", ` ${l}`)}`;
|
|
52
52
|
} else {
|
|
53
53
|
const preview = d.resultPreview.split("\n")[0]?.slice(0, 80) ?? "";
|
|
54
|
-
line +=
|
|
54
|
+
line += `\n ${theme.fg("dim", `⎿ ${preview}`)}`;
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
// Line 4: output file link (if present)
|
|
58
58
|
if (d.outputFile) {
|
|
59
|
-
line +=
|
|
59
|
+
line += `\n ${theme.fg("muted", `transcript: ${d.outputFile}`)}`;
|
|
60
60
|
}
|
|
61
61
|
|
|
62
62
|
return new Text(line, 0, 0);
|
|
@@ -46,7 +46,6 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
46
46
|
}
|
|
47
47
|
|
|
48
48
|
let model: unknown;
|
|
49
|
-
let modelName: string | undefined;
|
|
50
49
|
if (options?.model) {
|
|
51
50
|
const registry = this.runtime.currentCtx.modelRegistry;
|
|
52
51
|
if (!registry) {
|
|
@@ -59,9 +58,9 @@ export class SubagentsServiceAdapter implements SubagentsService {
|
|
|
59
58
|
model = resolved;
|
|
60
59
|
}
|
|
61
60
|
// Always compute display model name, even when same as parent
|
|
62
|
-
modelName = resolveModelName(
|
|
63
|
-
(model as { id?: string; name?: string } | undefined) ??
|
|
64
|
-
(this.runtime.currentCtx.model as { id?: string; name?: string } | undefined),
|
|
61
|
+
const modelName = resolveModelName(
|
|
62
|
+
(model as { id?: string; name?: string; provider?: string } | undefined) ??
|
|
63
|
+
(this.runtime.currentCtx.model as { id?: string; name?: string; provider?: string } | undefined),
|
|
65
64
|
);
|
|
66
65
|
|
|
67
66
|
const description = options?.description ?? prompt.slice(0, 80);
|
|
@@ -27,7 +27,7 @@ export function getAgentConversation(session: AgentSession): string {
|
|
|
27
27
|
if (toolNames.length > 0) parts.push(`[Tool Calls]:\n${toolNames.map((n) => ` Tool: ${n}`).join("\n")}`);
|
|
28
28
|
} else if (msg.role === "toolResult") {
|
|
29
29
|
const text = extractText(msg.content);
|
|
30
|
-
const truncated = text.length > 200 ? text.slice(0, 200)
|
|
30
|
+
const truncated = text.length > 200 ? `${text.slice(0, 200)}...` : text;
|
|
31
31
|
parts.push(`[Tool Result (${msg.toolName})]: ${truncated}`);
|
|
32
32
|
}
|
|
33
33
|
}
|
package/src/session/prompts.ts
CHANGED
|
@@ -66,14 +66,14 @@ You are operating as a sub-agent invoked to handle a specific task.
|
|
|
66
66
|
// placed verbatim (no wrapper tag) so it forms an identical byte prefix
|
|
67
67
|
// with the parent session, maximising KV cache hits. The <active_agent>
|
|
68
68
|
// tag and env block vary per call and are placed after the cached prefix.
|
|
69
|
-
return identity
|
|
69
|
+
return `${identity}\n\n${bridge}\n\n${activeAgentTag}${envBlock}${customSection}`;
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
// "replace" mode — parent/genericBase prefix first for KV cache reuse, then
|
|
73
73
|
// the active_agent tag, env block, and the config's full system prompt.
|
|
74
74
|
// Unlike append mode, no <sub_agent_context> bridge or <agent_instructions>
|
|
75
75
|
// wrapper is injected — the custom prompt retains full control.
|
|
76
|
-
return identity
|
|
76
|
+
return `${identity}\n\n${activeAgentTag}${envBlock}\n\n${config.systemPrompt}`;
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
/** Fallback base prompt when parent system prompt is unavailable (both modes). */
|
package/src/tools/agent-tool.ts
CHANGED
|
@@ -192,7 +192,7 @@ Guidelines:
|
|
|
192
192
|
const displayName = args.subagent_type ? getDisplayName(args.subagent_type as string, registry) : "Subagent";
|
|
193
193
|
const desc = (args.description as string | undefined) ?? "";
|
|
194
194
|
return new Text(
|
|
195
|
-
|
|
195
|
+
`▸ ${theme.fg("toolTitle", theme.bold(displayName))}${desc ? ` ${theme.fg("muted", desc)}` : ""}`,
|
|
196
196
|
0,
|
|
197
197
|
0,
|
|
198
198
|
);
|
|
@@ -33,8 +33,8 @@ export function renderAgentResult(
|
|
|
33
33
|
export function renderRunning(details: AgentDetails, theme: Theme): string {
|
|
34
34
|
const frame = SPINNER[details.spinnerFrame ?? 0];
|
|
35
35
|
const s = renderStats(details, theme);
|
|
36
|
-
let line = theme.fg("accent", frame) + (s ?
|
|
37
|
-
line +=
|
|
36
|
+
let line = theme.fg("accent", frame) + (s ? ` ${s}` : "");
|
|
37
|
+
line += `\n${theme.fg("dim", ` ⎿ ${details.activity ?? "thinking\u2026"}`)}`;
|
|
38
38
|
return line;
|
|
39
39
|
}
|
|
40
40
|
|
|
@@ -49,22 +49,22 @@ export function renderCompleted(details: AgentDetails, resultText: string, expan
|
|
|
49
49
|
const isSteered = details.status === "steered";
|
|
50
50
|
const icon = isSteered ? theme.fg("warning", "\u2713") : theme.fg("success", "\u2713");
|
|
51
51
|
const s = renderStats(details, theme);
|
|
52
|
-
let line = icon + (s ?
|
|
53
|
-
line +=
|
|
52
|
+
let line = icon + (s ? ` ${s}` : "");
|
|
53
|
+
line += ` ${theme.fg("dim", "\u00B7")} ${theme.fg("dim", duration)}`;
|
|
54
54
|
|
|
55
55
|
if (expanded) {
|
|
56
56
|
if (resultText) {
|
|
57
57
|
const lines = resultText.split("\n").slice(0, 50);
|
|
58
58
|
for (const l of lines) {
|
|
59
|
-
line +=
|
|
59
|
+
line += `\n${theme.fg("dim", ` ${l}`)}`;
|
|
60
60
|
}
|
|
61
61
|
if (resultText.split("\n").length > 50) {
|
|
62
|
-
line +=
|
|
62
|
+
line += `\n${theme.fg("muted", " ... (use get_subagent_result with verbose for full output)")}`;
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
} else {
|
|
66
66
|
const doneText = isSteered ? "Wrapped up (turn limit)" : "Done";
|
|
67
|
-
line +=
|
|
67
|
+
line += `\n${theme.fg("dim", ` \u23BF ${doneText}`)}`;
|
|
68
68
|
}
|
|
69
69
|
return line;
|
|
70
70
|
}
|
|
@@ -72,20 +72,20 @@ export function renderCompleted(details: AgentDetails, resultText: string, expan
|
|
|
72
72
|
/** Render stopped status: dim stop icon + stats + "Stopped". */
|
|
73
73
|
export function renderStopped(details: AgentDetails, theme: Theme): string {
|
|
74
74
|
const s = renderStats(details, theme);
|
|
75
|
-
let line = theme.fg("dim", "\u25A0") + (s ?
|
|
76
|
-
line +=
|
|
75
|
+
let line = theme.fg("dim", "\u25A0") + (s ? ` ${s}` : "");
|
|
76
|
+
line += `\n${theme.fg("dim", " \u23BF Stopped")}`;
|
|
77
77
|
return line;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
/** Render error or aborted status: error icon + stats + status message. */
|
|
81
81
|
export function renderFailed(details: AgentDetails, theme: Theme): string {
|
|
82
82
|
const s = renderStats(details, theme);
|
|
83
|
-
let line = theme.fg("error", "\u2717") + (s ?
|
|
83
|
+
let line = theme.fg("error", "\u2717") + (s ? ` ${s}` : "");
|
|
84
84
|
|
|
85
85
|
if (details.status === "error") {
|
|
86
|
-
line +=
|
|
86
|
+
line += `\n${theme.fg("error", ` \u23BF Error: ${details.error ?? "unknown"}`)}`;
|
|
87
87
|
} else {
|
|
88
|
-
line +=
|
|
88
|
+
line += `\n${theme.fg("warning", " \u23BF Aborted (max turns exceeded)")}`;
|
|
89
89
|
}
|
|
90
90
|
return line;
|
|
91
91
|
}
|
|
@@ -105,5 +105,5 @@ export function renderStats(details: AgentDetails, theme: Theme): string {
|
|
|
105
105
|
}
|
|
106
106
|
if (details.toolUses > 0) parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
|
|
107
107
|
if (details.tokens) parts.push(details.tokens);
|
|
108
|
-
return parts.map((p) => theme.fg("dim", p)).join(
|
|
108
|
+
return parts.map((p) => theme.fg("dim", p)).join(` ${theme.fg("dim", "\u00B7")} `);
|
|
109
109
|
}
|
|
@@ -16,16 +16,19 @@ import type { AgentInvocation, SubagentType, ThinkingLevel } from "../types";
|
|
|
16
16
|
import { type AgentDetails, buildInvocationTags, getDisplayName, getPromptModeLabel } from "../ui/display";
|
|
17
17
|
|
|
18
18
|
/** Derive a short display model name from a model object (or undefined). */
|
|
19
|
-
export function resolveModelName(
|
|
19
|
+
export function resolveModelName(
|
|
20
|
+
model: { id?: string; name?: string; provider?: string } | undefined,
|
|
21
|
+
): string | undefined {
|
|
20
22
|
if (!model) return undefined;
|
|
21
|
-
|
|
22
|
-
if (
|
|
23
|
-
|
|
23
|
+
// Primary: provider/id complete format
|
|
24
|
+
if (model.provider && model.id) return `${model.provider}/${model.id}`;
|
|
25
|
+
// Fallback: raw name or id, no transformation
|
|
26
|
+
return model.name ?? model.id ?? undefined;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
/** Model info extracted from the parent session context. */
|
|
27
30
|
export interface ModelInfo {
|
|
28
|
-
parentModel: { id: string; name?: string } | undefined;
|
|
31
|
+
parentModel: { id: string; name?: string; provider?: string } | undefined;
|
|
29
32
|
modelRegistry: unknown;
|
|
30
33
|
}
|
|
31
34
|
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -203,12 +203,12 @@ export class AgentWidget implements SubagentManagerObserver {
|
|
|
203
203
|
*/
|
|
204
204
|
private clearWidget(backgroundAgents: readonly AgentSummary[]): void {
|
|
205
205
|
if (this.widgetRegistered) {
|
|
206
|
-
this.uiCtx
|
|
206
|
+
this.uiCtx?.setWidget("agents", undefined);
|
|
207
207
|
this.widgetRegistered = false;
|
|
208
208
|
this.tui = undefined;
|
|
209
209
|
}
|
|
210
210
|
if (this.lastStatusText !== undefined) {
|
|
211
|
-
this.uiCtx
|
|
211
|
+
this.uiCtx?.setStatus("subagents", undefined);
|
|
212
212
|
this.lastStatusText = undefined;
|
|
213
213
|
}
|
|
214
214
|
if (this.widgetInterval) {
|
|
@@ -234,7 +234,7 @@ export class AgentWidget implements SubagentManagerObserver {
|
|
|
234
234
|
newStatusText = `${statusParts.join(", ")} agent${total === 1 ? "" : "s"}`;
|
|
235
235
|
}
|
|
236
236
|
if (newStatusText !== this.lastStatusText) {
|
|
237
|
-
this.uiCtx
|
|
237
|
+
this.uiCtx?.setStatus("subagents", newStatusText);
|
|
238
238
|
this.lastStatusText = newStatusText;
|
|
239
239
|
}
|
|
240
240
|
}
|
package/src/ui/display.ts
CHANGED
|
@@ -142,7 +142,7 @@ function truncateLine(text: string, len = 60): string {
|
|
|
142
142
|
.find((l) => l.trim())
|
|
143
143
|
?.trim() ?? "";
|
|
144
144
|
if (line.length <= len) return line;
|
|
145
|
-
return line.slice(0, len)
|
|
145
|
+
return `${line.slice(0, len)}…`;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
148
|
/** Build a human-readable activity string from currently-running tools or response text. */
|
|
@@ -162,7 +162,7 @@ export function describeActivity(activeTools: ReadonlyMap<string, string>, respo
|
|
|
162
162
|
parts.push(action);
|
|
163
163
|
}
|
|
164
164
|
}
|
|
165
|
-
return parts.join(", ")
|
|
165
|
+
return `${parts.join(", ")}…`;
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
// No tools active — show truncated response text if available
|
|
@@ -270,7 +270,7 @@ export class TranscriptOverlay implements Component {
|
|
|
270
270
|
|
|
271
271
|
const pad = (s: string, len: number): string => s + " ".repeat(Math.max(0, len - visibleWidth(s)));
|
|
272
272
|
const row = (content: string): string =>
|
|
273
|
-
th.fg("border", "│")
|
|
273
|
+
`${th.fg("border", "│")} ${truncateToWidth(pad(content, innerW), innerW)} ${th.fg("border", "│")}`;
|
|
274
274
|
const hrTop = th.fg("border", `╭${"─".repeat(width - 2)}╮`);
|
|
275
275
|
const hrBot = th.fg("border", `╰${"─".repeat(width - 2)}╯`);
|
|
276
276
|
const hrMid = row(th.fg("dim", "─".repeat(innerW)));
|
|
@@ -159,14 +159,14 @@ function buildSections(
|
|
|
159
159
|
): WidgetSections {
|
|
160
160
|
const finishedLines: string[] = [];
|
|
161
161
|
for (const a of categories.finished) {
|
|
162
|
-
finishedLines.push(truncate(theme.fg("dim", "\u251C\u2500")
|
|
162
|
+
finishedLines.push(truncate(`${theme.fg("dim", "\u251C\u2500")} ${renderFinishedLine(a, registry, theme)}`));
|
|
163
163
|
}
|
|
164
164
|
|
|
165
165
|
const runningLines: [string, string][] = [];
|
|
166
166
|
for (const a of categories.running) {
|
|
167
167
|
const [header, act] = renderRunningLines(a, registry, spinnerFrame, theme);
|
|
168
168
|
runningLines.push([
|
|
169
|
-
truncate(theme.fg("dim", "\u251C\u2500")
|
|
169
|
+
truncate(`${theme.fg("dim", "\u251C\u2500")} ${header}`),
|
|
170
170
|
truncate(theme.fg("dim", "\u2502 ") + act),
|
|
171
171
|
]);
|
|
172
172
|
}
|
|
@@ -252,7 +252,7 @@ function assembleOverflow(
|
|
|
252
252
|
const overflowText = overflowParts.join(", ");
|
|
253
253
|
lines.push(
|
|
254
254
|
truncate(
|
|
255
|
-
theme.fg("dim", "\u2514\u2500")
|
|
255
|
+
`${theme.fg("dim", "\u2514\u2500")} ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`,
|
|
256
256
|
),
|
|
257
257
|
);
|
|
258
258
|
return lines;
|
|
@@ -291,7 +291,7 @@ export function renderWidgetLines(params: {
|
|
|
291
291
|
// Assemble with overflow cap (heading takes 1 line).
|
|
292
292
|
const maxBody = MAX_WIDGET_LINES - 1;
|
|
293
293
|
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
294
|
-
const heading = truncate(theme.fg(headingColor, headingIcon)
|
|
294
|
+
const heading = truncate(`${theme.fg(headingColor, headingIcon)} ${theme.fg(headingColor, "Agents")}`);
|
|
295
295
|
|
|
296
296
|
if (totalBody <= maxBody) {
|
|
297
297
|
return assembleWithinBudget(heading, { finishedLines, runningLines, queuedLine });
|