pi-subagents-lite 1.4.1 → 1.4.3
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 +6 -5
- package/package.json +5 -4
- package/src/agents/agent-discovery.ts +1 -2
- package/src/agents/agent-manager.ts +2 -1
- package/src/agents/agent-runner.ts +2 -9
- package/src/agents/agent-status.ts +2 -2
- package/src/agents/agent-types.ts +22 -20
- package/src/agents/output-file.ts +92 -12
- package/src/agents/tool-execution.ts +11 -13
- package/src/agents/usage.ts +3 -10
- package/src/config/config-io.ts +40 -29
- package/src/config/config-store.ts +17 -19
- package/src/config/types.ts +1 -0
- package/src/events.ts +3 -1
- package/src/models/model-precedence.ts +2 -0
- package/src/spawn/spawn-coordinator.ts +41 -23
- package/src/spawn/worktree-validator.ts +10 -7
- package/src/ui/agent-widget.ts +21 -19
- package/src/ui/format.ts +3 -3
- package/src/ui/menu/helpers.ts +38 -8
- package/src/ui/menu/menu-concurrency.ts +31 -18
- package/src/ui/menu/menu-model-settings.ts +28 -23
- package/src/ui/menu/menu-running-agents.ts +2 -1
- package/src/ui/menu/menu-spawn-options.ts +5 -0
- package/src/ui/menu/menu-spawn-wizard.ts +34 -19
- package/src/ui/menu/menu-system-prompt.ts +6 -2
- package/src/ui/menu/menu-widget-settings.ts +28 -0
- package/src/ui/menu/submenus/confirm.ts +2 -1
- package/src/ui/menu/submenus/model-select.ts +5 -4
- package/src/ui/menu/wrappers/settings-list.ts +73 -105
- package/src/ui/result-viewer.ts +43 -11
- package/src/{models/model-selector.ts → ui/searchable-select.ts} +29 -29
- package/src/utils.ts +2 -0
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Names like `Agent`, `StopAgent`, `AgentStatus`, `run_in_background`, `worktree_p
|
|
|
35
35
|
- **Live widget** — persistent status bar with running/completed agents, full and compact modes
|
|
36
36
|
- **Result viewer** — fullscreen markdown with stats
|
|
37
37
|
- **Worktrees** — run agents in a git worktree via `worktree_path`
|
|
38
|
-
- **Output logs** — `tail -f` friendly, ISO-timestamped
|
|
38
|
+
- **Output logs** — `tail -f` friendly, ISO-timestamped with configurable thinking buffer (OFF, 80, 200, 500, 1000 chars). Flush rounds to sentence boundaries.
|
|
39
39
|
|
|
40
40
|
## Install
|
|
41
41
|
|
|
@@ -112,7 +112,7 @@ Stop a running agent by ID.
|
|
|
112
112
|
|---|---|---|
|
|
113
113
|
| `agent_id` | ✅ | The agent ID returned by `Agent` at spawn |
|
|
114
114
|
|
|
115
|
-
IDs come from the `Agent` result, the `StopAgent` error (lists all running IDs), or `/agents` → **Running agents**.
|
|
115
|
+
IDs come from the `Agent` result, the `StopAgent` error (lists all running IDs), or `/agents` → **Running agents**. Display format is `id (type)` (e.g. `a1b2c3 (Explore)`).
|
|
116
116
|
|
|
117
117
|
### `AgentStatus`
|
|
118
118
|
|
|
@@ -244,13 +244,13 @@ When `includeContextFiles` is `true` (default), AGENTS.md files from the project
|
|
|
244
244
|
Management menu with four sections:
|
|
245
245
|
|
|
246
246
|
- **Running agents** — status and description; per-agent actions (view snapshot, result, error; steer; stop) and bulk stop
|
|
247
|
-
- **Spawn agent** — manually spawn without the LLM. Pick a type, enter a prompt, tune options (model, thinking, max turns, max tokens, grace turns, background), then spawn. Options pre-fill from agent config.
|
|
247
|
+
- **Spawn agent** — manually spawn without the LLM. Pick a type (with search), enter a prompt, tune options (model, thinking, max turns, max tokens, grace turns, background), then spawn. Options pre-fill from agent config.
|
|
248
248
|
- **Settings**
|
|
249
249
|
- **Model settings** — global default, per-type overrides, session overrides, clear all
|
|
250
250
|
- **Spawn options** — force background, grace turns, default max turns, default thinking, disable default agents
|
|
251
251
|
- **System prompt** — mode, custom prompt file, include AGENTS.md, load skills/extensions implicitly
|
|
252
|
-
- **Concurrency** — default limit, per-provider and per-model slots, reset to defaults
|
|
253
|
-
- **Widget settings** — force compact, max lines, description length, ctrl+o shortcut, usage stats (toggle tools, turns, input/output tokens, context %, cost, time)
|
|
252
|
+
- **Concurrency** — default limit, per-provider and per-model slots (with search), reset to defaults
|
|
253
|
+
- **Widget settings** — force compact, max lines, description length, thinking buffer size, ctrl+o shortcut, usage stats (toggle tools, turns, input/output tokens, context %, cost, time)
|
|
254
254
|
|
|
255
255
|
## Interface
|
|
256
256
|
|
|
@@ -333,6 +333,7 @@ With **Cost display** ON, stats show dollar cost (`✓ Builder·2🛠 ·5⟳ ·
|
|
|
333
333
|
| `widgetDescLengthCompact` | `30` | Max description length in compact mode. |
|
|
334
334
|
| `widgetCompact` | `false` | Force compact mode regardless of ctrl+o state. |
|
|
335
335
|
| `widgetShortcut` | `false` | When ON, ctrl+o (tool expansion toggle) syncs with widget compact mode. When OFF, compact is manual via `widgetCompact`. |
|
|
336
|
+
| `outputThinkingBufferSize` | `200` | Thinking buffer ring size in chars. `0` = OFF. Flushes to output log at sentence boundaries. |
|
|
336
337
|
|
|
337
338
|
### Stats visibility
|
|
338
339
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-subagents-lite",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Lightweight sub-agents for pi — spawn specialized agents with isolated sessions, tools, and models.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -28,11 +28,12 @@
|
|
|
28
28
|
"url": "https://github.com/AlexParamonov/pi-subagents-lite/issues"
|
|
29
29
|
},
|
|
30
30
|
"peerDependencies": {
|
|
31
|
-
"@earendil-works/pi-ai": "
|
|
32
|
-
"@earendil-works/pi-coding-agent": "
|
|
33
|
-
"@earendil-works/pi-tui": "
|
|
31
|
+
"@earendil-works/pi-ai": "^0.80.1",
|
|
32
|
+
"@earendil-works/pi-coding-agent": "^0.80.1",
|
|
33
|
+
"@earendil-works/pi-tui": "^0.80.1"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
+
"@earendil-works/pi-agent-core": "^0.80.1",
|
|
36
37
|
"@sinclair/typebox": "^0.34.49"
|
|
37
38
|
},
|
|
38
39
|
"files": [
|
|
@@ -257,7 +257,6 @@ function compactDefined<T extends Record<string, unknown>>(obj: T): Partial<T> {
|
|
|
257
257
|
*/
|
|
258
258
|
export function parseAgentFile(
|
|
259
259
|
content: string,
|
|
260
|
-
_filename: string,
|
|
261
260
|
source: "user" | "project",
|
|
262
261
|
): AgentConfigFromMd {
|
|
263
262
|
const { frontmatter, body } = parseFrontmatter(content);
|
|
@@ -310,7 +309,7 @@ export async function scanAgentFilesInDir(
|
|
|
310
309
|
const filePath = path.join(dirPath, entry.name);
|
|
311
310
|
try {
|
|
312
311
|
const content = await fs.promises.readFile(filePath, "utf-8");
|
|
313
|
-
const info = parseAgentFile(content,
|
|
312
|
+
const info = parseAgentFile(content, source);
|
|
314
313
|
if (info.name) {
|
|
315
314
|
agents.push(info);
|
|
316
315
|
}
|
|
@@ -98,6 +98,7 @@ export class AgentManager {
|
|
|
98
98
|
onComplete?: OnAgentComplete,
|
|
99
99
|
concurrency?: ConcurrencyConfig,
|
|
100
100
|
onStart?: OnAgentStart,
|
|
101
|
+
private bufferSize: number = 0,
|
|
101
102
|
) {
|
|
102
103
|
this.onComplete = onComplete;
|
|
103
104
|
this.onStart = onStart;
|
|
@@ -258,7 +259,7 @@ export class AgentManager {
|
|
|
258
259
|
record.lifecycle.startedAt = Date.now();
|
|
259
260
|
|
|
260
261
|
// Create output log for this agent (creates file + writes [USER] entry)
|
|
261
|
-
record.execution.outputLog = new AgentOutputLog(id, prompt);
|
|
262
|
+
record.execution.outputLog = new AgentOutputLog(id, prompt, undefined, this.bufferSize);
|
|
262
263
|
record.display.outputFile = record.execution.outputLog.path;
|
|
263
264
|
|
|
264
265
|
this.onStart?.(record);
|
|
@@ -21,21 +21,14 @@ import {
|
|
|
21
21
|
import { getAgentConfig, getConfig, getToolNamesForType, resolveVisibleTools } from "./agent-types.js";
|
|
22
22
|
import { extractText } from "../prompt/context.js";
|
|
23
23
|
import type { LifetimeUsage } from "./usage.js";
|
|
24
|
-
import { findModelInRegistry } from "../utils.js";
|
|
24
|
+
import { findModelInRegistry, GIT_EXEC_TIMEOUT_MS } from "../utils.js";
|
|
25
25
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
26
26
|
import { buildAgentPrompt, type PromptExtras } from "../prompt/prompts.js";
|
|
27
27
|
import { preloadSkills, loadSkillMeta, type SkillMeta } from "../prompt/skill-loader.js";
|
|
28
28
|
import { type EnvInfo, type RunCallbacks, type RunTunables, SHORT_ID_LENGTH } from "../types.js";
|
|
29
29
|
import type { SubagentType, SystemPromptMode } from "./types.js";
|
|
30
30
|
import { getStore } from "../shell.js";
|
|
31
|
-
import { DEFAULT_GRACE_TURNS } from "../config/config-io.js";
|
|
32
|
-
|
|
33
|
-
/** Path to custom prompt file. Exported for use in menus.ts. */
|
|
34
|
-
export const CUSTOM_PROMPT_PATH = path.join(process.env.HOME || "", ".pi", "agent", "subagents-lite-prompt.md");
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
/** Timeout for quick git commands (branch detection, repo check). */
|
|
38
|
-
const GIT_EXEC_TIMEOUT_MS = 5000;
|
|
31
|
+
import { DEFAULT_GRACE_TURNS, CUSTOM_PROMPT_PATH } from "../config/config-io.js";
|
|
39
32
|
|
|
40
33
|
/** Normalize max turns. undefined or 0 = unlimited, otherwise minimum 1. */
|
|
41
34
|
function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
@@ -12,11 +12,11 @@ import { SHORT_ID_LENGTH } from "../types.js";
|
|
|
12
12
|
import { getManager } from "../shell.js";
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Format a single agent record as "type
|
|
15
|
+
* Format a single agent record as "short_id (type) status".
|
|
16
16
|
*/
|
|
17
17
|
function formatAgent(record: AgentRecord): string {
|
|
18
18
|
const shortId = record.id.slice(0, SHORT_ID_LENGTH);
|
|
19
|
-
return `${record.display.type}
|
|
19
|
+
return `${shortId} (${record.display.type}) ${record.lifecycle.status}`;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
/**
|
|
@@ -66,6 +66,15 @@ export function setAgentScanDirs(userDir: string, projectDir: string): void {
|
|
|
66
66
|
projectAgentDir = projectDir;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
+
/** Scan user and project agent directories, merge with defaults. Returns the merged Map. */
|
|
70
|
+
async function scanAndMerge(options?: { disableDefaultAgents?: boolean }): Promise<Map<string, AgentConfig>> {
|
|
71
|
+
const [userAgents, projectAgents] = await Promise.all([
|
|
72
|
+
scanAgentFilesInDir(userAgentDir, "user"),
|
|
73
|
+
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
74
|
+
]);
|
|
75
|
+
const defaults = options?.disableDefaultAgents ? new Map<string, AgentConfig>() : DEFAULT_AGENTS;
|
|
76
|
+
return mergeAgents(defaults, userAgents, projectAgents);
|
|
77
|
+
}
|
|
69
78
|
/**
|
|
70
79
|
* Scan the known agent directories and register any newly discovered agents
|
|
71
80
|
* that aren't already in the registry. Returns the number of new agents added.
|
|
@@ -77,13 +86,7 @@ export function setAgentScanDirs(userDir: string, projectDir: string): void {
|
|
|
77
86
|
* @param options - Optional settings. disableDefaultAgents skips DEFAULT_AGENTS in the merge.
|
|
78
87
|
*/
|
|
79
88
|
export async function discoverNewAgents(worktreeDir?: string, options?: { disableDefaultAgents?: boolean }): Promise<number> {
|
|
80
|
-
const
|
|
81
|
-
scanAgentFilesInDir(userAgentDir, "user"),
|
|
82
|
-
scanAgentFilesInDir(projectAgentDir, "project"),
|
|
83
|
-
]);
|
|
84
|
-
|
|
85
|
-
const defaults = options?.disableDefaultAgents ? new Map<string, AgentConfig>() : DEFAULT_AGENTS;
|
|
86
|
-
const merged = mergeAgents(defaults, userAgents, projectAgents);
|
|
89
|
+
const merged = await scanAndMerge(options);
|
|
87
90
|
|
|
88
91
|
let count = 0;
|
|
89
92
|
for (const [name, config] of merged) {
|
|
@@ -96,8 +99,6 @@ export async function discoverNewAgents(worktreeDir?: string, options?: { disabl
|
|
|
96
99
|
// Scan worktree-local agents (only when worktreeDir is provided)
|
|
97
100
|
if (worktreeDir) {
|
|
98
101
|
const worktreeAgents = await scanAgentFilesInDir(worktreeDir, "project");
|
|
99
|
-
// Use mergeAgents to convert AgentConfigFromMd to AgentConfig (applies fromMd
|
|
100
|
-
// and BASE_DEFAULTS), then add only names not already in the registry.
|
|
101
102
|
const wtMerged = mergeAgents(new Map(), [], worktreeAgents);
|
|
102
103
|
for (const [name, config] of wtMerged) {
|
|
103
104
|
if (!agents.has(name)) {
|
|
@@ -302,22 +303,23 @@ function applyGlobalDefaults(
|
|
|
302
303
|
};
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
/** Find the first non-hidden config: resolved type, then general-purpose, then undefined. */
|
|
307
|
+
function findActiveConfig(type: string): AgentConfig | undefined {
|
|
308
|
+
const key = resolveType(type);
|
|
309
|
+
const config = key ? agents.get(key) : undefined;
|
|
310
|
+
if (config?.hidden !== true) return config;
|
|
311
|
+
return agents.get("general-purpose");
|
|
312
|
+
}
|
|
313
|
+
|
|
305
314
|
/** Get config for a type (case-insensitive). Falls back to general-purpose. */
|
|
306
315
|
export function getConfig(
|
|
307
316
|
type: string,
|
|
308
317
|
loadSkillsImplicitly: boolean = true,
|
|
309
318
|
loadExtensionsImplicitly: boolean = true,
|
|
310
319
|
): ResolvedAgentConfig {
|
|
311
|
-
const
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
// If config exists and is not hidden, use it; otherwise fall back to general-purpose
|
|
315
|
-
const activeConfig = config?.hidden !== true
|
|
316
|
-
? config
|
|
317
|
-
: agents.get("general-purpose");
|
|
318
|
-
|
|
319
|
-
if (activeConfig && activeConfig.hidden !== true) {
|
|
320
|
-
const { skills, extensions, ...rest } = activeConfig;
|
|
320
|
+
const config = findActiveConfig(type);
|
|
321
|
+
if (config) {
|
|
322
|
+
const { skills, extensions, ...rest } = config;
|
|
321
323
|
const defaults = applyGlobalDefaults(skills, extensions, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
322
324
|
return {
|
|
323
325
|
displayName: rest.displayName ?? rest.name,
|
|
@@ -328,7 +330,7 @@ export function getConfig(
|
|
|
328
330
|
};
|
|
329
331
|
}
|
|
330
332
|
|
|
331
|
-
// Absolute fallback —
|
|
333
|
+
// Absolute fallback — no config found at all
|
|
332
334
|
const defaults = applyGlobalDefaults(undefined, undefined, loadSkillsImplicitly, loadExtensionsImplicitly);
|
|
333
335
|
return {
|
|
334
336
|
displayName: "Agent",
|
|
@@ -12,6 +12,26 @@ import type { AgentSession, AgentSessionEvent } from "@earendil-works/pi-coding-
|
|
|
12
12
|
import { formatTokens } from "./usage.js";
|
|
13
13
|
import { summarizeToolArgs } from "../ui/format.js";
|
|
14
14
|
|
|
15
|
+
|
|
16
|
+
/** Find the last sentence boundary in text. Returns the index of the
|
|
17
|
+
* terminal punctuation character, or -1 if none found. */
|
|
18
|
+
function findLastSentenceBoundary(text: string): number {
|
|
19
|
+
// Search backward for the most recent sentence-ending punctuation
|
|
20
|
+
for (let i = text.length - 1; i >= 0; i--) {
|
|
21
|
+
const ch = text[i];
|
|
22
|
+
if ([".", "!", "?", ",", "\n"].includes(ch)) {
|
|
23
|
+
return i;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return -1;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Format the [DONE] summary line with final stats. */
|
|
30
|
+
function formatDoneLine(stats: { turnCount: number; toolUseCount: number; totalTokens: number; cost: number }): string {
|
|
31
|
+
const tokensStr = `${formatTokens(stats.totalTokens)} tokens`;
|
|
32
|
+
const costStr = `$${stats.cost.toFixed(3)}`;
|
|
33
|
+
return `${timestamp()} [DONE] ${stats.turnCount} turns, ${stats.toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`;
|
|
34
|
+
}
|
|
15
35
|
/** Max content length for full tool result display — longer results get a summary line. */
|
|
16
36
|
const MAX_TOOL_RESULT_DISPLAY_LENGTH = 500;
|
|
17
37
|
|
|
@@ -111,12 +131,14 @@ function formatToolResult(toolName: string, content: ReadonlyArray<Record<string
|
|
|
111
131
|
function formatMessageLine(
|
|
112
132
|
role: "ASSISTANT" | "TOOL" | "USER",
|
|
113
133
|
content: string | ReadonlyArray<Record<string, unknown>> | undefined,
|
|
134
|
+
skipThinkingCount: number = 0,
|
|
114
135
|
): string {
|
|
115
136
|
if (typeof content === "string") {
|
|
116
137
|
return splitAndPrefix(content, role);
|
|
117
138
|
}
|
|
118
139
|
|
|
119
140
|
if (Array.isArray(content)) {
|
|
141
|
+
let thinkingSkipped = 0;
|
|
120
142
|
return content
|
|
121
143
|
.map((item) => {
|
|
122
144
|
if (item.type === "text" && typeof item.text === "string") {
|
|
@@ -126,6 +148,10 @@ function formatMessageLine(
|
|
|
126
148
|
return formatToolItem(item);
|
|
127
149
|
}
|
|
128
150
|
if (item.type === "thinking" && typeof item.thinking === "string") {
|
|
151
|
+
if (thinkingSkipped < skipThinkingCount) {
|
|
152
|
+
thinkingSkipped++;
|
|
153
|
+
return ""; // Already streamed, skip
|
|
154
|
+
}
|
|
129
155
|
const text = item.redacted ? "[redacted]" : item.thinking;
|
|
130
156
|
return splitAndPrefix(text, "THINKING");
|
|
131
157
|
}
|
|
@@ -136,7 +162,6 @@ function formatMessageLine(
|
|
|
136
162
|
|
|
137
163
|
return "";
|
|
138
164
|
}
|
|
139
|
-
|
|
140
165
|
/**
|
|
141
166
|
* Subscribe to session events and flush new messages to the output file
|
|
142
167
|
* on each turn_end. Returns a cleanup function that writes the DONE line
|
|
@@ -148,15 +173,28 @@ export function streamToOutputFile(
|
|
|
148
173
|
session: AgentSession,
|
|
149
174
|
path: string,
|
|
150
175
|
stats?: { turnCount: number; toolUseCount: number; totalTokens: number; cost: number },
|
|
176
|
+
bufferSize: number = 0,
|
|
151
177
|
): () => void {
|
|
152
178
|
let writtenCount = 1; // initial user prompt already written
|
|
179
|
+
let thinkingBuffer = "";
|
|
180
|
+
let streamedThinkingBlocks = 0; // thinking blocks written live; skipped in the final flush
|
|
181
|
+
let streamedThinkingChars = 0; // track total chars streamed for deduplication
|
|
182
|
+
let thinkingBlockInProgress = false; // true between thinking_start and thinking_end
|
|
183
|
+
|
|
184
|
+
const flushThinkingBuffer = () => {
|
|
185
|
+
if (thinkingBuffer.length > 0) {
|
|
186
|
+
safeAppend(path, `${timestamp()} [THINKING] ${thinkingBuffer}\n`);
|
|
187
|
+
streamedThinkingChars += thinkingBuffer.length;
|
|
188
|
+
thinkingBuffer = "";
|
|
189
|
+
}
|
|
190
|
+
};
|
|
153
191
|
|
|
154
192
|
const flush = () => {
|
|
155
193
|
const messages = session.messages;
|
|
156
194
|
while (writtenCount < messages.length) {
|
|
157
195
|
const msg = messages[writtenCount];
|
|
158
196
|
if (msg.role === "assistant") {
|
|
159
|
-
const lines = formatMessageLine("ASSISTANT", msg.content as any);
|
|
197
|
+
const lines = formatMessageLine("ASSISTANT", msg.content as any, streamedThinkingBlocks);
|
|
160
198
|
if (lines) safeAppend(path, lines);
|
|
161
199
|
} else if (msg.role === "user") {
|
|
162
200
|
const text = extractUserText(msg.content as any);
|
|
@@ -176,18 +214,60 @@ export function streamToOutputFile(
|
|
|
176
214
|
};
|
|
177
215
|
|
|
178
216
|
const unsubscribe = session.subscribe((event: AgentSessionEvent) => {
|
|
179
|
-
if (event.type === "turn_end")
|
|
217
|
+
if (event.type === "turn_end") {
|
|
218
|
+
flushThinkingBuffer();
|
|
219
|
+
// If thinking_end never fired, treat this as if it did to avoid duplicates
|
|
220
|
+
if (thinkingBlockInProgress) {
|
|
221
|
+
streamedThinkingBlocks++;
|
|
222
|
+
thinkingBlockInProgress = false;
|
|
223
|
+
}
|
|
224
|
+
flush();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (bufferSize > 0 && event.type === "message_update") {
|
|
228
|
+
const assistantEvent = event.assistantMessageEvent;
|
|
229
|
+
if (assistantEvent.type === "thinking_start") {
|
|
230
|
+
// Reset counter for new thinking block
|
|
231
|
+
streamedThinkingChars = 0;
|
|
232
|
+
thinkingBlockInProgress = true;
|
|
233
|
+
} else if (assistantEvent.type === "thinking_delta") {
|
|
234
|
+
thinkingBuffer += assistantEvent.delta;
|
|
235
|
+
if (thinkingBuffer.length >= bufferSize) {
|
|
236
|
+
// Round down to nearest sentence boundary when possible
|
|
237
|
+
const boundary = findLastSentenceBoundary(thinkingBuffer);
|
|
238
|
+
if (boundary >= 0) {
|
|
239
|
+
const flushText = thinkingBuffer.slice(0, boundary + 1);
|
|
240
|
+
thinkingBuffer = thinkingBuffer.slice(boundary + 1);
|
|
241
|
+
safeAppend(path, `${timestamp()} [THINKING] ${flushText}\n`);
|
|
242
|
+
streamedThinkingChars += flushText.length;
|
|
243
|
+
} else {
|
|
244
|
+
// No sentence boundary found, flush at buffer limit
|
|
245
|
+
flushThinkingBuffer();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
} else if (assistantEvent.type === "thinking_end") {
|
|
249
|
+
// thinking_end carries the full block. Flush the buffered tail first
|
|
250
|
+
// (counted in streamedThinkingChars), then stream whatever remains.
|
|
251
|
+
flushThinkingBuffer();
|
|
252
|
+
if (assistantEvent.content && assistantEvent.content.length > streamedThinkingChars) {
|
|
253
|
+
const remaining = assistantEvent.content.slice(streamedThinkingChars);
|
|
254
|
+
safeAppend(path, `${timestamp()} [THINKING] ${remaining}\n`);
|
|
255
|
+
streamedThinkingChars = assistantEvent.content.length;
|
|
256
|
+
}
|
|
257
|
+
streamedThinkingBlocks++;
|
|
258
|
+
thinkingBlockInProgress = false;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
180
261
|
});
|
|
181
262
|
|
|
182
263
|
return () => {
|
|
183
264
|
// Final flush
|
|
265
|
+
flushThinkingBuffer();
|
|
184
266
|
flush();
|
|
185
267
|
|
|
186
268
|
// Write DONE line
|
|
187
|
-
const { turnCount
|
|
188
|
-
|
|
189
|
-
const costStr = `$${cost.toFixed(3)}`;
|
|
190
|
-
safeAppend(path, `${timestamp()} [DONE] ${turnCount} turns, ${toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
269
|
+
const doneStats = stats ?? { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
|
|
270
|
+
safeAppend(path, formatDoneLine(doneStats));
|
|
191
271
|
|
|
192
272
|
// Unsubscribe from session events
|
|
193
273
|
unsubscribe();
|
|
@@ -219,10 +299,12 @@ export class AgentOutputLog {
|
|
|
219
299
|
readonly path: string;
|
|
220
300
|
private cleanup?: () => void;
|
|
221
301
|
private statsRef?: OutputFinalStats;
|
|
302
|
+
private bufferSize: number;
|
|
222
303
|
|
|
223
|
-
constructor(agentId: string, prompt: string, baseDir?: string) {
|
|
304
|
+
constructor(agentId: string, prompt: string, baseDir?: string, bufferSize: number = 0) {
|
|
224
305
|
this.path = createOutputFilePath(agentId, baseDir);
|
|
225
306
|
writeInitialEntry(this.path, prompt);
|
|
307
|
+
this.bufferSize = bufferSize;
|
|
226
308
|
}
|
|
227
309
|
|
|
228
310
|
/**
|
|
@@ -232,7 +314,7 @@ export class AgentOutputLog {
|
|
|
232
314
|
*/
|
|
233
315
|
attach(session: AgentSession): void {
|
|
234
316
|
this.statsRef = { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
|
|
235
|
-
this.cleanup = streamToOutputFile(session, this.path, this.statsRef);
|
|
317
|
+
this.cleanup = streamToOutputFile(session, this.path, this.statsRef, this.bufferSize);
|
|
236
318
|
}
|
|
237
319
|
|
|
238
320
|
/**
|
|
@@ -254,9 +336,7 @@ export class AgentOutputLog {
|
|
|
254
336
|
this.statsRef = undefined;
|
|
255
337
|
} else {
|
|
256
338
|
// No attach was called — write DONE directly
|
|
257
|
-
|
|
258
|
-
const costStr = `$${stats.cost.toFixed(3)}`;
|
|
259
|
-
safeAppend(this.path, `${timestamp()} [DONE] ${stats.turnCount} turns, ${stats.toolUseCount} tool uses, ${tokensStr}, ${costStr}\n`);
|
|
339
|
+
safeAppend(this.path, formatDoneLine(stats));
|
|
260
340
|
}
|
|
261
341
|
}
|
|
262
342
|
}
|
|
@@ -52,16 +52,9 @@ function errorResult(text: string, details?: Record<string, unknown>) {
|
|
|
52
52
|
* Consolidates the identical field-selection logic previously duplicated
|
|
53
53
|
* across emitIndividualNudge, executeSpawnForeground, and executeSpawnBackground.
|
|
54
54
|
*/
|
|
55
|
-
interface AgentDetailsOptions {
|
|
56
|
-
/** Include full stats (turns, tokens, context%, compactions, cost). Default: false. */
|
|
57
|
-
includeStats?: boolean;
|
|
58
|
-
/** Include status and outputFile. Default: false. */
|
|
59
|
-
includeStatus?: boolean;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
55
|
export function buildAgentDetails(
|
|
63
56
|
record: AgentRecord,
|
|
64
|
-
|
|
57
|
+
opts?: { includeStats?: boolean; includeStatus?: boolean },
|
|
65
58
|
): Record<string, unknown> {
|
|
66
59
|
const details: Record<string, unknown> = {
|
|
67
60
|
type: record.display.type,
|
|
@@ -72,12 +65,12 @@ export function buildAgentDetails(
|
|
|
72
65
|
details.worktreePath = record.display.worktreePath;
|
|
73
66
|
}
|
|
74
67
|
|
|
75
|
-
if (
|
|
68
|
+
if (opts?.includeStatus) {
|
|
76
69
|
details.status = record.lifecycle.status;
|
|
77
70
|
details.outputFile = record.display.outputFile;
|
|
78
71
|
}
|
|
79
72
|
|
|
80
|
-
if (
|
|
73
|
+
if (opts?.includeStats) {
|
|
81
74
|
const elapsedMs = record.lifecycle.completedAt ? record.lifecycle.completedAt - record.lifecycle.startedAt : 0;
|
|
82
75
|
|
|
83
76
|
details.turnCount = record.stats.turnCount;
|
|
@@ -113,8 +106,13 @@ export async function executeAgentTool(
|
|
|
113
106
|
if (rawWorktreePath && rawWorktreePath.trim() !== "") {
|
|
114
107
|
try {
|
|
115
108
|
const parentCwd = getSessionCtx()?.cwd ?? ctx.cwd;
|
|
116
|
-
const
|
|
109
|
+
const warnings: string[] = [];
|
|
110
|
+
const onWarning = (msg: string) => { warnings.push(msg); };
|
|
111
|
+
const validation = await validateWorktreePath(getPiInstance(), rawWorktreePath, parentCwd, onWarning);
|
|
117
112
|
if (!validation.ok) {
|
|
113
|
+
for (const msg of warnings) {
|
|
114
|
+
if (ctx.ui?.notify) ctx.ui.notify(`[pi-subagents-lite] ${msg}`, "warning");
|
|
115
|
+
}
|
|
118
116
|
return errorResult(validation.error);
|
|
119
117
|
}
|
|
120
118
|
validatedWorktreePath = validation.resolvedPath;
|
|
@@ -198,7 +196,7 @@ export async function executeAgentTool(
|
|
|
198
196
|
|
|
199
197
|
/**
|
|
200
198
|
* Build a compact list of running (or queued) agents.
|
|
201
|
-
* Format: "type
|
|
199
|
+
* Format: "short_id (type), short_id (type)" — one line, easy for LLM to parse.
|
|
202
200
|
*/
|
|
203
201
|
function formatRunningAgents(): string {
|
|
204
202
|
const agents = getManager()!.listAgents().filter(
|
|
@@ -208,7 +206,7 @@ function formatRunningAgents(): string {
|
|
|
208
206
|
if (agents.length === 0) return "none";
|
|
209
207
|
|
|
210
208
|
return agents
|
|
211
|
-
.map((a) => `${a.
|
|
209
|
+
.map((a) => `${a.id.slice(0, SHORT_ID_LENGTH)} (${a.display.type})`)
|
|
212
210
|
.join(", ");
|
|
213
211
|
}
|
|
214
212
|
|
package/src/agents/usage.ts
CHANGED
|
@@ -29,17 +29,10 @@ type SessionStatsLike = {
|
|
|
29
29
|
};
|
|
30
30
|
export type SessionLike = { getSessionStats(): SessionStatsLike };
|
|
31
31
|
|
|
32
|
-
/** Format a token count compactly: "12.3k", "1.2M", or raw number. */
|
|
33
|
-
export function formatTokens(count: number): string {
|
|
32
|
+
/** Format a token count compactly: "12.3k", "1.2M", or raw number. When compact is true, thousands round to whole numbers. */
|
|
33
|
+
export function formatTokens(count: number, compact = false): string {
|
|
34
34
|
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
35
|
-
if (count >= 1_000) return `${(count / 1_000).toFixed(1)}k`;
|
|
36
|
-
return `${count}`;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Format token count for widget display: rounded to whole number for k. */
|
|
40
|
-
export function formatTokensCompact(count: number): string {
|
|
41
|
-
if (count >= 1_000_000) return `${(count / 1_000_000).toFixed(1)}M`;
|
|
42
|
-
if (count >= 1_000) return `${Math.round(count / 1_000)}k`;
|
|
35
|
+
if (count >= 1_000) return compact ? `${Math.round(count / 1_000)}k` : `${(count / 1_000).toFixed(1)}k`;
|
|
43
36
|
return `${count}`;
|
|
44
37
|
}
|
|
45
38
|
|
package/src/config/config-io.ts
CHANGED
|
@@ -11,46 +11,57 @@ import type { SubagentsConfig } from "../models/model-precedence.js";
|
|
|
11
11
|
|
|
12
12
|
const CONFIG_DIR = path.join(process.env.HOME || "", ".pi", "agent");
|
|
13
13
|
const CONFIG_PATH = path.join(CONFIG_DIR, "subagents-lite.json");
|
|
14
|
-
|
|
14
|
+
/** Path to custom prompt file for subagent system prompts. */
|
|
15
|
+
export const CUSTOM_PROMPT_PATH = path.join(CONFIG_DIR, "subagents-lite-prompt.md");
|
|
15
16
|
/** Default number of grace turns before an agent is force-stopped. */
|
|
16
17
|
export const DEFAULT_GRACE_TURNS = 6;
|
|
17
18
|
|
|
18
|
-
/**
|
|
19
|
-
export const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
19
|
+
/** Valid system prompt modes. */
|
|
20
|
+
export const VALID_SYSTEM_PROMPT_MODES = new Set<string>(["replace", "inherit", "custom"]);
|
|
21
|
+
|
|
22
|
+
/** Default concurrency config — used for resets. */
|
|
23
|
+
export const DEFAULT_CONCURRENCY: SubagentsConfig["concurrency"] = { default: 4 };
|
|
24
|
+
|
|
25
|
+
/** Default agent settings — merged into loaded config so callers get a complete shape. */
|
|
26
|
+
const DEFAULT_AGENT: SubagentsConfig["agent"] = {
|
|
27
|
+
default: null,
|
|
28
|
+
forceBackground: false,
|
|
29
|
+
graceTurns: DEFAULT_GRACE_TURNS,
|
|
30
|
+
widgetMaxLines: 12,
|
|
31
|
+
widgetDescLengthFull: 50,
|
|
32
|
+
widgetDescLengthCompact: 30,
|
|
33
|
+
widgetCompact: false,
|
|
34
|
+
widgetShortcut: false,
|
|
35
|
+
systemPromptMode: "replace",
|
|
36
|
+
includeContextFiles: true,
|
|
37
|
+
disableDefaultAgents: false,
|
|
38
|
+
showTools: true,
|
|
39
|
+
showTurns: true,
|
|
40
|
+
showInput: true,
|
|
41
|
+
showOutput: true,
|
|
42
|
+
showContext: true,
|
|
43
|
+
showCost: false,
|
|
44
|
+
showTime: true,
|
|
42
45
|
};
|
|
43
46
|
|
|
44
47
|
/**
|
|
45
|
-
* Read config from disk.
|
|
48
|
+
* Read config from disk. Merges loaded values over defaults so the result
|
|
49
|
+
* is always a complete SubagentsConfig — no partial shapes for callers to handle.
|
|
46
50
|
*/
|
|
47
51
|
export function loadConfig(): SubagentsConfig {
|
|
52
|
+
let raw: SubagentsConfig;
|
|
48
53
|
try {
|
|
49
|
-
|
|
50
|
-
return JSON.parse(raw) as SubagentsConfig;
|
|
54
|
+
raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as SubagentsConfig;
|
|
51
55
|
} catch {
|
|
52
|
-
|
|
56
|
+
raw = {} as SubagentsConfig;
|
|
53
57
|
}
|
|
58
|
+
|
|
59
|
+
// @ts-expect-error TS2783: spread may override 'default', which is intentional (loaded value wins)
|
|
60
|
+
const concurrency = { default: 4, ...(raw.concurrency ?? {}) } as SubagentsConfig["concurrency"];
|
|
61
|
+
return {
|
|
62
|
+
agent: { ...DEFAULT_AGENT, ...raw.agent },
|
|
63
|
+
concurrency,
|
|
64
|
+
};
|
|
54
65
|
}
|
|
55
66
|
|
|
56
67
|
/** Write config to disk with atomic rename. */
|