pi-subagents-lite 1.3.0 → 1.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +184 -235
- package/package.json +1 -1
- package/src/{agent-discovery.ts → agents/agent-discovery.ts} +10 -7
- package/src/{agent-manager.ts → agents/agent-manager.ts} +34 -74
- package/src/{agent-runner.ts → agents/agent-runner.ts} +130 -181
- package/src/{agent-status.ts → agents/agent-status.ts} +4 -4
- package/src/agents/agent-types.ts +339 -0
- package/src/{default-agents.ts → agents/default-agents.ts} +2 -5
- package/src/{output-file.ts → agents/output-file.ts} +68 -1
- package/src/{tool-execution.ts → agents/tool-execution.ts} +60 -222
- package/src/agents/types.ts +54 -0
- package/src/{usage.ts → agents/usage.ts} +7 -0
- package/src/{config-io.ts → config/config-io.ts} +20 -3
- package/src/config/config-store.ts +472 -0
- package/src/config/types.ts +26 -0
- package/src/events.ts +185 -0
- package/src/index.ts +8 -281
- package/src/{model-precedence.ts → models/model-precedence.ts} +33 -0
- package/src/{model-selector.ts → models/model-selector.ts} +1 -1
- package/src/{context.ts → prompt/context.ts} +1 -1
- package/src/prompt/prompts.ts +180 -0
- package/src/prompt/skill-loader.ts +195 -0
- package/src/registration.ts +101 -0
- package/src/shell.ts +101 -0
- package/src/spawn/spawn-coordinator.ts +232 -0
- package/src/status-note.ts +10 -0
- package/src/types.ts +47 -71
- package/src/ui/agent-widget.ts +61 -49
- package/src/{format.ts → ui/format.ts} +64 -26
- package/src/ui/menu/helpers.ts +93 -0
- package/src/ui/menu/menu-concurrency.ts +192 -0
- package/src/ui/menu/menu-debug.ts +125 -0
- package/src/ui/menu/menu-model-settings.ts +208 -0
- package/src/ui/menu/menu-running-agents.ts +224 -0
- package/src/ui/menu/menu-spawn-options.ts +87 -0
- package/src/ui/menu/menu-spawn-wizard.ts +418 -0
- package/src/ui/menu/menu-system-prompt.ts +109 -0
- package/src/ui/menu/menu-widget-settings.ts +130 -0
- package/src/ui/menu/menus.ts +101 -0
- package/src/ui/menu/submenus/confirm.ts +47 -0
- package/src/ui/menu/submenus/model-select.ts +70 -0
- package/src/ui/menu/submenus/numeric-input.ts +98 -0
- package/src/ui/menu/wrappers/settings-list.ts +205 -0
- package/src/{renderer.ts → ui/renderer.ts} +7 -6
- package/src/{result-viewer.ts → ui/result-viewer.ts} +7 -2
- package/src/ui/types.ts +11 -0
- package/src/agent-types.ts +0 -184
- package/src/config-mutator.ts +0 -183
- package/src/menus.ts +0 -1333
- package/src/prompts.ts +0 -94
- package/src/skill-loader.ts +0 -178
- package/src/state.ts +0 -83
- /package/src/{worktree-validator.ts → spawn/worktree-validator.ts} +0 -0
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
import * as fs from "node:fs";
|
|
13
13
|
import * as path from "node:path";
|
|
14
|
-
import type { AgentConfig
|
|
15
|
-
import {
|
|
14
|
+
import type { AgentConfig } from "./types.js";
|
|
15
|
+
import type { ThinkingLevel } from "../types.js";
|
|
16
|
+
import { parseThinkingLevel } from "../utils.js";
|
|
16
17
|
|
|
17
18
|
/* ------------------------------------------------------------------ */
|
|
18
19
|
/* Types */
|
|
@@ -32,6 +33,7 @@ export interface AgentConfigFromMd {
|
|
|
32
33
|
model?: string;
|
|
33
34
|
thinking?: ThinkingLevel;
|
|
34
35
|
max_turns?: number;
|
|
36
|
+
max_tokens?: number;
|
|
35
37
|
hidden?: boolean;
|
|
36
38
|
systemPrompt: string;
|
|
37
39
|
source: "user" | "project";
|
|
@@ -128,11 +130,11 @@ function parseFrontmatter(
|
|
|
128
130
|
/* parseExtensions */
|
|
129
131
|
/* ------------------------------------------------------------------ */
|
|
130
132
|
|
|
131
|
-
/** Split comma-separated string, trim whitespace, and remove empty entries. */
|
|
133
|
+
/** Split comma-separated string, trim whitespace, strip brackets, and remove empty entries. */
|
|
132
134
|
function splitCommaList(value: string): string[] {
|
|
133
135
|
return value
|
|
134
136
|
.split(",")
|
|
135
|
-
.map((s) => s.trim())
|
|
137
|
+
.map((s) => s.trim().replace(/^\[|\]$/g, "").trim())
|
|
136
138
|
.filter((s) => s.length > 0);
|
|
137
139
|
}
|
|
138
140
|
|
|
@@ -273,6 +275,7 @@ export function parseAgentFile(
|
|
|
273
275
|
model: parseString(frontmatter, "model"),
|
|
274
276
|
thinking: parseThinkingLevel(parseString(frontmatter, "thinking")),
|
|
275
277
|
max_turns: parseNumber(frontmatter, "max_turns"),
|
|
278
|
+
max_tokens: parseNumber(frontmatter, "max_tokens"),
|
|
276
279
|
hidden: parseBoolean(frontmatter, "hidden"),
|
|
277
280
|
systemPrompt: body,
|
|
278
281
|
source: source,
|
|
@@ -397,8 +400,9 @@ function fromMd(md: AgentConfigFromMd): Partial<AgentConfig> {
|
|
|
397
400
|
skills: md.skills,
|
|
398
401
|
preloadSkills: md.preload_skills,
|
|
399
402
|
model: md.model,
|
|
400
|
-
|
|
403
|
+
thinkingLevel: md.thinking,
|
|
401
404
|
maxTurns: md.max_turns,
|
|
405
|
+
maxTokens: md.max_tokens,
|
|
402
406
|
hidden: md.hidden,
|
|
403
407
|
systemPrompt: md.systemPrompt,
|
|
404
408
|
source: md.source === "project" ? "project" : "global",
|
|
@@ -414,7 +418,6 @@ function fromMd(md: AgentConfigFromMd): Partial<AgentConfig> {
|
|
|
414
418
|
const BASE_DEFAULTS: AgentConfig = {
|
|
415
419
|
name: "unknown",
|
|
416
420
|
description: "",
|
|
417
|
-
extensions
|
|
418
|
-
skills: true,
|
|
421
|
+
// extensions and skills intentionally omitted — resolved by global default
|
|
419
422
|
systemPrompt: "",
|
|
420
423
|
};
|
|
@@ -5,21 +5,21 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
9
8
|
import type { AgentSession, ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
|
-
import { runAgent
|
|
11
|
-
import {
|
|
9
|
+
import { runAgent } from "./agent-runner.js";
|
|
10
|
+
import { AgentOutputLog } from "./output-file.js";
|
|
12
11
|
import {
|
|
13
|
-
type AgentInvocation,
|
|
14
12
|
type AgentRecord,
|
|
15
13
|
type AgentStatus,
|
|
16
14
|
type CompactionInfo,
|
|
15
|
+
type RunCallbacks,
|
|
17
16
|
SHORT_ID_LENGTH,
|
|
18
|
-
type
|
|
19
|
-
type
|
|
20
|
-
} from "
|
|
17
|
+
type SpawnConfig,
|
|
18
|
+
type ToolActivity,
|
|
19
|
+
} from "../types.js";
|
|
20
|
+
import type { SubagentType } from "./types.js";
|
|
21
21
|
import { addUsage, getLifetimeTotal, getSessionContextPercent, type LifetimeUsage } from "./usage.js";
|
|
22
|
-
import { errorMessage } from "
|
|
22
|
+
import { errorMessage } from "../utils.js";
|
|
23
23
|
|
|
24
24
|
/** How often to check for expired agent records (milliseconds). */
|
|
25
25
|
const CLEANUP_INTERVAL_MS = 60_000;
|
|
@@ -35,27 +35,6 @@ const AGENT_ID_PREFIX_LENGTH = 17;
|
|
|
35
35
|
/** Default per-model concurrency limit when not specified in config. */
|
|
36
36
|
const DEFAULT_CONCURRENCY_LIMIT = 4;
|
|
37
37
|
|
|
38
|
-
/**
|
|
39
|
-
* Create a cleanup function for the output file stream.
|
|
40
|
-
* Captures final stats from the record at cleanup time so the DONE line
|
|
41
|
-
* reflects actual turn count, tool uses, and total tokens.
|
|
42
|
-
*/
|
|
43
|
-
function createOutputCleanup(
|
|
44
|
-
session: AgentSession,
|
|
45
|
-
path: string,
|
|
46
|
-
record: AgentRecord,
|
|
47
|
-
): () => void {
|
|
48
|
-
const outputStats = { turnCount: 0, toolUseCount: 0, totalTokens: 0, cost: 0 };
|
|
49
|
-
const cleanup = streamToOutputFile(session, path, outputStats);
|
|
50
|
-
return () => {
|
|
51
|
-
outputStats.turnCount = record.stats.turnCount ?? 0;
|
|
52
|
-
outputStats.toolUseCount = record.stats.toolUses;
|
|
53
|
-
outputStats.totalTokens = getLifetimeTotal(record.stats.lifetimeUsage);
|
|
54
|
-
outputStats.cost = record.stats.lifetimeUsage.cost;
|
|
55
|
-
cleanup();
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
38
|
/** Whether the agent status is terminal (no longer running or queued). */
|
|
60
39
|
function isTerminalStatus(status: AgentStatus): boolean {
|
|
61
40
|
return status !== "running" && status !== "queued";
|
|
@@ -88,40 +67,10 @@ interface SpawnArgs {
|
|
|
88
67
|
options: SpawnOptions;
|
|
89
68
|
}
|
|
90
69
|
|
|
91
|
-
export interface SpawnOptions {
|
|
92
|
-
description: string;
|
|
93
|
-
model?: Model<any>;
|
|
94
|
-
maxTurns?: number;
|
|
95
|
-
thinkingLevel?: ThinkingLevel;
|
|
70
|
+
export interface SpawnOptions extends SpawnConfig, RunCallbacks {
|
|
96
71
|
isBackground?: boolean;
|
|
97
|
-
/** Resolved worktree path — forwarded as cwd to runAgent. */
|
|
98
|
-
worktreePath?: string;
|
|
99
|
-
/** Short display label for the worktree (set on record display after spawn). */
|
|
100
|
-
worktreeLabel?: string;
|
|
101
|
-
/**
|
|
102
|
-
* Model key for concurrency pool lookup (e.g. "llamacpp/4b_small").
|
|
103
|
-
* When set, the agent is counted against that model's concurrency limit.
|
|
104
|
-
* When unset, the agent bypasses per-model concurrency limits.
|
|
105
|
-
*/
|
|
106
|
-
modelKey?: string;
|
|
107
|
-
/** Resolved invocation snapshot captured for UI display. */
|
|
108
|
-
invocation?: AgentInvocation;
|
|
109
72
|
/** Parent abort signal — when aborted, the subagent is also stopped. */
|
|
110
73
|
signal?: AbortSignal;
|
|
111
|
-
/** Called on tool start/end with activity info (for streaming progress to UI). */
|
|
112
|
-
onToolActivity?: (activity: ToolActivity) => void;
|
|
113
|
-
/** Called on streaming text deltas from the assistant response. */
|
|
114
|
-
onTextDelta?: (delta: string, fullText: string) => void;
|
|
115
|
-
/** Called when the agent session is created (for accessing session stats). */
|
|
116
|
-
onSessionCreated?: (session: AgentSession) => void;
|
|
117
|
-
/** Called at the end of each agentic turn with the cumulative count. */
|
|
118
|
-
onTurnEnd?: (turnCount: number) => void;
|
|
119
|
-
/** Called once per assistant message_end with that message's usage delta. */
|
|
120
|
-
onAssistantUsage?: (usage: LifetimeUsage) => void;
|
|
121
|
-
/** Called when the session successfully compacts. */
|
|
122
|
-
onCompaction?: (info: CompactionInfo) => void;
|
|
123
|
-
/** Grace turns: extra turns allowed after hitting maxTurns. */
|
|
124
|
-
graceTurns?: number;
|
|
125
74
|
}
|
|
126
75
|
|
|
127
76
|
export class AgentManager {
|
|
@@ -273,6 +222,7 @@ export class AgentManager {
|
|
|
273
222
|
stats: {
|
|
274
223
|
lifetimeUsage: { input: 0, output: 0, cacheWrite: 0, cost: 0 },
|
|
275
224
|
toolUses: 0,
|
|
225
|
+
turnCount: 1,
|
|
276
226
|
compactionCount: 0,
|
|
277
227
|
maxTurns: options.maxTurns,
|
|
278
228
|
},
|
|
@@ -307,9 +257,9 @@ export class AgentManager {
|
|
|
307
257
|
record.lifecycle.status = "running";
|
|
308
258
|
record.lifecycle.startedAt = Date.now();
|
|
309
259
|
|
|
310
|
-
// Create output
|
|
311
|
-
record.
|
|
312
|
-
|
|
260
|
+
// Create output log for this agent (creates file + writes [USER] entry)
|
|
261
|
+
record.execution.outputLog = new AgentOutputLog(id, prompt);
|
|
262
|
+
record.display.outputFile = record.execution.outputLog.path;
|
|
313
263
|
|
|
314
264
|
this.onStart?.(record);
|
|
315
265
|
|
|
@@ -323,6 +273,7 @@ export class AgentManager {
|
|
|
323
273
|
agentId: id,
|
|
324
274
|
model: options.model,
|
|
325
275
|
maxTurns: options.maxTurns,
|
|
276
|
+
maxTokens: options.maxTokens,
|
|
326
277
|
thinkingLevel: options.thinkingLevel,
|
|
327
278
|
cwd: options.worktreePath,
|
|
328
279
|
graceTurns: options.graceTurns,
|
|
@@ -345,19 +296,17 @@ export class AgentManager {
|
|
|
345
296
|
}
|
|
346
297
|
record.execution.pendingSteers = undefined;
|
|
347
298
|
}
|
|
348
|
-
//
|
|
349
|
-
if (record.
|
|
350
|
-
record.execution.
|
|
351
|
-
session, record.display.outputFile, record,
|
|
352
|
-
);
|
|
299
|
+
// Attach output log stream to session
|
|
300
|
+
if (record.execution.outputLog) {
|
|
301
|
+
record.execution.outputLog.attach(session);
|
|
353
302
|
}
|
|
354
303
|
options.onSessionCreated?.(session);
|
|
355
304
|
},
|
|
356
305
|
})
|
|
357
|
-
.then(({ responseText, session, aborted,
|
|
306
|
+
.then(({ responseText, session, aborted, turnLimited }) => {
|
|
358
307
|
// Don't overwrite status if externally stopped via abort()
|
|
359
308
|
if (record.lifecycle.status !== "stopped") {
|
|
360
|
-
record.lifecycle.status = aborted ? "aborted" :
|
|
309
|
+
record.lifecycle.status = aborted ? "aborted" : turnLimited ? "turn_limited" : "completed";
|
|
361
310
|
}
|
|
362
311
|
record.result = responseText;
|
|
363
312
|
record.execution.session = session;
|
|
@@ -375,10 +324,17 @@ export class AgentManager {
|
|
|
375
324
|
return "";
|
|
376
325
|
})
|
|
377
326
|
.finally(() => {
|
|
378
|
-
//
|
|
379
|
-
if (record.execution.
|
|
380
|
-
try {
|
|
381
|
-
|
|
327
|
+
// Finalize output log with final stats
|
|
328
|
+
if (record.execution.outputLog) {
|
|
329
|
+
try {
|
|
330
|
+
record.execution.outputLog.finalize({
|
|
331
|
+
turnCount: record.stats.turnCount ?? 0,
|
|
332
|
+
toolUseCount: record.stats.toolUses,
|
|
333
|
+
totalTokens: getLifetimeTotal(record.stats.lifetimeUsage),
|
|
334
|
+
cost: record.stats.lifetimeUsage.cost,
|
|
335
|
+
});
|
|
336
|
+
} catch { /* ignore */ }
|
|
337
|
+
record.execution.outputLog = undefined;
|
|
382
338
|
}
|
|
383
339
|
|
|
384
340
|
// Decrement per-model concurrency count
|
|
@@ -397,6 +353,10 @@ export class AgentManager {
|
|
|
397
353
|
try { this.onComplete?.(record); } catch { /* ignore */ }
|
|
398
354
|
}
|
|
399
355
|
|
|
356
|
+
setOnComplete(cb: OnAgentComplete): void {
|
|
357
|
+
this.onComplete = cb;
|
|
358
|
+
}
|
|
359
|
+
|
|
400
360
|
/** Get the session-level cumulative agent cost. Survives agent eviction. */
|
|
401
361
|
getTotalAgentCost(): number {
|
|
402
362
|
return this.totalAgentCost;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Core execution engine: creates sessions, runs agents, collects results.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Tool visibility policy is owned by agent-types.ts (resolveVisibleTools).
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import fs from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
|
-
import type { Model } from "@earendil-works/pi-ai";
|
|
9
9
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import {
|
|
11
11
|
type AgentSession,
|
|
@@ -14,23 +14,25 @@ import {
|
|
|
14
14
|
DefaultResourceLoader,
|
|
15
15
|
type ExtensionAPI,
|
|
16
16
|
getAgentDir,
|
|
17
|
+
loadProjectContextFiles,
|
|
17
18
|
SessionManager,
|
|
18
19
|
SettingsManager,
|
|
19
20
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import { getAgentConfig, getConfig, getToolNamesForType,
|
|
21
|
-
import { extractText } from "
|
|
21
|
+
import { getAgentConfig, getConfig, getToolNamesForType, resolveVisibleTools } from "./agent-types.js";
|
|
22
|
+
import { extractText } from "../prompt/context.js";
|
|
22
23
|
import type { LifetimeUsage } from "./usage.js";
|
|
23
|
-
import { findModelInRegistry } from "
|
|
24
|
+
import { findModelInRegistry } from "../utils.js";
|
|
24
25
|
import { DEFAULT_AGENTS } from "./default-agents.js";
|
|
25
|
-
import { buildAgentPrompt, type PromptExtras } from "
|
|
26
|
-
import { preloadSkills, loadSkillMeta, type SkillMeta } from "
|
|
27
|
-
import { type
|
|
26
|
+
import { buildAgentPrompt, type PromptExtras } from "../prompt/prompts.js";
|
|
27
|
+
import { preloadSkills, loadSkillMeta, type SkillMeta } from "../prompt/skill-loader.js";
|
|
28
|
+
import { type EnvInfo, type RunCallbacks, type RunTunables, SHORT_ID_LENGTH } from "../types.js";
|
|
29
|
+
import type { SubagentType, SystemPromptMode } from "./types.js";
|
|
30
|
+
import { getStore } from "../shell.js";
|
|
31
|
+
import { DEFAULT_GRACE_TURNS } from "../config/config-io.js";
|
|
28
32
|
|
|
29
|
-
/**
|
|
30
|
-
const
|
|
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");
|
|
31
35
|
|
|
32
|
-
/** Default grace turns when not specified in config. */
|
|
33
|
-
const DEFAULT_GRACE_TURNS = 6;
|
|
34
36
|
|
|
35
37
|
/** Timeout for quick git commands (branch detection, repo check). */
|
|
36
38
|
const GIT_EXEC_TIMEOUT_MS = 5000;
|
|
@@ -42,42 +44,15 @@ function normalizeMaxTurns(n: number | undefined): number | undefined {
|
|
|
42
44
|
}
|
|
43
45
|
|
|
44
46
|
/** Info about a tool event in the subagent. */
|
|
45
|
-
|
|
46
|
-
type: "start" | "end";
|
|
47
|
-
toolName: string;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
interface RunOptions {
|
|
47
|
+
interface RunOptions extends RunTunables, RunCallbacks {
|
|
51
48
|
/** ExtensionAPI instance — used for pi.exec() for git detection. */
|
|
52
49
|
pi: ExtensionAPI;
|
|
53
50
|
/** Manager-assigned id; suffixes session name to disambiguate parallel spawns (e.g. `Explore#a1b2c3d4`). */
|
|
54
51
|
agentId?: string;
|
|
55
|
-
|
|
56
|
-
maxTurns?: number;
|
|
57
|
-
signal?: AbortSignal;
|
|
58
|
-
thinkingLevel?: ThinkingLevel;
|
|
59
|
-
/** Override working directory. */
|
|
52
|
+
/** Override working directory (resolved worktree path). */
|
|
60
53
|
cwd?: string;
|
|
61
|
-
/**
|
|
62
|
-
|
|
63
|
-
/** Called on streaming text deltas from the assistant response. */
|
|
64
|
-
onTextDelta?: (delta: string, fullText: string) => void;
|
|
65
|
-
onSessionCreated?: (session: AgentSession) => void;
|
|
66
|
-
/** Called at the end of each agentic turn with the cumulative count. */
|
|
67
|
-
onTurnEnd?: (turnCount: number) => void;
|
|
68
|
-
/**
|
|
69
|
-
* Called once per assistant message_end with that message's usage delta.
|
|
70
|
-
* Lets callers maintain a lifetime accumulator that survives compaction
|
|
71
|
-
* (which replaces session.state.messages and resets stats-derived sums).
|
|
72
|
-
*/
|
|
73
|
-
onAssistantUsage?: (usage: LifetimeUsage) => void;
|
|
74
|
-
/**
|
|
75
|
-
* Called when the session successfully compacts. `tokensBefore` is upstream's
|
|
76
|
-
* pre-compaction context size estimate. Aborted compactions don't fire.
|
|
77
|
-
*/
|
|
78
|
-
onCompaction?: (info: CompactionInfo) => void;
|
|
79
|
-
/** Grace turns: extra turns allowed after hitting maxTurns. Defaults to 6. */
|
|
80
|
-
graceTurns?: number;
|
|
54
|
+
/** Parent abort signal — when aborted, the subagent is also stopped. */
|
|
55
|
+
signal?: AbortSignal;
|
|
81
56
|
}
|
|
82
57
|
|
|
83
58
|
interface RunResult {
|
|
@@ -85,8 +60,8 @@ interface RunResult {
|
|
|
85
60
|
session: AgentSession;
|
|
86
61
|
/** True if the agent was hard-aborted (max_turns + grace exceeded). */
|
|
87
62
|
aborted: boolean;
|
|
88
|
-
/** True if the agent
|
|
89
|
-
|
|
63
|
+
/** True if the agent hit the soft turn limit and wrapped up within grace turns. */
|
|
64
|
+
turnLimited: boolean;
|
|
90
65
|
}
|
|
91
66
|
|
|
92
67
|
/**
|
|
@@ -229,120 +204,6 @@ function extractExtensionName(extPath: string): string {
|
|
|
229
204
|
return path.basename(path.dirname(extPath));
|
|
230
205
|
}
|
|
231
206
|
|
|
232
|
-
/**
|
|
233
|
-
* Resolve tool entries (with ext/* syntax) into concrete tool names.
|
|
234
|
-
* Returns a set of resolved tool names.
|
|
235
|
-
*/
|
|
236
|
-
function resolveToolEntries(
|
|
237
|
-
entries: string[],
|
|
238
|
-
extToolMap: Map<string, string[]> | undefined,
|
|
239
|
-
notify?: (msg: string) => void,
|
|
240
|
-
): Set<string> {
|
|
241
|
-
const resolved = new Set<string>();
|
|
242
|
-
|
|
243
|
-
for (const entry of entries) {
|
|
244
|
-
const slashIdx = entry.indexOf("/");
|
|
245
|
-
if (slashIdx !== -1) {
|
|
246
|
-
// ext/* or ext/tool syntax
|
|
247
|
-
const extName = entry.slice(0, slashIdx);
|
|
248
|
-
const toolPart = entry.slice(slashIdx + 1);
|
|
249
|
-
if (toolPart === "*") {
|
|
250
|
-
const extTools = extToolMap?.get(extName);
|
|
251
|
-
if (extTools && extTools.length > 0) {
|
|
252
|
-
for (const t of extTools) resolved.add(t);
|
|
253
|
-
} else {
|
|
254
|
-
notify?.(`extension "${extName}" is not loaded, "${entry}" will have no effect`);
|
|
255
|
-
}
|
|
256
|
-
} else {
|
|
257
|
-
// ext/tool syntax: e.g. "tavily/web_search"
|
|
258
|
-
resolved.add(toolPart);
|
|
259
|
-
}
|
|
260
|
-
} else {
|
|
261
|
-
// Bare tool name
|
|
262
|
-
resolved.add(entry);
|
|
263
|
-
}
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return resolved;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Filter active tools: apply tools allowlist/denylist and EXCLUDED_TOOL_NAMES.
|
|
271
|
-
*
|
|
272
|
-
* The `tools` config controls which tool schemas the LLM sees (built-in + extension).
|
|
273
|
-
* The `extensions` config controls which extensions are loaded (hooks + commands).
|
|
274
|
-
* `extensions` does NOT affect tool visibility — that's `tools`'s job.
|
|
275
|
-
*
|
|
276
|
-
* Supports ext/* syntax for both whitelist and blacklist modes.
|
|
277
|
-
*
|
|
278
|
-
* `tools` and `excludeTools` are mutually exclusive. If both set, `tools` wins.
|
|
279
|
-
*
|
|
280
|
-
* Returns null when no filtering is needed, otherwise the filtered tool list.
|
|
281
|
-
*/
|
|
282
|
-
function filterActiveTools(
|
|
283
|
-
activeTools: string[],
|
|
284
|
-
extToolMap: Map<string, string[]> | undefined,
|
|
285
|
-
tools: true | string[] | false | undefined,
|
|
286
|
-
excludeTools: string[] | undefined,
|
|
287
|
-
notify?: (msg: string) => void,
|
|
288
|
-
): string[] | null {
|
|
289
|
-
// Blacklist mode: excludeTools set and tools not set as whitelist
|
|
290
|
-
if (excludeTools && !Array.isArray(tools)) {
|
|
291
|
-
const excludeSet = resolveToolEntries(excludeTools, extToolMap, notify);
|
|
292
|
-
const filtered = activeTools.filter(t =>
|
|
293
|
-
!EXCLUDED_TOOL_NAMES.includes(t) && !excludeSet.has(t)
|
|
294
|
-
);
|
|
295
|
-
return filtered.length !== activeTools.length ? filtered : null;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
if (Array.isArray(tools)) {
|
|
299
|
-
// Whitelist mode: resolve entries with ext/* expansion
|
|
300
|
-
const allBuiltinSet = new Set(BUILTIN_TOOL_NAMES);
|
|
301
|
-
const allowedTools = resolveToolEntries(tools, extToolMap, notify);
|
|
302
|
-
|
|
303
|
-
// Warn about unknown entries
|
|
304
|
-
for (const entry of tools) {
|
|
305
|
-
const slashIdx = entry.indexOf("/");
|
|
306
|
-
if (slashIdx === -1 && !allBuiltinSet.has(entry)) {
|
|
307
|
-
// Bare name, not a known built-in — check if it's an extension tool
|
|
308
|
-
const toolExts = extToolMap ? [...extToolMap.entries()].filter(([, tools]) => tools.includes(entry)) : [];
|
|
309
|
-
if (toolExts.length === 0) {
|
|
310
|
-
notify?.(`tool "${entry}" not found in any loaded extension`);
|
|
311
|
-
}
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const visibleSet = new Set<string>();
|
|
316
|
-
for (const t of activeTools) {
|
|
317
|
-
if (EXCLUDED_TOOL_NAMES.includes(t)) continue;
|
|
318
|
-
if (allowedTools.has(t)) {
|
|
319
|
-
visibleSet.add(t);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
// Warn if a loaded extension has none of its tools in `tools`
|
|
324
|
-
if (extToolMap) {
|
|
325
|
-
for (const [extName, extTools] of extToolMap) {
|
|
326
|
-
const hasAny = extTools.some(t => allowedTools.has(t));
|
|
327
|
-
if (!hasAny) {
|
|
328
|
-
notify?.(`extension "${extName}" is loaded but none of its tools are in tools: [${tools.join(", ")}]`);
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return [...visibleSet];
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
if (tools === false) {
|
|
337
|
-
return [];
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// tools: true or undefined — all tools visible (except excluded)
|
|
341
|
-
const hasExcluded = activeTools.some(t => EXCLUDED_TOOL_NAMES.includes(t));
|
|
342
|
-
if (!hasExcluded) return null;
|
|
343
|
-
return activeTools.filter(t => !EXCLUDED_TOOL_NAMES.includes(t));
|
|
344
|
-
}
|
|
345
|
-
|
|
346
207
|
/** Run a git command via pi.exec, returning stdout on success or null on failure. */
|
|
347
208
|
async function execGit(pi: ExtensionAPI, args: string[], cwd: string): Promise<string | null> {
|
|
348
209
|
try {
|
|
@@ -371,8 +232,62 @@ async function detectEnv(pi: ExtensionAPI, cwd: string): Promise<EnvInfo> {
|
|
|
371
232
|
|
|
372
233
|
// ── runAgent phases ────────────────────────────────────────────────
|
|
373
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Resolve system prompt mode, fetch the appropriate source prompt, and
|
|
237
|
+
* load project context files. Returns everything buildPrompt needs.
|
|
238
|
+
*/
|
|
239
|
+
function resolveSystemPromptSources(
|
|
240
|
+
ctx: ExtensionContext,
|
|
241
|
+
cwd: string,
|
|
242
|
+
notify: (msg: string) => void,
|
|
243
|
+
): { mode: SystemPromptMode; extras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> } {
|
|
244
|
+
const store = getStore();
|
|
245
|
+
const mode = store.agent.systemPromptMode;
|
|
246
|
+
const extras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> = {};
|
|
247
|
+
|
|
248
|
+
// Fetch parent system prompt for inherit mode
|
|
249
|
+
if (mode === "inherit") {
|
|
250
|
+
try {
|
|
251
|
+
extras.parentSystemPrompt = ctx.getSystemPrompt();
|
|
252
|
+
} catch (err) {
|
|
253
|
+
notify(`Failed to get parent system prompt: ${err}. Falling back to replace mode.`);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Read custom prompt file for custom mode
|
|
258
|
+
if (mode === "custom") {
|
|
259
|
+
try {
|
|
260
|
+
const content = fs.readFileSync(CUSTOM_PROMPT_PATH, "utf-8").trim();
|
|
261
|
+
if (content) {
|
|
262
|
+
extras.customSystemPrompt = content;
|
|
263
|
+
} else {
|
|
264
|
+
notify(`Custom prompt file is empty: ${CUSTOM_PROMPT_PATH}. Falling back to replace mode.`);
|
|
265
|
+
}
|
|
266
|
+
} catch (err: any) {
|
|
267
|
+
if (err.code === "ENOENT") {
|
|
268
|
+
notify(`Custom prompt file not found: ${CUSTOM_PROMPT_PATH}. Falling back to replace mode.`);
|
|
269
|
+
} else {
|
|
270
|
+
notify(`Failed to read custom prompt file: ${err.message}. Falling back to replace mode.`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Load AGENTS.md context files when the setting is enabled
|
|
276
|
+
if (store.agent.includeContextFiles) {
|
|
277
|
+
try {
|
|
278
|
+
extras.contextFiles = loadProjectContextFiles({ cwd, agentDir: getAgentDir() });
|
|
279
|
+
} catch {
|
|
280
|
+
// Non-fatal: context files are supplementary
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return { mode, extras };
|
|
285
|
+
}
|
|
286
|
+
|
|
374
287
|
/**
|
|
375
288
|
* Phase 1: Resolve system prompt from agent config, skills, and env info.
|
|
289
|
+
*
|
|
290
|
+
* @param resolverExtras Partial extras from resolveSystemPromptSources (mode-specific prompts + context files).
|
|
376
291
|
*/
|
|
377
292
|
function buildPrompt(
|
|
378
293
|
type: SubagentType,
|
|
@@ -380,8 +295,10 @@ function buildPrompt(
|
|
|
380
295
|
config: ReturnType<typeof getConfig>,
|
|
381
296
|
cwd: string,
|
|
382
297
|
env: EnvInfo,
|
|
298
|
+
systemPromptMode: SystemPromptMode = "replace",
|
|
299
|
+
resolverExtras: Pick<PromptExtras, "parentSystemPrompt" | "customSystemPrompt" | "contextFiles"> = {},
|
|
383
300
|
): string {
|
|
384
|
-
const extras: PromptExtras = {};
|
|
301
|
+
const extras: PromptExtras = { ...resolverExtras };
|
|
385
302
|
if (Array.isArray(agentConfig?.preloadSkills)) {
|
|
386
303
|
extras.skillBlocks = preloadSkills(agentConfig.preloadSkills, cwd);
|
|
387
304
|
}
|
|
@@ -389,11 +306,11 @@ function buildPrompt(
|
|
|
389
306
|
extras.skillMetas = loadSkillMeta(config.skills, cwd);
|
|
390
307
|
}
|
|
391
308
|
if (agentConfig) {
|
|
392
|
-
return buildAgentPrompt(agentConfig, cwd, env, extras);
|
|
309
|
+
return buildAgentPrompt(agentConfig, cwd, env, extras, systemPromptMode);
|
|
393
310
|
}
|
|
394
311
|
const fallback = DEFAULT_AGENTS.get("general-purpose");
|
|
395
312
|
if (!fallback) throw new Error(`No fallback config available for unknown type "${type}"`);
|
|
396
|
-
return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras);
|
|
313
|
+
return buildAgentPrompt({ ...fallback, name: type }, cwd, env, extras, systemPromptMode);
|
|
397
314
|
}
|
|
398
315
|
|
|
399
316
|
/** Build extension name → tool names map from loaded extensions. */
|
|
@@ -482,7 +399,7 @@ async function initSession(
|
|
|
482
399
|
const model = options.model ?? findModelInRegistry(
|
|
483
400
|
agentConfig?.model, ctx.modelRegistry, ctx.model,
|
|
484
401
|
);
|
|
485
|
-
const thinkingLevel = options.thinkingLevel ?? agentConfig?.
|
|
402
|
+
const thinkingLevel = options.thinkingLevel ?? agentConfig?.thinkingLevel;
|
|
486
403
|
const agentDir = getAgentDir();
|
|
487
404
|
const sessionOpts: Parameters<typeof createAgentSession>[0] = {
|
|
488
405
|
cwd, agentDir,
|
|
@@ -492,7 +409,22 @@ async function initSession(
|
|
|
492
409
|
tools: getToolNamesForType(type), resourceLoader: loader,
|
|
493
410
|
};
|
|
494
411
|
if (thinkingLevel) sessionOpts.thinkingLevel = thinkingLevel;
|
|
495
|
-
|
|
412
|
+
const result = await createAgentSession(sessionOpts);
|
|
413
|
+
|
|
414
|
+
// Inject max_tokens into provider request payloads.
|
|
415
|
+
// Spawn-time value wins over agent config (frontmatter).
|
|
416
|
+
const maxTokens = options.maxTokens ?? agentConfig?.maxTokens;
|
|
417
|
+
if (maxTokens != null && maxTokens > 0 && model) {
|
|
418
|
+
const field = (model.compat as any)?.maxTokensField ?? "max_tokens";
|
|
419
|
+
const origOnPayload = result.session.agent.onPayload;
|
|
420
|
+
result.session.agent.onPayload = async (payload, m) => {
|
|
421
|
+
const applied = origOnPayload ? (await origOnPayload(payload, m)) ?? payload : payload;
|
|
422
|
+
const obj = typeof applied === "object" && applied && !Array.isArray(applied) ? applied : {};
|
|
423
|
+
return { ...obj, [field]: maxTokens };
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return result;
|
|
496
428
|
}
|
|
497
429
|
|
|
498
430
|
/**
|
|
@@ -518,10 +450,13 @@ async function createAndConfigureSession(
|
|
|
518
450
|
type: "end", toolName: `extension-error:${err.extensionPath}`,
|
|
519
451
|
}),
|
|
520
452
|
});
|
|
521
|
-
const filteredTools =
|
|
522
|
-
session.getActiveToolNames(),
|
|
523
|
-
|
|
524
|
-
|
|
453
|
+
const filteredTools = resolveVisibleTools({
|
|
454
|
+
activeTools: session.getActiveToolNames(),
|
|
455
|
+
tools: agentConfig?.tools,
|
|
456
|
+
excludeTools: agentConfig?.excludeTools,
|
|
457
|
+
extToolMap: buildExtToolMap(extResult.extensions),
|
|
458
|
+
notify,
|
|
459
|
+
});
|
|
525
460
|
if (filteredTools) session.setActiveToolsByName(filteredTools);
|
|
526
461
|
options.onSessionCreated?.(session);
|
|
527
462
|
return session;
|
|
@@ -555,7 +490,7 @@ function wireTurnTracking(
|
|
|
555
490
|
}
|
|
556
491
|
});
|
|
557
492
|
|
|
558
|
-
return { unsubscribe, getAborted: () => aborted,
|
|
493
|
+
return { unsubscribe, getAborted: () => aborted, getTurnLimited: () => softLimitReached };
|
|
559
494
|
}
|
|
560
495
|
|
|
561
496
|
/**
|
|
@@ -589,35 +524,49 @@ export async function runAgent(
|
|
|
589
524
|
prompt: string,
|
|
590
525
|
options: RunOptions,
|
|
591
526
|
): Promise<RunResult> {
|
|
592
|
-
const
|
|
527
|
+
const store = getStore();
|
|
528
|
+
const config = getConfig(type, store.agent.loadSkillsImplicitly, store.agent.loadExtensionsImplicitly);
|
|
593
529
|
const agentConfig = getAgentConfig(type);
|
|
594
530
|
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
};
|
|
531
|
+
// Buffer warnings during setup to avoid inserting custom_message entries
|
|
532
|
+
// between tool_use and tool_result in the session tree (causes Anthropic 400).
|
|
533
|
+
// Flushed after runTurnLoop completes.
|
|
534
|
+
const warnings: string[] = [];
|
|
535
|
+
const bufferNotify = (msg: string) => { warnings.push(msg); };
|
|
600
536
|
if (agentConfig?.excludeTools && Array.isArray(agentConfig.tools)) {
|
|
601
|
-
|
|
537
|
+
bufferNotify(`agent "${type}": both tools and exclude_tools set — tools (whitelist) wins`);
|
|
602
538
|
}
|
|
603
539
|
if (agentConfig?.excludeExtensions && Array.isArray(agentConfig.extensions)) {
|
|
604
|
-
|
|
540
|
+
bufferNotify(`agent "${type}": both extensions and exclude_extensions set — extensions (whitelist) wins`);
|
|
605
541
|
}
|
|
606
542
|
|
|
607
543
|
const effectiveCwd = options.cwd ?? ctx.cwd;
|
|
608
544
|
const env = await detectEnv(options.pi, effectiveCwd);
|
|
609
545
|
|
|
610
|
-
|
|
546
|
+
// Resolve system prompt mode + source prompts + context files
|
|
547
|
+
const { mode, extras: promptExtras } = resolveSystemPromptSources(ctx, effectiveCwd, bufferNotify);
|
|
548
|
+
|
|
549
|
+
const systemPrompt = buildPrompt(
|
|
550
|
+
type, agentConfig, config, effectiveCwd, env,
|
|
551
|
+
mode, promptExtras,
|
|
552
|
+
);
|
|
611
553
|
const { loader, reloadAndMap } = createResourceLoader(config, agentConfig, effectiveCwd, systemPrompt);
|
|
612
554
|
const { extResult } = await reloadAndMap();
|
|
613
555
|
const session = await createAndConfigureSession(
|
|
614
|
-
ctx, options, agentConfig, type, effectiveCwd, loader, extResult,
|
|
556
|
+
ctx, options, agentConfig, type, effectiveCwd, loader, extResult, bufferNotify,
|
|
615
557
|
);
|
|
616
|
-
const { unsubscribe: unsubTurns, getAborted,
|
|
558
|
+
const { unsubscribe: unsubTurns, getAborted, getTurnLimited } = wireTurnTracking(session, {
|
|
617
559
|
...options,
|
|
618
560
|
maxTurns: options.maxTurns ?? agentConfig?.maxTurns,
|
|
619
561
|
});
|
|
620
562
|
|
|
621
563
|
const responseText = await runTurnLoop(session, prompt, options, unsubTurns);
|
|
622
|
-
|
|
564
|
+
|
|
565
|
+
// Flush buffered warnings now that tool_result is in the session tree.
|
|
566
|
+
for (const msg of warnings) {
|
|
567
|
+
if (ctx.ui?.notify) ctx.ui.notify(`[pi-subagents-lite] ${msg}`, "warning");
|
|
568
|
+
else console.warn(`[pi-subagents-lite] ${msg}`);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return { responseText, session, aborted: getAborted(), turnLimited: getTurnLimited() };
|
|
623
572
|
}
|