@tintinweb/pi-subagents 0.3.1 → 0.4.1
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/CHANGELOG.md +29 -1
- package/README.md +17 -15
- package/dist/agent-manager.d.ts +70 -0
- package/dist/agent-manager.js +236 -0
- package/dist/agent-runner.d.ts +60 -0
- package/dist/agent-runner.js +265 -0
- package/dist/agent-types.d.ts +41 -0
- package/dist/agent-types.js +130 -0
- package/dist/context.d.ts +12 -0
- package/dist/context.js +56 -0
- package/dist/custom-agents.d.ts +14 -0
- package/dist/custom-agents.js +100 -0
- package/dist/default-agents.d.ts +7 -0
- package/dist/default-agents.js +126 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +28 -0
- package/dist/group-join.d.ts +32 -0
- package/dist/group-join.js +116 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +1270 -0
- package/dist/model-resolver.d.ts +19 -0
- package/dist/model-resolver.js +62 -0
- package/dist/prompts.d.ts +14 -0
- package/dist/prompts.js +48 -0
- package/dist/types.d.ts +62 -0
- package/dist/types.js +5 -0
- package/dist/ui/agent-widget.d.ts +101 -0
- package/dist/ui/agent-widget.js +333 -0
- package/dist/ui/conversation-viewer.d.ts +31 -0
- package/dist/ui/conversation-viewer.js +236 -0
- package/package.json +1 -1
- package/src/agent-manager.ts +22 -4
- package/src/agent-runner.ts +11 -45
- package/src/agent-types.ts +4 -15
- package/src/default-agents.ts +2 -36
- package/src/index.ts +30 -24
- package/src/prompts.ts +35 -20
- package/src/ui/agent-widget.ts +100 -24
- package/src/ui/conversation-viewer.ts +4 -2
package/src/prompts.ts
CHANGED
|
@@ -7,42 +7,57 @@ import type { AgentConfig, EnvInfo } from "./types.js";
|
|
|
7
7
|
/**
|
|
8
8
|
* Build the system prompt for an agent from its config.
|
|
9
9
|
*
|
|
10
|
-
* - "replace" mode:
|
|
11
|
-
* - "append" mode:
|
|
10
|
+
* - "replace" mode: env header + config.systemPrompt (full control, no parent identity)
|
|
11
|
+
* - "append" mode: env header + parent system prompt + sub-agent context + config.systemPrompt
|
|
12
|
+
* - "append" with empty systemPrompt: pure parent clone
|
|
13
|
+
*
|
|
14
|
+
* @param parentSystemPrompt The parent agent's effective system prompt (for append mode).
|
|
12
15
|
*/
|
|
13
|
-
export function buildAgentPrompt(
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
export function buildAgentPrompt(
|
|
17
|
+
config: AgentConfig,
|
|
18
|
+
cwd: string,
|
|
19
|
+
env: EnvInfo,
|
|
20
|
+
parentSystemPrompt?: string,
|
|
21
|
+
): string {
|
|
22
|
+
const envBlock = `# Environment
|
|
18
23
|
Working directory: ${cwd}
|
|
19
24
|
${env.isGitRepo ? `Git repository: yes\nBranch: ${env.branch}` : "Not a git repository"}
|
|
20
25
|
Platform: ${env.platform}`;
|
|
21
26
|
|
|
22
27
|
if (config.promptMode === "append") {
|
|
23
|
-
const
|
|
28
|
+
const identity = parentSystemPrompt || genericBase;
|
|
24
29
|
|
|
25
|
-
|
|
26
|
-
You are a
|
|
27
|
-
You have full access to read, write, edit files, and execute commands.
|
|
28
|
-
Do what has been asked; nothing more, nothing less.
|
|
29
|
-
|
|
30
|
-
# Tool Usage
|
|
30
|
+
const bridge = `<sub_agent_context>
|
|
31
|
+
You are operating as a sub-agent invoked to handle a specific task.
|
|
31
32
|
- Use the read tool instead of cat/head/tail
|
|
32
33
|
- Use the edit tool instead of sed/awk
|
|
33
34
|
- Use the write tool instead of echo/heredoc
|
|
34
35
|
- Use the find tool instead of bash find/ls for file search
|
|
35
36
|
- Use the grep tool instead of bash grep/rg for content search
|
|
36
37
|
- Make independent tool calls in parallel
|
|
37
|
-
|
|
38
|
-
# Output
|
|
39
38
|
- Use absolute file paths
|
|
40
39
|
- Do not use emojis
|
|
41
|
-
- Be concise but complete
|
|
40
|
+
- Be concise but complete
|
|
41
|
+
</sub_agent_context>`;
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
const customSection = config.systemPrompt?.trim()
|
|
44
|
+
? `\n\n<agent_instructions>\n${config.systemPrompt}\n</agent_instructions>`
|
|
45
|
+
: "";
|
|
46
|
+
|
|
47
|
+
return envBlock + "\n\n<inherited_system_prompt>\n" + identity + "\n</inherited_system_prompt>\n\n" + bridge + customSection;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
|
-
// "replace" mode — header + the config's full system prompt
|
|
47
|
-
|
|
50
|
+
// "replace" mode — env header + the config's full system prompt
|
|
51
|
+
const replaceHeader = `You are a pi coding agent sub-agent.
|
|
52
|
+
You have been invoked to handle a specific task autonomously.
|
|
53
|
+
|
|
54
|
+
${envBlock}`;
|
|
55
|
+
|
|
56
|
+
return replaceHeader + "\n\n" + config.systemPrompt;
|
|
48
57
|
}
|
|
58
|
+
|
|
59
|
+
/** Fallback base prompt when parent system prompt is unavailable in append mode. */
|
|
60
|
+
const genericBase = `# Role
|
|
61
|
+
You are a general-purpose coding agent for complex, multi-step tasks.
|
|
62
|
+
You have full access to read, write, edit files, and execute commands.
|
|
63
|
+
Do what has been asked; nothing more, nothing less.`;
|
package/src/ui/agent-widget.ts
CHANGED
|
@@ -12,6 +12,9 @@ import { getConfig } from "../agent-types.js";
|
|
|
12
12
|
|
|
13
13
|
// ---- Constants ----
|
|
14
14
|
|
|
15
|
+
/** Maximum number of rendered lines before overflow collapse kicks in. */
|
|
16
|
+
const MAX_WIDGET_LINES = 12;
|
|
17
|
+
|
|
15
18
|
/** Braille spinner frames for animated running indicator. */
|
|
16
19
|
export const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
17
20
|
|
|
@@ -77,11 +80,11 @@ export interface AgentDetails {
|
|
|
77
80
|
|
|
78
81
|
// ---- Formatting helpers ----
|
|
79
82
|
|
|
80
|
-
/** Format a token count
|
|
83
|
+
/** Format a token count compactly: "33.8k token", "1.2M token". */
|
|
81
84
|
export function formatTokens(count: number): string {
|
|
82
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M
|
|
83
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k
|
|
84
|
-
return `${count}
|
|
85
|
+
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M token`;
|
|
86
|
+
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k token`;
|
|
87
|
+
return `${count} token`;
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
/** Format milliseconds as human-readable duration. */
|
|
@@ -100,6 +103,12 @@ export function getDisplayName(type: SubagentType): string {
|
|
|
100
103
|
return getConfig(type).displayName;
|
|
101
104
|
}
|
|
102
105
|
|
|
106
|
+
/** Short label for prompt mode: "twin" for append, nothing for replace (the default). */
|
|
107
|
+
export function getPromptModeLabel(type: SubagentType): string | undefined {
|
|
108
|
+
const config = getConfig(type);
|
|
109
|
+
return config.promptMode === "append" ? "twin" : undefined;
|
|
110
|
+
}
|
|
111
|
+
|
|
103
112
|
/** Truncate text to a single line, max `len` chars. */
|
|
104
113
|
function truncateLine(text: string, len = 60): string {
|
|
105
114
|
const line = text.split("\n").find(l => l.trim())?.trim() ?? "";
|
|
@@ -193,6 +202,7 @@ export class AgentWidget {
|
|
|
193
202
|
/** Render a finished agent line. */
|
|
194
203
|
private renderFinishedLine(a: { type: SubagentType; status: string; description: string; toolUses: number; startedAt: number; completedAt?: number; error?: string }, theme: Theme): string {
|
|
195
204
|
const name = getDisplayName(a.type);
|
|
205
|
+
const modeLabel = getPromptModeLabel(a.type);
|
|
196
206
|
const duration = formatMs((a.completedAt ?? Date.now()) - a.startedAt);
|
|
197
207
|
|
|
198
208
|
let icon: string;
|
|
@@ -220,7 +230,8 @@ export class AgentWidget {
|
|
|
220
230
|
if (a.toolUses > 0) parts.push(`${a.toolUses} tool use${a.toolUses === 1 ? "" : "s"}`);
|
|
221
231
|
parts.push(duration);
|
|
222
232
|
|
|
223
|
-
|
|
233
|
+
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
234
|
+
return `${icon} ${theme.fg("dim", name)}${modeTag} ${theme.fg("dim", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", parts.join(" · "))}${statusText}`;
|
|
224
235
|
}
|
|
225
236
|
|
|
226
237
|
/** Force an immediate widget update. */
|
|
@@ -268,23 +279,20 @@ export class AgentWidget {
|
|
|
268
279
|
const truncate = (line: string) => truncateToWidth(line, w);
|
|
269
280
|
const headingColor = hasActive ? "accent" : "dim";
|
|
270
281
|
const headingIcon = hasActive ? "●" : "○";
|
|
271
|
-
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
|
|
272
282
|
|
|
273
|
-
//
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
283
|
+
// Build sections separately for overflow-aware assembly.
|
|
284
|
+
// Each running agent = 2 lines (header + activity), finished = 1 line, queued = 1 line.
|
|
285
|
+
|
|
286
|
+
const finishedLines: string[] = [];
|
|
287
|
+
for (const a of finished) {
|
|
288
|
+
finishedLines.push(truncate(theme.fg("dim", "├─") + " " + this.renderFinishedLine(a, theme)));
|
|
279
289
|
}
|
|
280
290
|
|
|
281
|
-
//
|
|
282
|
-
const
|
|
283
|
-
for (let i = 0; i < running.length; i++) {
|
|
284
|
-
const a = running[i];
|
|
285
|
-
const isLast = isLastSection && i === running.length - 1;
|
|
286
|
-
const connector = isLast ? "└─" : "├─";
|
|
291
|
+
const runningLines: string[][] = []; // each entry is [header, activity]
|
|
292
|
+
for (const a of running) {
|
|
287
293
|
const name = getDisplayName(a.type);
|
|
294
|
+
const modeLabel = getPromptModeLabel(a.type);
|
|
295
|
+
const modeTag = modeLabel ? ` ${theme.fg("dim", `(${modeLabel})`)}` : "";
|
|
288
296
|
const elapsed = formatMs(Date.now() - a.startedAt);
|
|
289
297
|
|
|
290
298
|
const bg = this.agentActivity.get(a.id);
|
|
@@ -302,14 +310,82 @@ export class AgentWidget {
|
|
|
302
310
|
|
|
303
311
|
const activity = bg ? describeActivity(bg.activeTools, bg.responseText) : "thinking…";
|
|
304
312
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
313
|
+
runningLines.push([
|
|
314
|
+
truncate(theme.fg("dim", "├─") + ` ${theme.fg("accent", frame)} ${theme.bold(name)}${modeTag} ${theme.fg("muted", a.description)} ${theme.fg("dim", "·")} ${theme.fg("dim", statsText)}`),
|
|
315
|
+
truncate(theme.fg("dim", "│ ") + theme.fg("dim", ` ⎿ ${activity}`)),
|
|
316
|
+
]);
|
|
308
317
|
}
|
|
309
318
|
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
319
|
+
const queuedLine = queued.length > 0
|
|
320
|
+
? truncate(theme.fg("dim", "├─") + ` ${theme.fg("muted", "◦")} ${theme.fg("dim", `${queued.length} queued`)}`)
|
|
321
|
+
: undefined;
|
|
322
|
+
|
|
323
|
+
// Assemble with overflow cap (heading + overflow indicator = 2 reserved lines).
|
|
324
|
+
const maxBody = MAX_WIDGET_LINES - 1; // heading takes 1 line
|
|
325
|
+
const totalBody = finishedLines.length + runningLines.length * 2 + (queuedLine ? 1 : 0);
|
|
326
|
+
|
|
327
|
+
const lines: string[] = [truncate(theme.fg(headingColor, headingIcon) + " " + theme.fg(headingColor, "Agents"))];
|
|
328
|
+
|
|
329
|
+
if (totalBody <= maxBody) {
|
|
330
|
+
// Everything fits — add all lines and fix up connectors for the last item.
|
|
331
|
+
lines.push(...finishedLines);
|
|
332
|
+
for (const pair of runningLines) lines.push(...pair);
|
|
333
|
+
if (queuedLine) lines.push(queuedLine);
|
|
334
|
+
|
|
335
|
+
// Fix last connector: swap ├─ → └─ and │ → space for activity lines.
|
|
336
|
+
if (lines.length > 1) {
|
|
337
|
+
const last = lines.length - 1;
|
|
338
|
+
lines[last] = lines[last].replace("├─", "└─");
|
|
339
|
+
// If last item is a running agent activity line, fix indent of that line
|
|
340
|
+
// and fix the header line above it.
|
|
341
|
+
if (runningLines.length > 0 && !queuedLine) {
|
|
342
|
+
// The last two lines are the last running agent's header + activity.
|
|
343
|
+
if (last >= 2) {
|
|
344
|
+
lines[last - 1] = lines[last - 1].replace("├─", "└─");
|
|
345
|
+
lines[last] = lines[last].replace("│ ", " ");
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
} else {
|
|
350
|
+
// Overflow — prioritize: running > queued > finished.
|
|
351
|
+
// Reserve 1 line for overflow indicator.
|
|
352
|
+
let budget = maxBody - 1;
|
|
353
|
+
let hiddenRunning = 0;
|
|
354
|
+
let hiddenFinished = 0;
|
|
355
|
+
|
|
356
|
+
// 1. Running agents (2 lines each)
|
|
357
|
+
for (const pair of runningLines) {
|
|
358
|
+
if (budget >= 2) {
|
|
359
|
+
lines.push(...pair);
|
|
360
|
+
budget -= 2;
|
|
361
|
+
} else {
|
|
362
|
+
hiddenRunning++;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
// 2. Queued line
|
|
367
|
+
if (queuedLine && budget >= 1) {
|
|
368
|
+
lines.push(queuedLine);
|
|
369
|
+
budget--;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// 3. Finished agents
|
|
373
|
+
for (const fl of finishedLines) {
|
|
374
|
+
if (budget >= 1) {
|
|
375
|
+
lines.push(fl);
|
|
376
|
+
budget--;
|
|
377
|
+
} else {
|
|
378
|
+
hiddenFinished++;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Overflow summary
|
|
383
|
+
const overflowParts: string[] = [];
|
|
384
|
+
if (hiddenRunning > 0) overflowParts.push(`${hiddenRunning} running`);
|
|
385
|
+
if (hiddenFinished > 0) overflowParts.push(`${hiddenFinished} finished`);
|
|
386
|
+
const overflowText = overflowParts.join(", ");
|
|
387
|
+
lines.push(truncate(theme.fg("dim", "└─") + ` ${theme.fg("dim", `+${hiddenRunning + hiddenFinished} more (${overflowText})`)}`)
|
|
388
|
+
);
|
|
313
389
|
}
|
|
314
390
|
|
|
315
391
|
return { render: () => lines, invalidate: () => {} };
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { matchesKey, truncateToWidth, visibleWidth, wrapTextWithAnsi, type Component, type TUI } from "@mariozechner/pi-tui";
|
|
9
9
|
import type { AgentSession } from "@mariozechner/pi-coding-agent";
|
|
10
10
|
import type { Theme } from "./agent-widget.js";
|
|
11
|
-
import { formatTokens, formatDuration, getDisplayName, describeActivity, type AgentActivity } from "./agent-widget.js";
|
|
11
|
+
import { formatTokens, formatDuration, getDisplayName, getPromptModeLabel, describeActivity, type AgentActivity } from "./agent-widget.js";
|
|
12
12
|
import type { AgentRecord } from "../types.js";
|
|
13
13
|
import { extractText } from "../context.js";
|
|
14
14
|
|
|
@@ -89,6 +89,8 @@ export class ConversationViewer implements Component {
|
|
|
89
89
|
// Header
|
|
90
90
|
lines.push(hrTop);
|
|
91
91
|
const name = getDisplayName(this.record.type);
|
|
92
|
+
const modeLabel = getPromptModeLabel(this.record.type);
|
|
93
|
+
const modeTag = modeLabel ? ` ${th.fg("dim", `(${modeLabel})`)}` : "";
|
|
92
94
|
const statusIcon = this.record.status === "running"
|
|
93
95
|
? th.fg("accent", "●")
|
|
94
96
|
: this.record.status === "completed"
|
|
@@ -109,7 +111,7 @@ export class ConversationViewer implements Component {
|
|
|
109
111
|
}
|
|
110
112
|
|
|
111
113
|
lines.push(row(
|
|
112
|
-
`${statusIcon} ${th.bold(name)} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
|
|
114
|
+
`${statusIcon} ${th.bold(name)}${modeTag} ${th.fg("muted", this.record.description)} ${th.fg("dim", "·")} ${th.fg("dim", headerParts.join(" · "))}`,
|
|
113
115
|
));
|
|
114
116
|
lines.push(hrMid);
|
|
115
117
|
|