pi-subagents-lite 0.2.0 → 0.3.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/README.md +222 -36
- package/package.json +3 -1
- package/src/agent-discovery.ts +36 -45
- package/src/agent-manager.ts +101 -87
- package/src/agent-runner.ts +40 -49
- package/src/agent-types.ts +15 -37
- package/src/config-io.ts +40 -0
- package/src/context.ts +80 -1
- package/src/index.ts +105 -1117
- package/src/menus.ts +866 -0
- package/src/model-precedence.ts +46 -36
- package/src/model-selector.ts +19 -19
- package/src/output-file.ts +123 -33
- package/src/prompts.ts +2 -2
- package/src/result-viewer.ts +166 -37
- package/src/skill-loader.ts +1 -1
- package/src/stop-agent-tool.ts +76 -0
- package/src/tool-execution.ts +361 -0
- package/src/types.ts +16 -1
- package/src/ui/agent-widget.ts +98 -91
- package/src/usage.ts +12 -4
- package/src/utils.ts +53 -4
package/src/model-precedence.ts
CHANGED
|
@@ -4,17 +4,20 @@
|
|
|
4
4
|
* Pure function — no side effects, no file I/O, no pi SDK imports.
|
|
5
5
|
*
|
|
6
6
|
* Precedence chain (highest to lowest):
|
|
7
|
-
* 1.
|
|
8
|
-
* 2.
|
|
9
|
-
* 3.
|
|
10
|
-
* 4.
|
|
7
|
+
* 1. sessionOverrides[subagentType] (session per-type override)
|
|
8
|
+
* 2. sessionOverrides["default"] (session global default)
|
|
9
|
+
* 3. config.agent[subagentType] (config per-type override)
|
|
10
|
+
* 4. config.agent["default"] (config global default)
|
|
11
|
+
* 5. agentConfig?.model (agent config / frontmatter)
|
|
12
|
+
* 6. parentModelId (inherit from parent)
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
/** Shape of the subagents-lite.json config file. */
|
|
14
16
|
export interface SubagentsConfig {
|
|
15
17
|
agent: {
|
|
16
18
|
default: string | null;
|
|
17
|
-
|
|
19
|
+
forceBackground: boolean;
|
|
20
|
+
[agentType: string]: string | null | undefined | boolean;
|
|
18
21
|
};
|
|
19
22
|
concurrency: {
|
|
20
23
|
default: number;
|
|
@@ -23,49 +26,56 @@ export interface SubagentsConfig {
|
|
|
23
26
|
};
|
|
24
27
|
}
|
|
25
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Shape of session-only model overrides.
|
|
31
|
+
* Same as config.agent but without the forceBackground flag.
|
|
32
|
+
* Not persisted — cleared on session_start.
|
|
33
|
+
*/
|
|
34
|
+
export interface SessionModelOverrides {
|
|
35
|
+
default: string | null;
|
|
36
|
+
[agentType: string]: string | null | undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
/** Options for resolveModel. */
|
|
41
|
+
export interface ResolveModelOptions {
|
|
42
|
+
/** The type of subagent being spawned. */
|
|
43
|
+
subagentType: string;
|
|
44
|
+
/** The agent's config (from .md frontmatter or defaults). */
|
|
45
|
+
agentConfig?: { model?: string };
|
|
46
|
+
/** The global subagents-lite.json config (model overrides). */
|
|
47
|
+
config: SubagentsConfig;
|
|
48
|
+
/** The parent agent's model ID (final fallback). */
|
|
49
|
+
parentModelId: string;
|
|
50
|
+
/** Session-only overrides (checked first). */
|
|
51
|
+
sessionOverrides?: SessionModelOverrides;
|
|
52
|
+
}
|
|
53
|
+
|
|
26
54
|
/**
|
|
27
55
|
* Resolve the model for a subagent invocation.
|
|
28
56
|
*
|
|
29
57
|
* Returns the first non-null, non-undefined, non-empty-string value
|
|
30
58
|
* from the precedence chain. If all are empty/null, returns parentModelId.
|
|
31
|
-
*
|
|
32
|
-
* @param subagentType - The type of subagent being spawned
|
|
33
|
-
* @param agentConfig - The agent's config (from .md frontmatter or defaults)
|
|
34
|
-
* @param config - The global subagents-lite.json config (model overrides)
|
|
35
|
-
* @param parentModelId - The parent agent's model ID (final fallback)
|
|
36
|
-
* @returns The resolved model ID string
|
|
37
59
|
*/
|
|
38
|
-
export function resolveModel(
|
|
39
|
-
subagentType
|
|
40
|
-
agentConfig: { model?: string } | undefined,
|
|
41
|
-
config: SubagentsConfig,
|
|
42
|
-
parentModelId: string,
|
|
43
|
-
): string {
|
|
44
|
-
// Level 1: per-type override
|
|
45
|
-
const perTypeOverride = config.agent[subagentType];
|
|
46
|
-
if (isValidValue(perTypeOverride)) {
|
|
47
|
-
return perTypeOverride;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// Level 2: global default override
|
|
51
|
-
const globalDefault = config.agent["default"];
|
|
52
|
-
if (isValidValue(globalDefault)) {
|
|
53
|
-
return globalDefault;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
// Level 3: agent config/frontmatter model
|
|
57
|
-
if (agentConfig && isValidValue(agentConfig.model)) {
|
|
58
|
-
return agentConfig.model;
|
|
59
|
-
}
|
|
60
|
+
export function resolveModel(options: ResolveModelOptions): string {
|
|
61
|
+
const { subagentType, agentConfig, config, parentModelId, sessionOverrides } = options;
|
|
60
62
|
|
|
61
|
-
//
|
|
62
|
-
|
|
63
|
+
// Precedence chain: session > config > frontmatter > parent
|
|
64
|
+
const candidates: Array<string | boolean | null | undefined> = [
|
|
65
|
+
sessionOverrides?.[subagentType],
|
|
66
|
+
sessionOverrides?.["default"],
|
|
67
|
+
config.agent[subagentType],
|
|
68
|
+
config.agent["default"],
|
|
69
|
+
agentConfig?.model,
|
|
70
|
+
parentModelId, // final fallback (always a valid string)
|
|
71
|
+
];
|
|
72
|
+
return candidates.find(isValidValue) ?? parentModelId;
|
|
63
73
|
}
|
|
64
74
|
|
|
65
75
|
/**
|
|
66
76
|
* Check if a value is a valid non-empty model string.
|
|
67
77
|
* Returns true for non-null, non-undefined, non-empty strings.
|
|
68
78
|
*/
|
|
69
|
-
function isValidValue(value: string | null | undefined): value is string {
|
|
79
|
+
function isValidValue(value: string | boolean | null | undefined): value is string {
|
|
70
80
|
return typeof value === "string" && value.length > 0;
|
|
71
81
|
}
|
package/src/model-selector.ts
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
Container,
|
|
13
|
-
type Component,
|
|
14
13
|
type Focusable,
|
|
15
14
|
fuzzyFilter,
|
|
16
15
|
getKeybindings,
|
|
@@ -37,7 +36,7 @@ export interface ModelOption {
|
|
|
37
36
|
provider: string;
|
|
38
37
|
}
|
|
39
38
|
|
|
40
|
-
|
|
39
|
+
interface ModelSelectorCallbacks {
|
|
41
40
|
onSelect: (value: string) => void;
|
|
42
41
|
onCancel: () => void;
|
|
43
42
|
}
|
|
@@ -126,9 +125,20 @@ export class ModelSelectorDialog extends Container implements Focusable {
|
|
|
126
125
|
handleInput(keyData: string): void {
|
|
127
126
|
const kb = getKeybindings();
|
|
128
127
|
|
|
128
|
+
// Navigation keys — no-op when list is empty
|
|
129
|
+
if (this.filteredItems.length === 0) {
|
|
130
|
+
if (
|
|
131
|
+
kb.matches(keyData, "tui.select.up") ||
|
|
132
|
+
kb.matches(keyData, "tui.select.down") ||
|
|
133
|
+
kb.matches(keyData, "tui.select.pageUp") ||
|
|
134
|
+
kb.matches(keyData, "tui.select.pageDown")
|
|
135
|
+
) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
129
140
|
// Up — wrap to bottom
|
|
130
141
|
if (kb.matches(keyData, "tui.select.up")) {
|
|
131
|
-
if (this.filteredItems.length === 0) return;
|
|
132
142
|
this.selectedIndex =
|
|
133
143
|
this.selectedIndex === 0
|
|
134
144
|
? this.filteredItems.length - 1
|
|
@@ -139,7 +149,6 @@ export class ModelSelectorDialog extends Container implements Focusable {
|
|
|
139
149
|
|
|
140
150
|
// Down — wrap to top
|
|
141
151
|
if (kb.matches(keyData, "tui.select.down")) {
|
|
142
|
-
if (this.filteredItems.length === 0) return;
|
|
143
152
|
this.selectedIndex =
|
|
144
153
|
this.selectedIndex === this.filteredItems.length - 1
|
|
145
154
|
? 0
|
|
@@ -150,7 +159,6 @@ export class ModelSelectorDialog extends Container implements Focusable {
|
|
|
150
159
|
|
|
151
160
|
// PageUp — jump up one page
|
|
152
161
|
if (kb.matches(keyData, "tui.select.pageUp")) {
|
|
153
|
-
if (this.filteredItems.length === 0) return;
|
|
154
162
|
this.selectedIndex = Math.max(0, this.selectedIndex - MAX_VISIBLE);
|
|
155
163
|
this.updateList();
|
|
156
164
|
return;
|
|
@@ -158,7 +166,6 @@ export class ModelSelectorDialog extends Container implements Focusable {
|
|
|
158
166
|
|
|
159
167
|
// PageDown — jump down one page
|
|
160
168
|
if (kb.matches(keyData, "tui.select.pageDown")) {
|
|
161
|
-
if (this.filteredItems.length === 0) return;
|
|
162
169
|
this.selectedIndex = Math.min(
|
|
163
170
|
this.filteredItems.length - 1,
|
|
164
171
|
this.selectedIndex + MAX_VISIBLE,
|
|
@@ -242,19 +249,12 @@ export class ModelSelectorDialog extends Container implements Focusable {
|
|
|
242
249
|
const isSelected = i === this.selectedIndex;
|
|
243
250
|
const isCurrent = item.value === this.currentModel;
|
|
244
251
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
line = `${prefix}${modelText} ${providerBadge}${checkmark}`;
|
|
252
|
-
} else {
|
|
253
|
-
const modelText = ` ${item.label}`;
|
|
254
|
-
const providerBadge = this.theme.fg("muted", `[${item.provider}]`);
|
|
255
|
-
const checkmark = isCurrent ? this.theme.fg("success", " ✓") : "";
|
|
256
|
-
line = `${modelText} ${providerBadge}${checkmark}`;
|
|
257
|
-
}
|
|
252
|
+
const modelText = isSelected
|
|
253
|
+
? this.theme.fg("accent", "→ ") + this.theme.fg("accent", item.label)
|
|
254
|
+
: ` ${item.label}`;
|
|
255
|
+
const providerBadge = this.theme.fg("muted", `[${item.provider}]`);
|
|
256
|
+
const checkmark = isCurrent ? this.theme.fg("success", " ✓") : "";
|
|
257
|
+
const line = `${modelText} ${providerBadge}${checkmark}`;
|
|
258
258
|
|
|
259
259
|
this.listContainer.addChild(new Text(line, 0, 0));
|
|
260
260
|
}
|
package/src/output-file.ts
CHANGED
|
@@ -13,6 +13,15 @@ import { appendFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-agent";
|
|
15
15
|
|
|
16
|
+
/** Max length for a truncated command in tool arg summaries. */
|
|
17
|
+
const MAX_COMMAND_DISPLAY_LENGTH = 100;
|
|
18
|
+
|
|
19
|
+
/** Max length for a truncated string value in default tool arg summaries. */
|
|
20
|
+
const MAX_DEFAULT_STRING_DISPLAY_LENGTH = 200;
|
|
21
|
+
|
|
22
|
+
/** Max content length for full tool result display — longer results get a summary line. */
|
|
23
|
+
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
24
|
+
|
|
16
25
|
/** Get an ISO 8601 timestamp string suitable for log output. */
|
|
17
26
|
function timestamp(): string {
|
|
18
27
|
return new Date().toISOString();
|
|
@@ -20,11 +29,14 @@ function timestamp(): string {
|
|
|
20
29
|
|
|
21
30
|
/**
|
|
22
31
|
* Create the output file path for an agent.
|
|
23
|
-
*
|
|
32
|
+
* Default path: /tmp/pi-agent-outputs/<agentId>.log
|
|
24
33
|
* Ensures the parent directory exists with 0o700 permissions.
|
|
34
|
+
*
|
|
35
|
+
* @param baseDir - Optional base directory (defaults to /tmp/pi-agent-outputs).
|
|
36
|
+
* Provided for testability; production callers omit it.
|
|
25
37
|
*/
|
|
26
|
-
export function createOutputFilePath(agentId: string): string {
|
|
27
|
-
const dir = "/tmp/pi-agent-outputs";
|
|
38
|
+
export function createOutputFilePath(agentId: string, baseDir?: string): string {
|
|
39
|
+
const dir = baseDir ?? "/tmp/pi-agent-outputs";
|
|
28
40
|
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
29
41
|
return join(dir, `${agentId}.log`);
|
|
30
42
|
}
|
|
@@ -41,6 +53,14 @@ export function writeInitialEntry(
|
|
|
41
53
|
writeFileSync(path, line, "utf-8");
|
|
42
54
|
}
|
|
43
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Safe append — silently ignores write errors.
|
|
58
|
+
* Used for best-effort output file writes that must never throw.
|
|
59
|
+
*/
|
|
60
|
+
function safeAppend(path: string, content: string): void {
|
|
61
|
+
try { appendFileSync(path, content, "utf-8"); } catch { /* ignore write errors */ }
|
|
62
|
+
}
|
|
63
|
+
|
|
44
64
|
/** Split text into non-empty lines, prefixing each with a timestamp and role tag. */
|
|
45
65
|
function splitAndPrefix(text: string, role: string): string {
|
|
46
66
|
return text
|
|
@@ -50,25 +70,74 @@ function splitAndPrefix(text: string, role: string): string {
|
|
|
50
70
|
.join("");
|
|
51
71
|
}
|
|
52
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Summarize tool arguments for log-friendly display.
|
|
75
|
+
*
|
|
76
|
+
* Heavy tools (read, write, edit, bash, grep, rg) get compact summaries.
|
|
77
|
+
* Other tools fall back to the default JSON formatting.
|
|
78
|
+
*/
|
|
79
|
+
export function summarizeToolArgs(name: string, rawArgs: Record<string, unknown> | undefined): string {
|
|
80
|
+
if (!rawArgs || typeof rawArgs !== "object" || Object.keys(rawArgs).length === 0) return "";
|
|
81
|
+
|
|
82
|
+
switch (name) {
|
|
83
|
+
case "read": {
|
|
84
|
+
// read("/path/to/file") — just the path
|
|
85
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
86
|
+
return `(${JSON.stringify(path)})`;
|
|
87
|
+
}
|
|
88
|
+
case "write": {
|
|
89
|
+
// write("/path/to/file", <N> chars) — path + content size
|
|
90
|
+
const path = typeof rawArgs.file_path === "string" ? rawArgs.file_path : "";
|
|
91
|
+
const content = rawArgs.content;
|
|
92
|
+
const size = typeof content === "string" ? content.length : 0;
|
|
93
|
+
return `(${JSON.stringify(path)}, ${size} chars)`;
|
|
94
|
+
}
|
|
95
|
+
case "edit": {
|
|
96
|
+
// edit("/path/to/file", <N> edits) — path + edit count
|
|
97
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
98
|
+
const edits = rawArgs.edits;
|
|
99
|
+
const editCount = Array.isArray(edits) ? edits.length : 0;
|
|
100
|
+
return `(${JSON.stringify(path)}, ${editCount} edits)`;
|
|
101
|
+
}
|
|
102
|
+
case "bash": {
|
|
103
|
+
// bash("command") — just the command, strip heredoc, truncate long
|
|
104
|
+
const cmd = typeof rawArgs.command === "string" ? rawArgs.command : "";
|
|
105
|
+
// Strip heredoc: truncate at << followed by delimiter
|
|
106
|
+
const heredocIdx = cmd.search(/<<\s*['"]?\w+['"]?/);
|
|
107
|
+
const cleanCmd = heredocIdx >= 0 ? cmd.slice(0, heredocIdx).trim() : cmd.trim();
|
|
108
|
+
// Truncate long commands
|
|
109
|
+
const display = cleanCmd.length > MAX_COMMAND_DISPLAY_LENGTH
|
|
110
|
+
? cleanCmd.slice(0, MAX_COMMAND_DISPLAY_LENGTH) + "…" : cleanCmd;
|
|
111
|
+
return `(${JSON.stringify(display)})`;
|
|
112
|
+
}
|
|
113
|
+
case "grep":
|
|
114
|
+
case "rg": {
|
|
115
|
+
// grep("pattern", "/path") — pattern + path
|
|
116
|
+
const pattern = typeof rawArgs.pattern === "string" ? rawArgs.pattern : "";
|
|
117
|
+
const path = typeof rawArgs.path === "string" ? rawArgs.path : "";
|
|
118
|
+
return `(${JSON.stringify(pattern)}, ${JSON.stringify(path)})`;
|
|
119
|
+
}
|
|
120
|
+
default: {
|
|
121
|
+
// Default behavior for other tools: single-arg shorthand or JSON dump
|
|
122
|
+
const keys = Object.keys(rawArgs);
|
|
123
|
+
if (keys.length === 1) {
|
|
124
|
+
const val = rawArgs[keys[0]];
|
|
125
|
+
const display = typeof val === "string" && val.length > MAX_DEFAULT_STRING_DISPLAY_LENGTH
|
|
126
|
+
? JSON.stringify(val.slice(0, MAX_DEFAULT_STRING_DISPLAY_LENGTH) + "...")
|
|
127
|
+
: JSON.stringify(val);
|
|
128
|
+
return `(${display})`;
|
|
129
|
+
}
|
|
130
|
+
return ` ${JSON.stringify(rawArgs)}`;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
53
135
|
/** Format a toolUse/toolCall content item as a single log line. */
|
|
54
136
|
function formatToolItem(item: Record<string, unknown>): string {
|
|
55
|
-
const name = item.name ?? item.toolName ?? "unknown";
|
|
137
|
+
const name = (item.name ?? item.toolName ?? "unknown") as string;
|
|
56
138
|
// pi-ai ToolCall uses `arguments`, legacy/anthropic format uses `input`
|
|
57
139
|
const rawArgs = (item.arguments ?? item.input) as Record<string, unknown> | undefined;
|
|
58
|
-
|
|
59
|
-
if (rawArgs && typeof rawArgs === "object" && Object.keys(rawArgs).length > 0) {
|
|
60
|
-
const keys = Object.keys(rawArgs);
|
|
61
|
-
if (keys.length === 1) {
|
|
62
|
-
// Single-arg shorthand: read("src/file.ts")
|
|
63
|
-
const val = rawArgs[keys[0]];
|
|
64
|
-
const display = typeof val === "string" && val.length > 200
|
|
65
|
-
? JSON.stringify(val.slice(0, 200) + "...")
|
|
66
|
-
: JSON.stringify(val);
|
|
67
|
-
argsStr = `(${display})`;
|
|
68
|
-
} else {
|
|
69
|
-
argsStr = ` ${JSON.stringify(rawArgs)}`;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
140
|
+
const argsStr = summarizeToolArgs(name, rawArgs);
|
|
72
141
|
return `${timestamp()} [TOOL] ${name}${argsStr}\n`;
|
|
73
142
|
}
|
|
74
143
|
|
|
@@ -81,6 +150,29 @@ function extractUserText(content: string | ReadonlyArray<Record<string, unknown>
|
|
|
81
150
|
return "";
|
|
82
151
|
}
|
|
83
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Format a tool result message as log line(s), truncating if content is too long.
|
|
155
|
+
*
|
|
156
|
+
* - If content length ≤ MAX_TOOL_RESULT_DISPLAY_LENGTH chars: each line is prefixed with [TOOL_RESULT]
|
|
157
|
+
* - If content length > MAX_TOOL_RESULT_DISPLAY_LENGTH chars: single summary line `[TOOL_RESULT] <toolName>: <N> chars`
|
|
158
|
+
*/
|
|
159
|
+
function formatToolResult(toolName: string, content: ReadonlyArray<Record<string, unknown>> | undefined): string {
|
|
160
|
+
if (!content || !Array.isArray(content)) return "";
|
|
161
|
+
|
|
162
|
+
const text = content
|
|
163
|
+
.filter((c): c is { type: "text"; text: string } => c.type === "text" && typeof c.text === "string")
|
|
164
|
+
.map((c) => c.text)
|
|
165
|
+
.join("\n");
|
|
166
|
+
|
|
167
|
+
if (text.length > MAX_TOOL_RESULT_DISPLAY_LENGTH) {
|
|
168
|
+
return `${timestamp()} [TOOL_RESULT] ${toolName}: ${text.length} chars\n`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (!text.trim()) return "";
|
|
172
|
+
|
|
173
|
+
return splitAndPrefix(text, "TOOL_RESULT");
|
|
174
|
+
}
|
|
175
|
+
|
|
84
176
|
/**
|
|
85
177
|
* Format a single message content item as log lines.
|
|
86
178
|
* Handles text, toolUse/toolCall, and thinking content.
|
|
@@ -124,7 +216,7 @@ function formatMessageLine(
|
|
|
124
216
|
export function streamToOutputFile(
|
|
125
217
|
session: AgentSession,
|
|
126
218
|
path: string,
|
|
127
|
-
stats?: { turnCount: number; toolUseCount: number; totalTokens: number },
|
|
219
|
+
stats?: { turnCount: number; toolUseCount: number; totalTokens: number; cost: number },
|
|
128
220
|
): () => void {
|
|
129
221
|
let writtenCount = 1; // initial user prompt already written
|
|
130
222
|
|
|
@@ -134,17 +226,20 @@ export function streamToOutputFile(
|
|
|
134
226
|
const msg = messages[writtenCount];
|
|
135
227
|
if (msg.role === "assistant") {
|
|
136
228
|
const lines = formatMessageLine("ASSISTANT", msg.content as any);
|
|
137
|
-
if (lines)
|
|
138
|
-
try { appendFileSync(path, lines, "utf-8"); } catch { /* ignore write errors */ }
|
|
139
|
-
}
|
|
229
|
+
if (lines) safeAppend(path, lines);
|
|
140
230
|
} else if (msg.role === "user") {
|
|
141
231
|
const text = extractUserText(msg.content as any);
|
|
142
232
|
if (text.trim()) {
|
|
143
|
-
|
|
233
|
+
safeAppend(path, `${timestamp()} [USER] ${text}\n`);
|
|
144
234
|
}
|
|
235
|
+
} else if (msg.role === "toolResult") {
|
|
236
|
+
const msgAny = msg as unknown as Record<string, unknown>;
|
|
237
|
+
const lines = formatToolResult(
|
|
238
|
+
(msgAny.toolName ?? "unknown") as string,
|
|
239
|
+
msgAny.content as ReadonlyArray<Record<string, unknown>> | undefined,
|
|
240
|
+
);
|
|
241
|
+
if (lines) safeAppend(path, lines);
|
|
145
242
|
}
|
|
146
|
-
// NOTE: toolResult messages are enumerated as text content and already
|
|
147
|
-
// included in the assistant message content. No separate TOOL_RESULT role.
|
|
148
243
|
writtenCount++;
|
|
149
244
|
}
|
|
150
245
|
};
|
|
@@ -158,17 +253,12 @@ export function streamToOutputFile(
|
|
|
158
253
|
flush();
|
|
159
254
|
|
|
160
255
|
// Write DONE line
|
|
161
|
-
const { turnCount = 0, toolUseCount = 0, totalTokens = 0 } = stats ?? {};
|
|
256
|
+
const { turnCount = 0, toolUseCount = 0, totalTokens = 0, cost = 0 } = stats ?? {};
|
|
162
257
|
const tokensStr = totalTokens >= 1000
|
|
163
258
|
? `${(totalTokens / 1000).toFixed(1)}k tokens`
|
|
164
259
|
: `${totalTokens} tokens`;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
path,
|
|
168
|
-
`${timestamp()} [DONE] ${turnCount} turns, ${toolUseCount} tool uses, ${tokensStr}\n`,
|
|
169
|
-
"utf-8",
|
|
170
|
-
);
|
|
171
|
-
} catch { /* ignore write errors */ }
|
|
260
|
+
const costStr = `$${cost.toFixed(3)}`;
|
|
261
|
+
safeAppend(path, `${timestamp()} [DONE] ${turnCount} turns, ${toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
172
262
|
|
|
173
263
|
// Unsubscribe from session events
|
|
174
264
|
unsubscribe();
|
package/src/prompts.ts
CHANGED
|
@@ -48,14 +48,14 @@ export function buildAgentPrompt(
|
|
|
48
48
|
extraSections.push(`\n# Preloaded Skill: ${skill.name}\n${skill.content}`);
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
|
-
const extrasSuffix = extraSections.length > 0 ?
|
|
51
|
+
const extrasSuffix = extraSections.length > 0 ? `\n\n${extraSections.join("\n")}` : "";
|
|
52
52
|
|
|
53
53
|
const header = `You are a pi coding agent sub-agent.
|
|
54
54
|
You have been invoked to handle a specific task autonomously.
|
|
55
55
|
|
|
56
56
|
${envBlock}`;
|
|
57
57
|
|
|
58
|
-
return activeAgentTag
|
|
58
|
+
return `${activeAgentTag}${header}\n\n${config.systemPrompt}${extrasSuffix}`;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
|