@clanker-code/pi-subagents 0.10.8 → 0.11.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.
- package/AGENTS.md +2 -0
- package/CHANGELOG.md +17 -0
- package/README.md +22 -2
- package/bugs.txt +57 -0
- package/dist/agent-manager.d.ts +8 -0
- package/dist/agent-manager.js +54 -22
- package/dist/agent-runner.d.ts +3 -0
- package/dist/agent-runner.js +12 -3
- package/dist/dashboard-ui.d.ts +15 -0
- package/dist/dashboard-ui.js +206 -0
- package/dist/default-agents.js +0 -1
- package/dist/index.js +96 -11
- package/dist/peek.js +8 -2
- package/dist/subagent-list-clear.d.ts +57 -0
- package/dist/subagent-list-clear.js +331 -0
- package/dist/ui/agent-tool-rendering.js +1 -1
- package/dist/ui/agent-widget-tree.js +19 -2
- package/dist/ui/agent-widget.d.ts +7 -1
- package/dist/ui/agent-widget.js +52 -10
- package/package.json +1 -1
- package/src/agent-manager.ts +44 -13
- package/src/agent-runner.ts +14 -3
- package/src/dashboard-ui.ts +270 -0
- package/src/default-agents.ts +0 -1
- package/src/index.ts +113 -15
- package/src/peek.ts +7 -2
- package/src/subagent-list-clear.ts +405 -0
- package/src/ui/agent-tool-rendering.ts +1 -1
- package/src/ui/agent-widget-tree.ts +16 -2
- package/src/ui/agent-widget.ts +50 -10
package/dist/default-agents.js
CHANGED
|
@@ -30,7 +30,6 @@ export const DEFAULT_AGENTS = new Map([
|
|
|
30
30
|
builtinToolNames: READ_ONLY_TOOLS,
|
|
31
31
|
extensions: true,
|
|
32
32
|
skills: true,
|
|
33
|
-
model: "anthropic/claude-haiku-4-5-20251001",
|
|
34
33
|
systemPrompt: `# CRITICAL: READ-ONLY MODE - NO FILE MODIFICATIONS
|
|
35
34
|
You are a file search specialist. You excel at thoroughly navigating and exploring codebases.
|
|
36
35
|
Your role is EXCLUSIVELY to search and analyze existing code. You do NOT have access to file editing tools.
|
package/dist/index.js
CHANGED
|
@@ -23,8 +23,10 @@ import { getAgentConversation, getCurrentExtensionAgentId, getCurrentExtensionDe
|
|
|
23
23
|
import { buildAgentToolDescription, getModelLabelFromConfig } from "./agent-tool-description.js";
|
|
24
24
|
import { BUILTIN_TOOL_NAMES, getAgentConfig, getAllTypes, getAvailableTypes, isDefaultsDisabled, registerAgents, resolveType, setDefaultsDisabled } from "./agent-types.js";
|
|
25
25
|
import { formatOutputFileHint, limitText, MAX_RESULT_CHARS, MAX_VERBOSE_CHARS } from "./bounded-output.js";
|
|
26
|
+
import { extractText } from "./context.js";
|
|
26
27
|
import { registerRpcHandlers } from "./cross-extension-rpc.js";
|
|
27
28
|
import { loadCustomAgents } from "./custom-agents.js";
|
|
29
|
+
import { registerDashboardModules } from "./dashboard-ui.js";
|
|
28
30
|
import { isModelInScope, readEnabledModels, resolveEnabledModels } from "./enabled-models.js";
|
|
29
31
|
import { GroupJoinManager } from "./group-join.js";
|
|
30
32
|
import { resolveAgentInvocationConfig, resolveJoinMode } from "./invocation-config.js";
|
|
@@ -36,8 +38,9 @@ import { SubagentScheduler } from "./schedule.js";
|
|
|
36
38
|
import { resolveStorePath, ScheduleStore } from "./schedule-store.js";
|
|
37
39
|
import { applyAndEmitLoaded, DEFAULT_WAIT_TIMEOUT_SECONDS, saveAndEmitChanged } from "./settings.js";
|
|
38
40
|
import { getStatusNote } from "./status-note.js";
|
|
41
|
+
import { registerSubagentListClearTools } from "./subagent-list-clear.js";
|
|
39
42
|
import { MAX_RECURSIVE_DEPTH } from "./types.js";
|
|
40
|
-
import { renderAgentCall, renderAgentResult, renderSteerCall, tailPreview } from "./ui/agent-tool-rendering.js";
|
|
43
|
+
import { renderAgentCall, renderAgentResult, renderSteerCall, snipMiddleLines, tailPreview } from "./ui/agent-tool-rendering.js";
|
|
41
44
|
import { AgentWidget, buildInvocationTags, describeActivity, formatContextWindow, formatDuration, getDisplayName, getPromptModeLabel, } from "./ui/agent-widget.js";
|
|
42
45
|
import { menuSelect } from "./ui/menu-select.js";
|
|
43
46
|
import { showSchedulesMenu } from "./ui/schedule-menu.js";
|
|
@@ -388,6 +391,8 @@ export default function (pi) {
|
|
|
388
391
|
// Capture ctx from session_start for RPC spawn handler + start the scheduler.
|
|
389
392
|
pi.on("session_start", async (_event, ctx) => {
|
|
390
393
|
currentCtx = ctx;
|
|
394
|
+
clearBatchState();
|
|
395
|
+
groupJoin.dispose();
|
|
391
396
|
manager.clearCompleted();
|
|
392
397
|
widget.clearSnapshots();
|
|
393
398
|
retryStash.clear();
|
|
@@ -395,8 +400,10 @@ export default function (pi) {
|
|
|
395
400
|
startScheduler(ctx);
|
|
396
401
|
});
|
|
397
402
|
pi.on("session_before_switch", () => {
|
|
403
|
+
clearBatchState();
|
|
404
|
+
groupJoin.dispose();
|
|
398
405
|
manager.clearCompleted();
|
|
399
|
-
widget.
|
|
406
|
+
widget.dispose();
|
|
400
407
|
retryStash.clear();
|
|
401
408
|
scheduler.stop();
|
|
402
409
|
});
|
|
@@ -414,13 +421,17 @@ export default function (pi) {
|
|
|
414
421
|
unsubSpawnRpc();
|
|
415
422
|
unsubStopRpc();
|
|
416
423
|
unsubPingRpc();
|
|
424
|
+
unsubWidgetCreated?.();
|
|
417
425
|
unsubWidgetStarted?.();
|
|
418
426
|
unsubWidgetCompleted?.();
|
|
419
427
|
unsubWidgetFailed?.();
|
|
420
428
|
currentCtx = undefined;
|
|
421
429
|
delete globalThis[MANAGER_KEY];
|
|
422
430
|
scheduler.stop();
|
|
431
|
+
clearBatchState();
|
|
432
|
+
groupJoin.dispose();
|
|
423
433
|
manager.abortAll();
|
|
434
|
+
widget.dispose();
|
|
424
435
|
for (const timer of pendingNudges.values())
|
|
425
436
|
clearTimeout(timer);
|
|
426
437
|
pendingNudges.clear();
|
|
@@ -434,6 +445,7 @@ export default function (pi) {
|
|
|
434
445
|
if (snapshot)
|
|
435
446
|
widget.upsertSnapshot(snapshot);
|
|
436
447
|
};
|
|
448
|
+
const unsubWidgetCreated = pi.events.on("subagents:created", upsertWidgetEventSnapshot);
|
|
437
449
|
const unsubWidgetStarted = pi.events.on("subagents:started", upsertWidgetEventSnapshot);
|
|
438
450
|
const unsubWidgetCompleted = pi.events.on("subagents:completed", upsertWidgetEventSnapshot);
|
|
439
451
|
const unsubWidgetFailed = pi.events.on("subagents:failed", upsertWidgetEventSnapshot);
|
|
@@ -501,6 +513,13 @@ export default function (pi) {
|
|
|
501
513
|
let currentBatchAgents = [];
|
|
502
514
|
let batchFinalizeTimer;
|
|
503
515
|
let batchCounter = 0;
|
|
516
|
+
function clearBatchState() {
|
|
517
|
+
if (batchFinalizeTimer) {
|
|
518
|
+
clearTimeout(batchFinalizeTimer);
|
|
519
|
+
batchFinalizeTimer = undefined;
|
|
520
|
+
}
|
|
521
|
+
currentBatchAgents = [];
|
|
522
|
+
}
|
|
504
523
|
/** Finalize the current batch: if 2+ smart-mode agents, register as a group. */
|
|
505
524
|
function finalizeBatch() {
|
|
506
525
|
batchFinalizeTimer = undefined;
|
|
@@ -597,7 +616,7 @@ export default function (pi) {
|
|
|
597
616
|
description: "A short (3-5 word) description of the task (shown in UI).",
|
|
598
617
|
}),
|
|
599
618
|
subagent_type: Type.Optional(Type.String({
|
|
600
|
-
description: `The type of specialized agent to use. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
|
|
619
|
+
description: `The type of specialized agent to use. Defaults to general-purpose when omitted. Available types: ${getAvailableTypes().join(", ")}. Custom agents from .pi/agents/*.md (project) or ${getAgentDir()}/agents/*.md (global) are also available. OMIT when retrying (preserved by the handle) unless you want to override it.`,
|
|
601
620
|
})),
|
|
602
621
|
model: Type.Optional(Type.String({
|
|
603
622
|
description: 'Optional model override. Accepts "provider/modelId" or fuzzy name (e.g. "haiku", "sonnet"). Omit to use the agent type\'s default.',
|
|
@@ -673,14 +692,14 @@ export default function (pi) {
|
|
|
673
692
|
const { retry: _omit, ...overrides } = params;
|
|
674
693
|
P = { ...stashed.params, ...overrides };
|
|
675
694
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
695
|
+
const requestedSubagentType = (P.subagent_type ?? "general-purpose");
|
|
696
|
+
emitPromptPreview(P.prompt, P.description, requestedSubagentType);
|
|
697
|
+
// Retry supplied the prompt from the stash; otherwise prompt is required.
|
|
698
|
+
// subagent_type defaults to general-purpose when omitted.
|
|
699
|
+
if (!retryHandle && !P.prompt) {
|
|
700
|
+
return textResult("Missing required argument: prompt.");
|
|
682
701
|
}
|
|
683
|
-
const rawType =
|
|
702
|
+
const rawType = requestedSubagentType;
|
|
684
703
|
const resolved = resolveType(rawType);
|
|
685
704
|
if (!resolved) {
|
|
686
705
|
// Unknown agent type — recoverable. List valid types so the orchestrator
|
|
@@ -896,6 +915,10 @@ export default function (pi) {
|
|
|
896
915
|
isBackground: true,
|
|
897
916
|
depth: record?.depth ?? nextSubagentDepth,
|
|
898
917
|
parentAgentId: extensionAgentId,
|
|
918
|
+
status: record?.status ?? "running",
|
|
919
|
+
startedAt: record?.startedAt,
|
|
920
|
+
toolUses: record?.toolUses ?? 0,
|
|
921
|
+
invocation: record?.invocation,
|
|
899
922
|
});
|
|
900
923
|
const isQueued = record?.status === "queued";
|
|
901
924
|
return textResult(`Agent ${isQueued ? "queued" : "started"} in background.\nAgent ID: ${id}\nType: ${displayName}\nDescription: ${P.description}\n` +
|
|
@@ -928,6 +951,56 @@ export default function (pi) {
|
|
|
928
951
|
description: "Return a lightweight tail/filter view of the agent's result or live output file, with line numbers. Ignored when verbose is true.",
|
|
929
952
|
})),
|
|
930
953
|
}),
|
|
954
|
+
renderResult(result, { expanded }, theme) {
|
|
955
|
+
const details = result.details;
|
|
956
|
+
const text = extractText(result.content);
|
|
957
|
+
// Header: status + stats + description
|
|
958
|
+
let line = "";
|
|
959
|
+
if (details) {
|
|
960
|
+
const icon = details.status === "error" || details.status === "stopped" || details.status === "aborted"
|
|
961
|
+
? theme.fg("error", "✗")
|
|
962
|
+
: details.status === "running" || details.status === "queued"
|
|
963
|
+
? theme.fg("accent", "◌")
|
|
964
|
+
: theme.fg("success", "✓");
|
|
965
|
+
const parts = [];
|
|
966
|
+
if (details.toolUses > 0)
|
|
967
|
+
parts.push(`${details.toolUses} tool use${details.toolUses === 1 ? "" : "s"}`);
|
|
968
|
+
if (details.tokens)
|
|
969
|
+
parts.push(details.tokens);
|
|
970
|
+
if (details.contextPercent !== null)
|
|
971
|
+
parts.push(`ctx ${Math.round(details.contextPercent)}%`);
|
|
972
|
+
if (details.duration)
|
|
973
|
+
parts.push(details.duration);
|
|
974
|
+
const stats = parts.map(p => theme.fg("dim", p)).join(" " + theme.fg("dim", "·") + " ");
|
|
975
|
+
line = `${icon} ${theme.bold(details.description)} ${theme.fg("dim", details.status)}`;
|
|
976
|
+
if (stats)
|
|
977
|
+
line += "\n " + stats;
|
|
978
|
+
}
|
|
979
|
+
// Body: snip when collapsed, full when expanded
|
|
980
|
+
// Extract the body portion (after the first blank line) to keep the
|
|
981
|
+
// tool-output header always visible and only snip the actual result.
|
|
982
|
+
if (text.trim()) {
|
|
983
|
+
const firstBlank = text.indexOf("\n\n");
|
|
984
|
+
const body = firstBlank >= 0 ? text.slice(firstBlank + 2) : text;
|
|
985
|
+
if (expanded) {
|
|
986
|
+
for (const l of text.split("\n")) {
|
|
987
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
else {
|
|
991
|
+
// Show the tool-output header verbatim, then snip only the body
|
|
992
|
+
if (firstBlank >= 0) {
|
|
993
|
+
for (const l of text.slice(0, firstBlank).split("\n")) {
|
|
994
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
for (const l of snipMiddleLines(body, 20)) {
|
|
998
|
+
line += "\n" + theme.fg("dim", ` ${l}`);
|
|
999
|
+
}
|
|
1000
|
+
}
|
|
1001
|
+
}
|
|
1002
|
+
return new Text(line, 0, 0);
|
|
1003
|
+
},
|
|
931
1004
|
execute: async (_toolCallId, params, signal, _onUpdate, ctx) => {
|
|
932
1005
|
const record = manager.getRecord(params.agent_id);
|
|
933
1006
|
if (!record) {
|
|
@@ -974,6 +1047,15 @@ export default function (pi) {
|
|
|
974
1047
|
if (record.compactionCount)
|
|
975
1048
|
statsParts.push(`Compactions: ${record.compactionCount}`);
|
|
976
1049
|
statsParts.push(`Duration: ${duration}`);
|
|
1050
|
+
const details = {
|
|
1051
|
+
status: record.status,
|
|
1052
|
+
description: record.description,
|
|
1053
|
+
toolUses: record.toolUses,
|
|
1054
|
+
tokens: tokens || null,
|
|
1055
|
+
contextPercent,
|
|
1056
|
+
duration,
|
|
1057
|
+
outputFile: record.outputFile,
|
|
1058
|
+
};
|
|
977
1059
|
let output = `Agent: ${record.id}\n` +
|
|
978
1060
|
`Type: ${displayName} | Status: ${record.status}${getStatusNote(record.status)} | ${statsParts.join(" | ")}\n` +
|
|
979
1061
|
`Description: ${record.description}\n` +
|
|
@@ -1014,7 +1096,7 @@ export default function (pi) {
|
|
|
1014
1096
|
}
|
|
1015
1097
|
}
|
|
1016
1098
|
}
|
|
1017
|
-
return textResult(output);
|
|
1099
|
+
return textResult(output, details);
|
|
1018
1100
|
},
|
|
1019
1101
|
}));
|
|
1020
1102
|
// ---- steer_subagent tool ----
|
|
@@ -1072,6 +1154,9 @@ export default function (pi) {
|
|
|
1072
1154
|
}
|
|
1073
1155
|
},
|
|
1074
1156
|
}));
|
|
1157
|
+
// ---- list_subagents / clear_subagents tools ----
|
|
1158
|
+
registerSubagentListClearTools(pi, manager);
|
|
1159
|
+
registerDashboardModules(pi, manager);
|
|
1075
1160
|
// ---- list_models tool ----
|
|
1076
1161
|
pi.registerTool(defineTool({
|
|
1077
1162
|
name: SUBAGENT_TOOL_NAMES.LIST_MODELS,
|
package/dist/peek.js
CHANGED
|
@@ -81,6 +81,12 @@ function parseOutputFileLines(path) {
|
|
|
81
81
|
return [];
|
|
82
82
|
}
|
|
83
83
|
const out = [];
|
|
84
|
+
const pushRenderedLines = (text) => {
|
|
85
|
+
for (const renderedLine of text.trimEnd().split("\n")) {
|
|
86
|
+
if (renderedLine.trim())
|
|
87
|
+
out.push(renderedLine);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
84
90
|
for (const line of raw.split("\n")) {
|
|
85
91
|
const trimmed = line.trim();
|
|
86
92
|
if (!trimmed)
|
|
@@ -96,12 +102,12 @@ function parseOutputFileLines(path) {
|
|
|
96
102
|
if (!Array.isArray(content)) {
|
|
97
103
|
// Some entries may carry a plain string content.
|
|
98
104
|
if (typeof content === "string" && content.trim())
|
|
99
|
-
|
|
105
|
+
pushRenderedLines(content);
|
|
100
106
|
continue;
|
|
101
107
|
}
|
|
102
108
|
for (const block of content) {
|
|
103
109
|
if (block?.type === "text" && typeof block.text === "string" && block.text.trim()) {
|
|
104
|
-
|
|
110
|
+
pushRenderedLines(block.text);
|
|
105
111
|
}
|
|
106
112
|
}
|
|
107
113
|
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { type ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import type { AgentManager } from "./agent-manager.js";
|
|
4
|
+
import type { AgentRecord } from "./types.js";
|
|
5
|
+
export interface ListSubagentsOptions {
|
|
6
|
+
all?: boolean;
|
|
7
|
+
now?: number;
|
|
8
|
+
recentSuccessLimit?: number;
|
|
9
|
+
}
|
|
10
|
+
export interface ListSubagentsAgentDetails {
|
|
11
|
+
id: string;
|
|
12
|
+
type: AgentRecord["type"];
|
|
13
|
+
description: string;
|
|
14
|
+
status: AgentRecord["status"];
|
|
15
|
+
startedAt: number;
|
|
16
|
+
completedAt?: number;
|
|
17
|
+
}
|
|
18
|
+
export interface ListSubagentsDetails {
|
|
19
|
+
total: number;
|
|
20
|
+
all: boolean;
|
|
21
|
+
visible: ListSubagentsAgentDetails[];
|
|
22
|
+
hiddenDoneCount: number;
|
|
23
|
+
activeCount: number;
|
|
24
|
+
problemCount: number;
|
|
25
|
+
recentDoneCount: number;
|
|
26
|
+
now: number;
|
|
27
|
+
}
|
|
28
|
+
export interface ClearSubagentsOptions {
|
|
29
|
+
agentIds?: string[];
|
|
30
|
+
now?: number;
|
|
31
|
+
olderThanMs?: number;
|
|
32
|
+
includeErrors?: boolean;
|
|
33
|
+
}
|
|
34
|
+
export interface ClearSelectionResult {
|
|
35
|
+
clearIds: string[];
|
|
36
|
+
errors: string[];
|
|
37
|
+
requestedCount: number;
|
|
38
|
+
keptActiveCount: number;
|
|
39
|
+
keptFailedCount: number;
|
|
40
|
+
keptYoungSuccessCount: number;
|
|
41
|
+
}
|
|
42
|
+
export interface ClearSubagentsDetails extends ClearSelectionResult {
|
|
43
|
+
clearedCount: number;
|
|
44
|
+
}
|
|
45
|
+
export type RenderTheme = {
|
|
46
|
+
fg(color: string, text: string): string;
|
|
47
|
+
bold(text: string): string;
|
|
48
|
+
};
|
|
49
|
+
export declare function buildListSubagentsDetails(records: AgentRecord[], options?: ListSubagentsOptions): ListSubagentsDetails;
|
|
50
|
+
export declare function clearSubagentRecords(records: AgentRecord[], options?: ClearSubagentsOptions): ClearSelectionResult;
|
|
51
|
+
export declare function buildClearSubagentsDetails(result: ClearSelectionResult): ClearSubagentsDetails;
|
|
52
|
+
export declare function renderListSubagentsDetails(details: ListSubagentsDetails, theme: RenderTheme): Component;
|
|
53
|
+
export declare function renderClearSubagentsDetails(details: ClearSubagentsDetails, theme: RenderTheme): Component;
|
|
54
|
+
export declare function renderEmptyCall(): Component;
|
|
55
|
+
export declare function formatListSubagentsText(details: ListSubagentsDetails): string;
|
|
56
|
+
export declare function formatClearSubagentsText(details: ClearSubagentsDetails): string;
|
|
57
|
+
export declare function registerSubagentListClearTools(pi: ExtensionAPI, manager: AgentManager): void;
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { defineTool } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
4
|
+
import { SUBAGENT_TOOL_NAMES } from "./agent-runner.js";
|
|
5
|
+
const DEFAULT_RECENT_SUCCESS_LIMIT = 2;
|
|
6
|
+
const DEFAULT_CLEAR_AGE_MS = 5 * 60_000;
|
|
7
|
+
const INDENT = " ";
|
|
8
|
+
const HEADER_GLYPH = "⏣";
|
|
9
|
+
const TREE_MID = "├─";
|
|
10
|
+
const TREE_END = "└─";
|
|
11
|
+
const TREE_GAP = " ";
|
|
12
|
+
const TREE_PREFIX_LEN = INDENT.length + TREE_MID.length + TREE_GAP.length;
|
|
13
|
+
const SUCCESS_STATUSES = new Set(["completed", "steered"]);
|
|
14
|
+
const ACTIVE_STATUSES = new Set(["running", "queued"]);
|
|
15
|
+
const PROBLEM_STATUSES = new Set(["error", "aborted", "stopped"]);
|
|
16
|
+
class LineListComponent {
|
|
17
|
+
getLines;
|
|
18
|
+
constructor(getLines) {
|
|
19
|
+
this.getLines = getLines;
|
|
20
|
+
}
|
|
21
|
+
render(width) { return this.getLines(width); }
|
|
22
|
+
invalidate() { }
|
|
23
|
+
}
|
|
24
|
+
function isActive(record) {
|
|
25
|
+
return ACTIVE_STATUSES.has(record.status);
|
|
26
|
+
}
|
|
27
|
+
function isSuccess(record) {
|
|
28
|
+
return SUCCESS_STATUSES.has(record.status);
|
|
29
|
+
}
|
|
30
|
+
function isProblem(record) {
|
|
31
|
+
return PROBLEM_STATUSES.has(record.status);
|
|
32
|
+
}
|
|
33
|
+
function recency(record) {
|
|
34
|
+
return record.completedAt ?? record.startedAt;
|
|
35
|
+
}
|
|
36
|
+
function newestFirst(a, b) {
|
|
37
|
+
return recency(b) - recency(a);
|
|
38
|
+
}
|
|
39
|
+
function plural(n, one, many = `${one}s`) {
|
|
40
|
+
return `${n} ${n === 1 ? one : many}`;
|
|
41
|
+
}
|
|
42
|
+
function toListSubagentsAgentDetails(record) {
|
|
43
|
+
const details = {
|
|
44
|
+
id: record.id,
|
|
45
|
+
type: record.type,
|
|
46
|
+
description: record.description,
|
|
47
|
+
status: record.status,
|
|
48
|
+
startedAt: record.startedAt,
|
|
49
|
+
};
|
|
50
|
+
if (record.completedAt !== undefined)
|
|
51
|
+
details.completedAt = record.completedAt;
|
|
52
|
+
return details;
|
|
53
|
+
}
|
|
54
|
+
function formatAge(record, now) {
|
|
55
|
+
const start = record.completedAt ?? record.startedAt;
|
|
56
|
+
const elapsed = Math.max(0, now - start);
|
|
57
|
+
if (elapsed < 1_000)
|
|
58
|
+
return "0s";
|
|
59
|
+
const seconds = Math.floor(elapsed / 1_000);
|
|
60
|
+
if (seconds < 60)
|
|
61
|
+
return `${seconds}s`;
|
|
62
|
+
const minutes = Math.floor(seconds / 60);
|
|
63
|
+
if (minutes < 60)
|
|
64
|
+
return `${minutes}m`;
|
|
65
|
+
const hours = Math.floor(minutes / 60);
|
|
66
|
+
if (hours < 48)
|
|
67
|
+
return `${hours}h`;
|
|
68
|
+
return `${Math.floor(hours / 24)}d`;
|
|
69
|
+
}
|
|
70
|
+
function pad(text, width) {
|
|
71
|
+
return text.length >= width ? text : text + " ".repeat(width - text.length);
|
|
72
|
+
}
|
|
73
|
+
function shortId(id) {
|
|
74
|
+
return id.slice(0, 8);
|
|
75
|
+
}
|
|
76
|
+
function displayType(type) {
|
|
77
|
+
return type === "general-purpose" ? "general" : type;
|
|
78
|
+
}
|
|
79
|
+
function statusLabel(status) {
|
|
80
|
+
if (status === "completed")
|
|
81
|
+
return "done";
|
|
82
|
+
if (status === "steered")
|
|
83
|
+
return "done";
|
|
84
|
+
return status;
|
|
85
|
+
}
|
|
86
|
+
function statusIcon(status) {
|
|
87
|
+
switch (status) {
|
|
88
|
+
case "running": return "⠋";
|
|
89
|
+
case "queued": return "◼";
|
|
90
|
+
case "completed":
|
|
91
|
+
case "steered": return "✓";
|
|
92
|
+
case "error":
|
|
93
|
+
case "aborted": return "✗";
|
|
94
|
+
case "stopped": return "■";
|
|
95
|
+
default: return "•";
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
function statusColor(status) {
|
|
99
|
+
switch (status) {
|
|
100
|
+
case "running":
|
|
101
|
+
case "queued": return "accent";
|
|
102
|
+
case "completed":
|
|
103
|
+
case "steered": return "success";
|
|
104
|
+
case "stopped": return "dim";
|
|
105
|
+
default: return "error";
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function labeledPrefix(label, theme) {
|
|
109
|
+
return `${INDENT}${theme.fg("accent", HEADER_GLYPH)} ${theme.fg("accent", label)}${theme.fg("dim", " · ")}`;
|
|
110
|
+
}
|
|
111
|
+
function normalizeIds(agentIds) {
|
|
112
|
+
return [...new Set((agentIds ?? []).map((id) => id.trim()).filter(Boolean))];
|
|
113
|
+
}
|
|
114
|
+
function resolveId(records, query) {
|
|
115
|
+
const exact = records.find((record) => record.id === query);
|
|
116
|
+
if (exact)
|
|
117
|
+
return { id: exact.id };
|
|
118
|
+
const matches = records.filter((record) => record.id.startsWith(query));
|
|
119
|
+
if (matches.length === 0)
|
|
120
|
+
return { error: `${query} not found` };
|
|
121
|
+
if (matches.length > 1)
|
|
122
|
+
return { error: `${query} matched multiple agents: ${matches.map((r) => shortId(r.id)).join(", ")}` };
|
|
123
|
+
return { id: matches[0].id };
|
|
124
|
+
}
|
|
125
|
+
export function buildListSubagentsDetails(records, options = {}) {
|
|
126
|
+
const now = options.now ?? Date.now();
|
|
127
|
+
const all = options.all === true;
|
|
128
|
+
const sorted = [...records].sort(newestFirst);
|
|
129
|
+
const active = sorted.filter(isActive);
|
|
130
|
+
const problems = sorted.filter(isProblem);
|
|
131
|
+
const successes = sorted.filter(isSuccess);
|
|
132
|
+
const recentSuccessLimit = options.recentSuccessLimit ?? DEFAULT_RECENT_SUCCESS_LIMIT;
|
|
133
|
+
const recentSuccesses = successes.slice(0, recentSuccessLimit);
|
|
134
|
+
const visible = all ? sorted : [...active, ...problems, ...recentSuccesses];
|
|
135
|
+
return {
|
|
136
|
+
total: records.length,
|
|
137
|
+
all,
|
|
138
|
+
visible: visible.map(toListSubagentsAgentDetails),
|
|
139
|
+
hiddenDoneCount: all ? 0 : Math.max(0, successes.length - recentSuccesses.length),
|
|
140
|
+
activeCount: active.length,
|
|
141
|
+
problemCount: problems.length,
|
|
142
|
+
recentDoneCount: all ? successes.length : recentSuccesses.length,
|
|
143
|
+
now,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
export function clearSubagentRecords(records, options = {}) {
|
|
147
|
+
const now = options.now ?? Date.now();
|
|
148
|
+
const olderThanMs = options.olderThanMs ?? DEFAULT_CLEAR_AGE_MS;
|
|
149
|
+
const requestedIds = normalizeIds(options.agentIds);
|
|
150
|
+
const errors = [];
|
|
151
|
+
const clearIds = [];
|
|
152
|
+
if (requestedIds.length > 0) {
|
|
153
|
+
for (const query of requestedIds) {
|
|
154
|
+
const resolved = resolveId(records, query);
|
|
155
|
+
if (resolved.error || !resolved.id) {
|
|
156
|
+
errors.push(resolved.error ?? `${query} not found`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const record = records.find((r) => r.id === resolved.id);
|
|
160
|
+
if (isActive(record)) {
|
|
161
|
+
errors.push(`${query} matched ${record.status} agent ${record.id}`);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
clearIds.push(record.id);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
for (const record of records) {
|
|
169
|
+
const age = now - (record.completedAt ?? record.startedAt);
|
|
170
|
+
if (age < olderThanMs)
|
|
171
|
+
continue;
|
|
172
|
+
if (isSuccess(record) || (options.includeErrors && isProblem(record))) {
|
|
173
|
+
clearIds.push(record.id);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
const clearIdSet = new Set(clearIds);
|
|
178
|
+
const remaining = records.filter((record) => !clearIdSet.has(record.id));
|
|
179
|
+
const keptActiveCount = remaining.filter(isActive).length;
|
|
180
|
+
const keptFailedCount = remaining.filter(isProblem).length;
|
|
181
|
+
const keptYoungSuccessCount = remaining.filter((record) => isSuccess(record) && now - (record.completedAt ?? record.startedAt) < olderThanMs).length;
|
|
182
|
+
return { clearIds, errors, requestedCount: requestedIds.length, keptActiveCount, keptFailedCount, keptYoungSuccessCount };
|
|
183
|
+
}
|
|
184
|
+
export function buildClearSubagentsDetails(result) {
|
|
185
|
+
return { ...result, clearedCount: result.clearIds.length };
|
|
186
|
+
}
|
|
187
|
+
function renderAgentLine(record, theme, now) {
|
|
188
|
+
const icon = theme.fg(statusColor(record.status), statusIcon(record.status));
|
|
189
|
+
const id = theme.fg("muted", shortId(record.id));
|
|
190
|
+
const type = theme.fg("text", pad(displayType(record.type), 8));
|
|
191
|
+
const status = theme.fg(statusColor(record.status), pad(statusLabel(record.status), 8));
|
|
192
|
+
const age = theme.fg("dim", pad(formatAge(record, now), 4));
|
|
193
|
+
return `${icon} ${id} ${type} ${status} ${age} ${theme.fg("muted", record.description)}`;
|
|
194
|
+
}
|
|
195
|
+
function listSummary(details, theme) {
|
|
196
|
+
if (details.total === 0)
|
|
197
|
+
return `${theme.fg("text", "0 visible")} ${theme.fg("dim", "(empty)")}`;
|
|
198
|
+
if (details.all)
|
|
199
|
+
return `${theme.fg("text", plural(details.visible.length, "agent"))} ${theme.fg("dim", "(full list)")}`;
|
|
200
|
+
const parts = [
|
|
201
|
+
plural(details.activeCount, "active"),
|
|
202
|
+
plural(details.problemCount, "problem"),
|
|
203
|
+
`${plural(details.recentDoneCount, "recent done", "recent done")}`,
|
|
204
|
+
];
|
|
205
|
+
const hidden = details.hiddenDoneCount > 0 ? `; ${plural(details.hiddenDoneCount, "hidden done", "hidden done")}` : "";
|
|
206
|
+
return `${theme.fg("text", `${details.visible.length} visible`)} ${theme.fg("dim", `(${parts.join(", ")}${hidden})`)}`;
|
|
207
|
+
}
|
|
208
|
+
export function renderListSubagentsDetails(details, theme) {
|
|
209
|
+
return new LineListComponent((width) => {
|
|
210
|
+
const header = `${labeledPrefix("List Agents", theme)}${listSummary(details, theme)}`;
|
|
211
|
+
if (details.visible.length === 0)
|
|
212
|
+
return [truncateToWidth(header, width)];
|
|
213
|
+
const lines = [truncateToWidth(header, width)];
|
|
214
|
+
const avail = Math.max(1, width - TREE_PREFIX_LEN);
|
|
215
|
+
details.visible.forEach((record, i) => {
|
|
216
|
+
const connector = i === details.visible.length - 1 ? TREE_END : TREE_MID;
|
|
217
|
+
const line = truncateToWidth(renderAgentLine(record, theme, details.now), avail);
|
|
218
|
+
lines.push(`${INDENT}${connector}${TREE_GAP}${line}`);
|
|
219
|
+
});
|
|
220
|
+
return lines;
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
function clearSummary(details, theme) {
|
|
224
|
+
const primary = theme.fg("text", `cleared ${plural(details.clearedCount, "record")}`);
|
|
225
|
+
const extra = [];
|
|
226
|
+
if (details.keptYoungSuccessCount)
|
|
227
|
+
extra.push(`${plural(details.keptYoungSuccessCount, "new done", "new done")} kept`);
|
|
228
|
+
if (details.keptFailedCount)
|
|
229
|
+
extra.push(`${plural(details.keptFailedCount, "failed", "failed")} kept`);
|
|
230
|
+
if (details.keptActiveCount)
|
|
231
|
+
extra.push(`${plural(details.keptActiveCount, "active", "active")} kept`);
|
|
232
|
+
if (details.errors.length)
|
|
233
|
+
extra.push(theme.fg("error", `${plural(details.errors.length, "error")}`));
|
|
234
|
+
if (extra.length === 0)
|
|
235
|
+
return primary;
|
|
236
|
+
return `${primary}${theme.fg("dim", " (")}${extra.join(theme.fg("dim", ", "))}${theme.fg("dim", ")")}`;
|
|
237
|
+
}
|
|
238
|
+
export function renderClearSubagentsDetails(details, theme) {
|
|
239
|
+
return new LineListComponent((width) => [truncateToWidth(`${labeledPrefix("Clear Agents", theme)}${clearSummary(details, theme)}`, width)]);
|
|
240
|
+
}
|
|
241
|
+
export function renderEmptyCall() {
|
|
242
|
+
return new LineListComponent(() => []);
|
|
243
|
+
}
|
|
244
|
+
export function formatListSubagentsText(details) {
|
|
245
|
+
const lines = [`${details.visible.length} visible of ${details.total} retained subagents.`];
|
|
246
|
+
for (const record of details.visible) {
|
|
247
|
+
lines.push(`${record.id} | ${displayType(record.type)} | ${record.status} | ${record.description}`);
|
|
248
|
+
}
|
|
249
|
+
if (details.hiddenDoneCount > 0) {
|
|
250
|
+
lines.push(`${details.hiddenDoneCount} successful completed subagent(s) hidden. Pass all: true for the full retained list.`);
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n");
|
|
253
|
+
}
|
|
254
|
+
export function formatClearSubagentsText(details) {
|
|
255
|
+
const lines = [`Cleared ${details.clearedCount} subagent record(s).`];
|
|
256
|
+
if (details.clearIds.length)
|
|
257
|
+
lines.push(`Cleared IDs: ${details.clearIds.join(", ")}`);
|
|
258
|
+
if (details.keptYoungSuccessCount)
|
|
259
|
+
lines.push(`Kept ${details.keptYoungSuccessCount} successful subagent(s) newer than the age threshold.`);
|
|
260
|
+
if (details.keptFailedCount)
|
|
261
|
+
lines.push(`Kept ${details.keptFailedCount} failed/stopped/aborted subagent(s).`);
|
|
262
|
+
if (details.keptActiveCount)
|
|
263
|
+
lines.push(`Kept ${details.keptActiveCount} active subagent(s).`);
|
|
264
|
+
if (details.errors.length)
|
|
265
|
+
lines.push(`Errors: ${details.errors.join("; ")}`);
|
|
266
|
+
return lines.join("\n");
|
|
267
|
+
}
|
|
268
|
+
function textResult(msg, details) {
|
|
269
|
+
return { content: [{ type: "text", text: msg }], details: details };
|
|
270
|
+
}
|
|
271
|
+
export function registerSubagentListClearTools(pi, manager) {
|
|
272
|
+
pi.registerTool(defineTool({
|
|
273
|
+
name: SUBAGENT_TOOL_NAMES.LIST_SUBAGENTS,
|
|
274
|
+
label: "List Agents",
|
|
275
|
+
description: "List retained subagent records. By default shows queued/running agents, failed/stopped/aborted agents, " +
|
|
276
|
+
"and the most recent 2 successful agents that have not been cleaned up. Also reports how many successful " +
|
|
277
|
+
"completed agents are hidden. Pass all: true to show the full retained list.",
|
|
278
|
+
promptSnippet: "List retained subagents and their current status",
|
|
279
|
+
parameters: Type.Object({
|
|
280
|
+
all: Type.Optional(Type.Boolean({
|
|
281
|
+
description: "If true, show every retained subagent record instead of the default compact view.",
|
|
282
|
+
})),
|
|
283
|
+
}),
|
|
284
|
+
renderShell: "self",
|
|
285
|
+
renderCall: () => renderEmptyCall(),
|
|
286
|
+
renderResult: (result, _options, theme) => {
|
|
287
|
+
const details = result?.details;
|
|
288
|
+
return details ? renderListSubagentsDetails(details, theme) : renderEmptyCall();
|
|
289
|
+
},
|
|
290
|
+
execute: async (_toolCallId, params) => {
|
|
291
|
+
const details = buildListSubagentsDetails(manager.listAgents(), { all: params.all === true });
|
|
292
|
+
return textResult(formatListSubagentsText(details), details);
|
|
293
|
+
},
|
|
294
|
+
}));
|
|
295
|
+
pi.registerTool(defineTool({
|
|
296
|
+
name: SUBAGENT_TOOL_NAMES.CLEAR_SUBAGENTS,
|
|
297
|
+
label: "Clear Agents",
|
|
298
|
+
description: "Clear retained terminal subagent records. By default clears successful completed/steered subagents older than 5 minutes. " +
|
|
299
|
+
"Provide agent_ids to clear specific terminal subagents by exact ID or unique prefix. Running and queued subagents are never cleared; " +
|
|
300
|
+
"specific attempts to clear them are reported as errors.",
|
|
301
|
+
promptSnippet: "Clear completed subagent records that are still retained",
|
|
302
|
+
parameters: Type.Object({
|
|
303
|
+
agent_ids: Type.Optional(Type.Array(Type.String(), {
|
|
304
|
+
description: "Optional exact IDs or unique prefixes to clear. When provided, the age threshold is ignored for those IDs.",
|
|
305
|
+
})),
|
|
306
|
+
older_than_minutes: Type.Optional(Type.Number({
|
|
307
|
+
description: "Default-mode age threshold in minutes. Defaults to 5. Ignored when agent_ids is provided.",
|
|
308
|
+
minimum: 0,
|
|
309
|
+
})),
|
|
310
|
+
include_errors: Type.Optional(Type.Boolean({
|
|
311
|
+
description: "In default mode, also clear failed/stopped/aborted terminal records older than the age threshold. Default false.",
|
|
312
|
+
})),
|
|
313
|
+
}),
|
|
314
|
+
renderShell: "self",
|
|
315
|
+
renderCall: () => renderEmptyCall(),
|
|
316
|
+
renderResult: (result, _options, theme) => {
|
|
317
|
+
const details = result?.details;
|
|
318
|
+
return details ? renderClearSubagentsDetails(details, theme) : renderEmptyCall();
|
|
319
|
+
},
|
|
320
|
+
execute: async (_toolCallId, params) => {
|
|
321
|
+
const selection = clearSubagentRecords(manager.listAgents(), {
|
|
322
|
+
agentIds: params.agent_ids,
|
|
323
|
+
olderThanMs: (params.older_than_minutes ?? 5) * 60_000,
|
|
324
|
+
includeErrors: params.include_errors === true,
|
|
325
|
+
});
|
|
326
|
+
const removed = manager.clearRecords(selection.clearIds);
|
|
327
|
+
const details = buildClearSubagentsDetails({ ...selection, clearIds: removed });
|
|
328
|
+
return textResult(formatClearSubagentsText(details), details);
|
|
329
|
+
},
|
|
330
|
+
}));
|
|
331
|
+
}
|
|
@@ -23,7 +23,7 @@ export function snipMiddleLines(text, edgeLines = 20) {
|
|
|
23
23
|
const omitted = lines.length - maxLines;
|
|
24
24
|
return [
|
|
25
25
|
...lines.slice(0, edgeLines),
|
|
26
|
-
|
|
26
|
+
`─────── ⋐ ${omitted} lines hidden from preview ⋑ ───────`,
|
|
27
27
|
...lines.slice(-edgeLines),
|
|
28
28
|
];
|
|
29
29
|
}
|