pi-subagents-lite 1.0.2 → 1.1.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/README.md +43 -6
- package/package.json +1 -1
- package/src/agent-manager.ts +81 -62
- package/src/agent-runner.ts +194 -167
- package/src/config-io.ts +9 -1
- package/src/config-mutator.ts +183 -0
- package/src/context.ts +1 -1
- package/src/format.ts +173 -0
- package/src/index.ts +124 -176
- package/src/menus.ts +188 -136
- package/src/model-precedence.ts +5 -0
- package/src/output-file.ts +1 -68
- package/src/renderer.ts +157 -0
- package/src/result-viewer.ts +2 -1
- package/src/state.ts +80 -0
- package/src/tool-execution.ts +145 -53
- package/src/types.ts +100 -31
- package/src/ui/agent-widget.ts +148 -145
- package/src/usage.ts +5 -0
- package/src/stop-agent-tool.ts +0 -77
package/src/output-file.ts
CHANGED
|
@@ -10,12 +10,7 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
12
12
|
import { formatTokens } from "./usage.js";
|
|
13
|
-
|
|
14
|
-
/** Max length for a truncated command in tool arg summaries. */
|
|
15
|
-
const MAX_COMMAND_DISPLAY_LENGTH = 100;
|
|
16
|
-
|
|
17
|
-
/** Max length for a truncated string value in default tool arg summaries. */
|
|
18
|
-
const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
|
|
13
|
+
import { summarizeToolArgs } from "./format.js";
|
|
19
14
|
|
|
20
15
|
/** Max content length for full tool result display — longer results get a summary line. */
|
|
21
16
|
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
@@ -68,68 +63,6 @@ function splitAndPrefix(text: string, role: string): string {
|
|
|
68
63
|
.join("");
|
|
69
64
|
}
|
|
70
65
|
|
|
71
|
-
/**
|
|
72
|
-
* Summarize tool arguments for log-friendly display.
|
|
73
|
-
*
|
|
74
|
-
* Heavy tools (read, write, edit, bash, grep, rg) get compact summaries.
|
|
75
|
-
* Other tools fall back to the default JSON formatting.
|
|
76
|
-
*/
|
|
77
|
-
export function summarizeToolArgs(name: string, rawArgs: Record<string, unknown> | undefined): string {
|
|
78
|
-
if (!rawArgs || typeof rawArgs !== "object" || Object.keys(rawArgs).length === 0) return "";
|
|
79
|
-
|
|
80
|
-
switch (name) {
|
|
81
|
-
case "read": {
|
|
82
|
-
// read("/path/to/file") — just the path
|
|
83
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
84
|
-
return `(${JSON.stringify(path)})`;
|
|
85
|
-
}
|
|
86
|
-
case "write": {
|
|
87
|
-
// write("/path/to/file", <N> chars) — path + content size
|
|
88
|
-
const path = typeof rawArgs.file_path === "string" ? rawArgs.file_path : "";
|
|
89
|
-
const content = rawArgs.content;
|
|
90
|
-
const size = typeof content === "string" ? content.length : 0;
|
|
91
|
-
return `(${JSON.stringify(path)}, ${size} chars)`;
|
|
92
|
-
}
|
|
93
|
-
case "edit": {
|
|
94
|
-
// edit("/path/to/file", <N> edits) — path + edit count
|
|
95
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
96
|
-
const edits = rawArgs.edits;
|
|
97
|
-
const editCount = Array.isArray(edits) ? edits.length : 0;
|
|
98
|
-
return `(${JSON.stringify(path)}, ${editCount} edits)`;
|
|
99
|
-
}
|
|
100
|
-
case "bash": {
|
|
101
|
-
// bash("command") — just the command, strip heredoc, truncate long
|
|
102
|
-
const cmd = typeof rawArgs.command === "string" ? rawArgs.command : "";
|
|
103
|
-
// Strip heredoc: truncate at << followed by delimiter
|
|
104
|
-
const heredocIdx = cmd.search(/<<\s*['"]?\w+['"]?/);
|
|
105
|
-
const cleanCmd = heredocIdx >= 0 ? cmd.slice(0, heredocIdx).trim() : cmd.trim();
|
|
106
|
-
// Truncate long commands
|
|
107
|
-
const display = cleanCmd.length > MAX_COMMAND_DISPLAY_LENGTH
|
|
108
|
-
? cleanCmd.slice(0, MAX_COMMAND_DISPLAY_LENGTH) + "…" : cleanCmd;
|
|
109
|
-
return `(${JSON.stringify(display)})`;
|
|
110
|
-
}
|
|
111
|
-
case "grep":
|
|
112
|
-
case "rg": {
|
|
113
|
-
// grep("pattern", "/path") — pattern + path
|
|
114
|
-
const pattern = typeof rawArgs.pattern === "string" ? rawArgs.pattern : "";
|
|
115
|
-
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
116
|
-
return `(${JSON.stringify(pattern)}, ${JSON.stringify(path)})`;
|
|
117
|
-
}
|
|
118
|
-
default: {
|
|
119
|
-
// Default behavior for other tools: single-arg shorthand or JSON dump
|
|
120
|
-
const keys = Object.keys(rawArgs);
|
|
121
|
-
if (keys.length === 1) {
|
|
122
|
-
const val = rawArgs[keys[0]];
|
|
123
|
-
const display = typeof val === "string" && val.length > MAX_DEFAULT_STRING_DISPLAY_LENGTH
|
|
124
|
-
? JSON.stringify(val.slice(0, MAX_DEFAULT_STRING_DISPLAY_LENGTH) + "...")
|
|
125
|
-
: JSON.stringify(val);
|
|
126
|
-
return `(${display})`;
|
|
127
|
-
}
|
|
128
|
-
return ` ${JSON.stringify(rawArgs)}`;
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
66
|
/** Format a toolUse/toolCall content item as a single log line. */
|
|
134
67
|
function formatToolItem(item: Record<string, unknown>): string {
|
|
135
68
|
const name = (item.name ?? item.toolName ?? "unknown") as string;
|
package/src/renderer.ts
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* renderer.ts — Rendering helpers for the Agent tool and subagent-result messages.
|
|
3
|
+
*
|
|
4
|
+
* Extracted from index.ts to separate display concerns from extension wiring.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { Box, Container, Spacer, Text } from "@earendil-works/pi-tui";
|
|
8
|
+
import type { Theme } from "./types.js";
|
|
9
|
+
import { buildStatsParts, formatMs, getDisplayName } from "./format.js";
|
|
10
|
+
import { __config } from "./state.js";
|
|
11
|
+
|
|
12
|
+
// ============================================================================
|
|
13
|
+
// Stats rendering helpers
|
|
14
|
+
// ============================================================================
|
|
15
|
+
|
|
16
|
+
/** Format agent display name with optional model: "Agent (mimo-v2.5-pro)" or "Agent". */
|
|
17
|
+
export function agentNameLabel(d: Record<string, unknown>, theme: Theme): string {
|
|
18
|
+
const typeName = getDisplayName((d.type as string) || "");
|
|
19
|
+
const modelName = d.modelName as string | undefined;
|
|
20
|
+
return modelName ? `${theme.bold(typeName)} (${modelName})` : theme.bold(typeName);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Build the stats line for an agent result card. */
|
|
24
|
+
export function buildStatsLine(d: Record<string, unknown>, theme: Theme): string {
|
|
25
|
+
const showCost = __config.agent.showCost === true;
|
|
26
|
+
const parts = buildStatsParts({
|
|
27
|
+
toolUses: (d.toolUses as number) ?? 0,
|
|
28
|
+
turnCount: d.turnCount as number | undefined,
|
|
29
|
+
maxTurns: d.maxTurns as number | undefined,
|
|
30
|
+
tokens: (d.tokens as number) ?? 0,
|
|
31
|
+
contextPercent: d.contextPercent as number | null,
|
|
32
|
+
compactions: (d.compactions as number) ?? 0,
|
|
33
|
+
cost: showCost ? (d.cost as number | undefined) : undefined,
|
|
34
|
+
}, theme);
|
|
35
|
+
parts.push(formatMs(d.durationMs as number));
|
|
36
|
+
return parts.join("·");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ============================================================================
|
|
40
|
+
// Agent tool renderers
|
|
41
|
+
// ============================================================================
|
|
42
|
+
|
|
43
|
+
/** Render the Agent tool call line (e.g., "▸ Agent (model)"). */
|
|
44
|
+
export function renderAgentToolCall(
|
|
45
|
+
args: Record<string, unknown>,
|
|
46
|
+
theme: Theme,
|
|
47
|
+
): Text {
|
|
48
|
+
const typeName = getDisplayName((args.agent as string) || "");
|
|
49
|
+
const label = typeName || "Agent";
|
|
50
|
+
let text = `▸ ${theme.fg("accent", theme.bold(label))}`;
|
|
51
|
+
|
|
52
|
+
const modelOverride = args._modelOverride as string | undefined;
|
|
53
|
+
if (modelOverride) {
|
|
54
|
+
text += ` (${modelOverride})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return new Text(text, 0, 0);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Render the Agent tool result — compact or expanded. */
|
|
61
|
+
export function renderAgentToolResult(
|
|
62
|
+
result: { content: Array<{ type: string; text?: string }>; details?: Record<string, unknown>; isError?: boolean },
|
|
63
|
+
options: { expanded?: boolean },
|
|
64
|
+
theme: Theme,
|
|
65
|
+
): Text {
|
|
66
|
+
const { expanded } = options;
|
|
67
|
+
const text = result.content[0]?.type === "text" ? result.content[0].text ?? "" : "";
|
|
68
|
+
const d = result.details;
|
|
69
|
+
const icon = result.isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
70
|
+
const desc = (d?.description as string) || "";
|
|
71
|
+
|
|
72
|
+
if (d && d.turnCount != null) {
|
|
73
|
+
const namePart = agentNameLabel(d, theme);
|
|
74
|
+
const statsLine = buildStatsLine(d, theme);
|
|
75
|
+
let lines = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", desc)}`;
|
|
76
|
+
if (expanded && text) {
|
|
77
|
+
lines += "\n" + text.split("\n").map(l => ` ${l}`).join("\n");
|
|
78
|
+
}
|
|
79
|
+
return new Text(lines, 0, 0);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Minimal card — background spawns (no stats) use space placeholder
|
|
83
|
+
const isBackground = text.includes("running in background") || text.includes("queued");
|
|
84
|
+
const prefix = isBackground ? " " : `${icon} `;
|
|
85
|
+
if (desc) {
|
|
86
|
+
return new Text(`${prefix}${theme.fg("text", desc)}`, 0, 0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return new Text(`${prefix}${theme.fg("dim", text)}`, 0, 0);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ============================================================================
|
|
93
|
+
// Message renderer — subagent-result (background agent completion)
|
|
94
|
+
// ============================================================================
|
|
95
|
+
|
|
96
|
+
/** Render a subagent-result message injected after background agent completion. */
|
|
97
|
+
export function renderSubagentResult(
|
|
98
|
+
message: { content?: string; details?: Record<string, unknown> },
|
|
99
|
+
options: { expanded?: boolean },
|
|
100
|
+
theme: Theme,
|
|
101
|
+
): Container {
|
|
102
|
+
const { expanded } = options;
|
|
103
|
+
const d = message.details;
|
|
104
|
+
const text = (message.content as string)?.trim() || "";
|
|
105
|
+
|
|
106
|
+
const inner = new Container();
|
|
107
|
+
inner.addChild(new Text(theme.fg("customMessageLabel", "Subagent Result"), 0, 0));
|
|
108
|
+
inner.addChild(new Spacer(1));
|
|
109
|
+
|
|
110
|
+
if (d && d.turnCount != null) {
|
|
111
|
+
const isError = d.status === "error" || d.status === "aborted" || d.status === "stopped";
|
|
112
|
+
const icon = isError ? theme.fg("error", "✗") : theme.fg("success", "✓");
|
|
113
|
+
|
|
114
|
+
const namePart = agentNameLabel(d, theme);
|
|
115
|
+
const statsLine = buildStatsLine(d, theme);
|
|
116
|
+
let headerLine = `${icon} ${namePart}·${statsLine}\n ${theme.fg("text", (d.description as string) || "")}`;
|
|
117
|
+
if (d.outputFile as string) {
|
|
118
|
+
headerLine += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
119
|
+
}
|
|
120
|
+
inner.addChild(new Text(headerLine, 0, 0));
|
|
121
|
+
|
|
122
|
+
if (expanded && text) {
|
|
123
|
+
inner.addChild(new Spacer(1));
|
|
124
|
+
inner.addChild(new Text(text.split("\n").map(l => ` ${l}`).join("\n"), 0, 0));
|
|
125
|
+
}
|
|
126
|
+
} else {
|
|
127
|
+
inner.addChild(new Text(buildFallbackResultLine(d, text, theme), 0, 0));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const box = new Box(1, 1, (t) => theme.bg("customMessageBg", t));
|
|
131
|
+
box.addChild(inner);
|
|
132
|
+
|
|
133
|
+
const outer = new Container();
|
|
134
|
+
outer.addChild(new Spacer(1));
|
|
135
|
+
outer.addChild(box);
|
|
136
|
+
outer.addChild(new Spacer(1));
|
|
137
|
+
return outer;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Build a fallback result line for subagent-result messages without stats. */
|
|
141
|
+
function buildFallbackResultLine(
|
|
142
|
+
d: Record<string, unknown> | undefined,
|
|
143
|
+
text: string,
|
|
144
|
+
theme: Theme,
|
|
145
|
+
): string {
|
|
146
|
+
const icon = theme.fg("success", "✓");
|
|
147
|
+
let line = icon;
|
|
148
|
+
if (d?.type) {
|
|
149
|
+
line += ` ${agentNameLabel(d, theme)}`;
|
|
150
|
+
}
|
|
151
|
+
const desc = (d?.description as string) || "";
|
|
152
|
+
if (desc) line += `\n ${theme.fg("text", desc)}`;
|
|
153
|
+
if (d?.outputFile) {
|
|
154
|
+
line += `\n ${theme.fg("dim", `tail -f ${d.outputFile}`)}`;
|
|
155
|
+
}
|
|
156
|
+
return line;
|
|
157
|
+
}
|
package/src/result-viewer.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
} from "@earendil-works/pi-tui";
|
|
18
18
|
import { DynamicBorder } from "@earendil-works/pi-coding-agent";
|
|
19
19
|
import { type LifetimeUsage, formatTokens } from "./usage.js";
|
|
20
|
-
import {
|
|
20
|
+
import type { Theme } from "./ui/agent-widget.js";
|
|
21
|
+
import { formatMs } from "./format.js";
|
|
21
22
|
|
|
22
23
|
/* ------------------------------------------------------------------ */
|
|
23
24
|
/* Types */
|
package/src/state.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* state.ts — Shared module state. Extracted from index.ts to break circular deps.
|
|
3
|
+
*
|
|
4
|
+
* manager and widget use holders because they're reassigned after import and the
|
|
5
|
+
* PI runtime doesn't propagate ESM live binding reassignments.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
9
|
+
import type { SessionModelOverrides, SubagentsConfig } from "./model-precedence.js";
|
|
10
|
+
import { DEFAULT_CONFIG } from "./config-io.js";
|
|
11
|
+
import { AgentManager } from "./agent-manager.js";
|
|
12
|
+
import { AgentWidget, type AgentActivity } from "./ui/agent-widget.js";
|
|
13
|
+
|
|
14
|
+
export let sessionOverrides: SessionModelOverrides = { default: null };
|
|
15
|
+
export let __config: SubagentsConfig = { ...DEFAULT_CONFIG, agent: { ...DEFAULT_CONFIG.agent }, concurrency: { ...DEFAULT_CONFIG.concurrency } };
|
|
16
|
+
export const agentActivity = new Map<string, AgentActivity>();
|
|
17
|
+
export let piInstance: ExtensionAPI;
|
|
18
|
+
|
|
19
|
+
// Holder objects — PI runtime doesn't propagate ESM live binding reassignments
|
|
20
|
+
const managerHolder: { current?: AgentManager } = {};
|
|
21
|
+
const widgetHolder: { current?: AgentWidget } = {};
|
|
22
|
+
|
|
23
|
+
export function setConfig(config: SubagentsConfig): void { __config = config; }
|
|
24
|
+
export function resetSessionOverrides(): void { sessionOverrides = { default: null }; }
|
|
25
|
+
export function setManager(m: AgentManager): void { managerHolder.current = m; }
|
|
26
|
+
export function clearManager(): void { managerHolder.current = undefined; }
|
|
27
|
+
export function setWidget(w: AgentWidget | undefined): void { widgetHolder.current = w; }
|
|
28
|
+
export function setPiInstance(pi: ExtensionAPI): void { piInstance = pi; }
|
|
29
|
+
export function getManager(): AgentManager { return managerHolder.current!; }
|
|
30
|
+
export function getWidget(): AgentWidget | undefined { return widgetHolder.current; }
|
|
31
|
+
|
|
32
|
+
// State mutation helpers
|
|
33
|
+
|
|
34
|
+
/** Update the cost display toggle in config and sync to widget. */
|
|
35
|
+
export function setShowCostEnabled(enabled: boolean): void {
|
|
36
|
+
__config.agent.showCost = enabled;
|
|
37
|
+
getWidget()?.setShowCost(enabled);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Sync widget display settings from config to the widget instance. */
|
|
41
|
+
export function syncWidgetSettings(): void {
|
|
42
|
+
const w = getWidget();
|
|
43
|
+
if (!w) return;
|
|
44
|
+
w.setForceCompact(__config.agent.widgetCompact === true);
|
|
45
|
+
w.setWidgetShortcut(__config.agent.widgetShortcut === true);
|
|
46
|
+
w.setMaxLines(__config.agent.widgetMaxLines ?? 12);
|
|
47
|
+
w.setMaxLinesCompact(
|
|
48
|
+
__config.agent.widgetMaxLinesCompact ?? Math.floor((__config.agent.widgetMaxLines ?? 12) / 2),
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Track previous tool expansion state to detect ctrl+o toggle. */
|
|
53
|
+
let lastToolsExpanded: boolean | undefined;
|
|
54
|
+
|
|
55
|
+
/** Reset lastToolsExpanded (called at session_start). */
|
|
56
|
+
export function resetLastToolsExpanded(): void {
|
|
57
|
+
lastToolsExpanded = undefined;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Sync compact mode with the tool expansion state (ctrl+o toggle).
|
|
61
|
+
* Only syncs when widgetShortcut is enabled in config (opt-in behavior).
|
|
62
|
+
* Only triggers on state change (not every tool_execution_start).
|
|
63
|
+
* When forceCompact (widgetCompact) is ON, ignores ctrl+o state changes.
|
|
64
|
+
*/
|
|
65
|
+
export function syncCompactFromToolsExpanded(expanded: boolean): void {
|
|
66
|
+
if (__config.agent.widgetShortcut !== true) {
|
|
67
|
+
lastToolsExpanded = expanded;
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
// When forceCompact is ON, ignore ctrl+o state changes
|
|
71
|
+
if (__config.agent.widgetCompact === true) {
|
|
72
|
+
lastToolsExpanded = expanded;
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
// Tools expanded → widget full, tools collapsed → widget compact
|
|
76
|
+
if (lastToolsExpanded !== undefined && lastToolsExpanded !== expanded) {
|
|
77
|
+
getWidget()?.setCompactMode(!expanded);
|
|
78
|
+
}
|
|
79
|
+
lastToolsExpanded = expanded;
|
|
80
|
+
}
|
package/src/tool-execution.ts
CHANGED
|
@@ -8,22 +8,23 @@
|
|
|
8
8
|
import type { ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-agent";
|
|
9
9
|
|
|
10
10
|
import type { AgentRecord } from "./types.js";
|
|
11
|
+
import { SHORT_ID_LENGTH } from "./types.js";
|
|
11
12
|
import type { SpawnOptions as AgentManagerSpawnOptions } from "./agent-manager.js";
|
|
12
13
|
import type { AgentActivity } from "./ui/agent-widget.js";
|
|
13
14
|
import { resolveType, getAgentConfig, discoverNewAgents } from "./agent-types.js";
|
|
14
15
|
import { resolveModel } from "./model-precedence.js";
|
|
15
16
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
16
17
|
|
|
17
|
-
// Shared state imported from
|
|
18
|
+
// Shared state imported from state.ts
|
|
18
19
|
import { parseModelKey, findModelInRegistry, parseThinkingLevel } from "./utils.js";
|
|
19
20
|
import {
|
|
20
21
|
__config,
|
|
21
22
|
sessionOverrides,
|
|
22
|
-
manager,
|
|
23
23
|
piInstance,
|
|
24
24
|
agentActivity,
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
getManager,
|
|
26
|
+
getWidget,
|
|
27
|
+
} from "./state.js";
|
|
27
28
|
|
|
28
29
|
// ============================================================================
|
|
29
30
|
// Module-level state
|
|
@@ -103,6 +104,61 @@ function createActivityTracker(maxTurns?: number, onStreamUpdate?: () => void) {
|
|
|
103
104
|
return { state, callbacks };
|
|
104
105
|
}
|
|
105
106
|
|
|
107
|
+
// ============================================================================
|
|
108
|
+
// buildAgentDetails — consolidated stats/details construction
|
|
109
|
+
// ============================================================================
|
|
110
|
+
|
|
111
|
+
interface AgentDetailsOptions {
|
|
112
|
+
/** Include full stats (turns, tokens, context%, compactions, cost). Default: false. */
|
|
113
|
+
includeStats?: boolean;
|
|
114
|
+
/** Include status and outputFile. Default: false. */
|
|
115
|
+
includeStatus?: boolean;
|
|
116
|
+
/** Override the turnCount (e.g. from activity tracker). Default: record.turnCount. */
|
|
117
|
+
turnCount?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Build a details Record from an AgentRecord, controlled by options.
|
|
122
|
+
*
|
|
123
|
+
* Always includes `type` and `description`. Optional groups:
|
|
124
|
+
* - `includeStatus`: adds `status`, `outputFile`
|
|
125
|
+
* - `includeStats`: adds turn/token/cost/context/compaction/model fields
|
|
126
|
+
*
|
|
127
|
+
* Consolidates the identical field-selection logic previously duplicated
|
|
128
|
+
* across emitIndividualNudge, executeSpawnForeground, and executeSpawnBackground.
|
|
129
|
+
*/
|
|
130
|
+
export function buildAgentDetails(
|
|
131
|
+
record: AgentRecord,
|
|
132
|
+
options?: AgentDetailsOptions,
|
|
133
|
+
): Record<string, unknown> {
|
|
134
|
+
const details: Record<string, unknown> = {
|
|
135
|
+
type: record.display.type,
|
|
136
|
+
description: record.display.description,
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
if (options?.includeStatus) {
|
|
140
|
+
details.status = record.lifecycle.status;
|
|
141
|
+
details.outputFile = record.display.outputFile;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (options?.includeStats) {
|
|
145
|
+
const totalTokens = getLifetimeTotal(record.stats.lifetimeUsage);
|
|
146
|
+
const elapsedMs = record.lifecycle.completedAt ? record.lifecycle.completedAt - record.lifecycle.startedAt : 0;
|
|
147
|
+
|
|
148
|
+
details.turnCount = options.turnCount ?? record.stats.turnCount;
|
|
149
|
+
details.maxTurns = record.stats.maxTurns;
|
|
150
|
+
details.toolUses = record.stats.toolUses;
|
|
151
|
+
details.tokens = totalTokens;
|
|
152
|
+
details.contextPercent = getSessionContextPercent(record.execution.session);
|
|
153
|
+
details.durationMs = elapsedMs;
|
|
154
|
+
details.compactions = record.stats.compactionCount;
|
|
155
|
+
details.modelName = record.display.invocation?.modelName;
|
|
156
|
+
details.cost = record.stats.lifetimeUsage.cost;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return details;
|
|
160
|
+
}
|
|
161
|
+
|
|
106
162
|
// ============================================================================
|
|
107
163
|
// Nudge scheduling — batch completion notifications within the hold window
|
|
108
164
|
// ============================================================================
|
|
@@ -118,7 +174,7 @@ export function scheduleNudge(agentId: string): void {
|
|
|
118
174
|
pendingNudges.clear();
|
|
119
175
|
|
|
120
176
|
for (const id of batch) {
|
|
121
|
-
emitIndividualNudge(id,
|
|
177
|
+
emitIndividualNudge(id, getManager()?.getRecord(id));
|
|
122
178
|
}
|
|
123
179
|
}, NUDGE_DELAY_MS);
|
|
124
180
|
}
|
|
@@ -126,31 +182,16 @@ export function scheduleNudge(agentId: string): void {
|
|
|
126
182
|
function emitIndividualNudge(agentId: string, record?: AgentRecord): void {
|
|
127
183
|
if (!record) return;
|
|
128
184
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
:
|
|
133
|
-
|
|
134
|
-
const details: Record<string, unknown> = {
|
|
135
|
-
type: record.type,
|
|
136
|
-
description: record.description,
|
|
137
|
-
status: record.status,
|
|
138
|
-
outputFile: record.outputFile,
|
|
139
|
-
turnCount: record.turnCount ?? agentActivity.get(agentId)?.turnCount,
|
|
140
|
-
maxTurns: record.maxTurns,
|
|
141
|
-
toolUses: record.toolUses,
|
|
142
|
-
tokens: totalTokens,
|
|
143
|
-
cost: record.lifetimeUsage.cost,
|
|
144
|
-
contextPercent: getSessionContextPercent(record.session),
|
|
145
|
-
durationMs: elapsedMs,
|
|
146
|
-
compactions: record.compactionCount,
|
|
147
|
-
modelName: record.invocation?.modelName,
|
|
148
|
-
};
|
|
185
|
+
const details = buildAgentDetails(record, {
|
|
186
|
+
includeStats: true,
|
|
187
|
+
includeStatus: true,
|
|
188
|
+
turnCount: record.stats.turnCount ?? agentActivity.get(agentId)?.turnCount,
|
|
189
|
+
});
|
|
149
190
|
|
|
150
191
|
piInstance.sendMessage(
|
|
151
192
|
{
|
|
152
193
|
customType: "subagent-result",
|
|
153
|
-
content: `[Subagent "${record.type}" completed]\n\n${record.result ?? ""}`,
|
|
194
|
+
content: `[Subagent "${record.display.type}" completed]\n\n${record.result ?? ""}`,
|
|
154
195
|
details,
|
|
155
196
|
display: true,
|
|
156
197
|
},
|
|
@@ -225,20 +266,20 @@ async function executeSpawnBackground(
|
|
|
225
266
|
spawnOptions.maxTurns,
|
|
226
267
|
);
|
|
227
268
|
|
|
228
|
-
const agentId =
|
|
269
|
+
const agentId = getManager().spawn(piInstance, ctx, resolvedType, prompt, {
|
|
229
270
|
...spawnOptions,
|
|
230
271
|
isBackground: true,
|
|
231
272
|
...callbacks,
|
|
232
273
|
});
|
|
233
274
|
backgroundAgentIds.add(agentId);
|
|
234
275
|
agentActivity.set(agentId, state);
|
|
235
|
-
|
|
236
|
-
|
|
276
|
+
getWidget()?.ensureTimer();
|
|
277
|
+
getWidget()?.update();
|
|
237
278
|
|
|
238
|
-
const record =
|
|
239
|
-
const details
|
|
279
|
+
const record = getManager().getRecord(agentId)!;
|
|
280
|
+
const details = buildAgentDetails(record);
|
|
240
281
|
const suffix = `A notification will arrive when done - User asks you not to poll, check status or duplicate the delegated work.\n\nAgent ID: ${agentId}`;
|
|
241
|
-
const label = record.status === "queued" ? "Agent queued" : "Agent running";
|
|
282
|
+
const label = record.lifecycle.status === "queued" ? "Agent queued" : "Agent running";
|
|
242
283
|
|
|
243
284
|
return successResult(`[${label}] ${suffix}`, details);
|
|
244
285
|
}
|
|
@@ -253,37 +294,27 @@ async function executeSpawnForeground(
|
|
|
253
294
|
spawnOptions.maxTurns,
|
|
254
295
|
);
|
|
255
296
|
|
|
256
|
-
const fgId =
|
|
297
|
+
const fgId = getManager().spawn(piInstance, ctx, resolvedType, prompt, {
|
|
257
298
|
...spawnOptions,
|
|
258
299
|
...fgCallbacks,
|
|
259
300
|
isBackground: false,
|
|
260
301
|
});
|
|
261
302
|
agentActivity.set(fgId, fgState);
|
|
262
|
-
|
|
303
|
+
getWidget()?.ensureTimer();
|
|
263
304
|
|
|
264
|
-
const record =
|
|
265
|
-
await record.promise;
|
|
305
|
+
const record = getManager().getRecord(fgId)!;
|
|
306
|
+
await record.execution.promise;
|
|
266
307
|
|
|
267
308
|
agentActivity.delete(fgId);
|
|
268
|
-
|
|
269
|
-
|
|
309
|
+
getWidget()?.markFinished(fgId);
|
|
310
|
+
getWidget()?.update();
|
|
270
311
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
const stats: Record<string, unknown> = {
|
|
274
|
-
type: resolvedType,
|
|
312
|
+
const stats = buildAgentDetails(record, {
|
|
313
|
+
includeStats: true,
|
|
275
314
|
turnCount: fgState.turnCount,
|
|
276
|
-
|
|
277
|
-
toolUses: record.toolUses,
|
|
278
|
-
tokens: totalTokens,
|
|
279
|
-
contextPercent: getSessionContextPercent(fgState.session),
|
|
280
|
-
durationMs: elapsedMs,
|
|
281
|
-
description: spawnOptions.description,
|
|
282
|
-
compactions: record.compactionCount,
|
|
283
|
-
modelName: record.invocation?.modelName,
|
|
284
|
-
};
|
|
315
|
+
});
|
|
285
316
|
|
|
286
|
-
if (record.status === "error") {
|
|
317
|
+
if (record.lifecycle.status === "error") {
|
|
287
318
|
return errorResult(`Agent failed: ${record.error || "unknown error"}`, stats);
|
|
288
319
|
}
|
|
289
320
|
|
|
@@ -291,9 +322,70 @@ async function executeSpawnForeground(
|
|
|
291
322
|
}
|
|
292
323
|
|
|
293
324
|
// ============================================================================
|
|
294
|
-
//
|
|
325
|
+
// Running agents list helper (used by executeStopAgentTool)
|
|
295
326
|
// ============================================================================
|
|
296
327
|
|
|
328
|
+
/**
|
|
329
|
+
* Build a compact list of running (or queued) agents.
|
|
330
|
+
* Format: "type·short_id, type·short_id" — one line, easy for LLM to parse.
|
|
331
|
+
*/
|
|
332
|
+
function formatRunningAgents(): string {
|
|
333
|
+
const agents = getManager().listAgents().filter(
|
|
334
|
+
(a) => a.lifecycle.status === "running" || a.lifecycle.status === "queued",
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
if (agents.length === 0) return "none";
|
|
338
|
+
|
|
339
|
+
return agents
|
|
340
|
+
.map((a) => `${a.display.type}·${a.id.slice(0, SHORT_ID_LENGTH)}`)
|
|
341
|
+
.join(", ");
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ============================================================================
|
|
345
|
+
// StopAgent execute handler
|
|
346
|
+
// ============================================================================
|
|
347
|
+
|
|
348
|
+
export async function executeStopAgentTool(
|
|
349
|
+
_toolCallId: string,
|
|
350
|
+
params: Record<string, unknown>,
|
|
351
|
+
_signal: AbortSignal | undefined,
|
|
352
|
+
_onUpdate: ((update: any) => void) | undefined,
|
|
353
|
+
_ctx: ExtensionContext,
|
|
354
|
+
): Promise<any> {
|
|
355
|
+
const agentId = params.agent_id as string | undefined;
|
|
356
|
+
|
|
357
|
+
if (!agentId) {
|
|
358
|
+
return errorResult("agent_id is required");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
const record = getManager().getRecord(agentId);
|
|
362
|
+
|
|
363
|
+
if (!record) {
|
|
364
|
+
// Agent not found → return error + list of running agents
|
|
365
|
+
return errorResult(
|
|
366
|
+
`Agent ${agentId} not found. Running agents: ${formatRunningAgents()}`,
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Check if already in a terminal state (not running or queued)
|
|
371
|
+
if (record.lifecycle.status !== "running" && record.lifecycle.status !== "queued") {
|
|
372
|
+
return successResult(
|
|
373
|
+
`Agent ${agentId} is already ${record.lifecycle.status}. Running agents: ${formatRunningAgents()}`,
|
|
374
|
+
);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Attempt to stop the running/queued agent
|
|
378
|
+
if (getManager().abort(agentId)) {
|
|
379
|
+
return successResult(`Stopped agent ${agentId.slice(0, SHORT_ID_LENGTH)}`);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
return errorResult(`Failed to stop agent ${agentId}`);
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// ============================================================================
|
|
386
|
+
// Tool_call listener — inject model into Agent tool calls
|
|
387
|
+
// =============================================================================
|
|
388
|
+
|
|
297
389
|
export async function toolCallListener(
|
|
298
390
|
event: ToolCallEvent,
|
|
299
391
|
ctx: ExtensionContext,
|