lsd-pi 1.2.2 → 1.2.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/dist/resources/extensions/slash-commands/plan.js +11 -7
- package/dist/resources/extensions/subagent/agents.js +17 -8
- package/dist/resources/extensions/subagent/index.js +112 -20
- package/package.json +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +1 -0
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.js +53 -3
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js +15 -0
- package/packages/pi-coding-agent/dist/core/skill-tool.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/skills.js +1 -0
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/system-prompt.js +1 -0
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/path-display.test.js +1 -0
- package/packages/pi-coding-agent/dist/tests/path-display.test.js.map +1 -1
- package/packages/pi-coding-agent/package.json +1 -1
- package/packages/pi-coding-agent/src/core/agent-session.ts +51 -3
- package/packages/pi-coding-agent/src/core/sdk.ts +1 -1
- package/packages/pi-coding-agent/src/core/skill-tool.test.ts +18 -0
- package/packages/pi-coding-agent/src/core/skills.ts +1 -0
- package/packages/pi-coding-agent/src/core/system-prompt.ts +3 -0
- package/packages/pi-coding-agent/src/tests/path-display.test.ts +1 -0
- package/pkg/package.json +1 -1
- package/src/resources/extensions/slash-commands/plan.ts +14 -8
- package/src/resources/extensions/subagent/agents.ts +20 -6
- package/src/resources/extensions/subagent/index.ts +138 -18
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
* Modes use this class and add their own I/O layer on top.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { readFileSync } from "node:fs";
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
17
|
import { basename, dirname, join } from "node:path";
|
|
18
18
|
import { Text } from "@gsd/pi-tui";
|
|
19
19
|
import type {
|
|
@@ -1001,7 +1001,52 @@ export class AgentSession {
|
|
|
1001
1001
|
return this.resourceLoader.getSkills().skills.find((skill) => skill.name === skillName);
|
|
1002
1002
|
}
|
|
1003
1003
|
|
|
1004
|
+
private _findKnownSubagentByName(agentName: string): { name: string; source: string } | undefined {
|
|
1005
|
+
const normalized = agentName.trim().toLowerCase();
|
|
1006
|
+
if (!normalized) return undefined;
|
|
1007
|
+
|
|
1008
|
+
const builtInAgentNames = new Set(["scout", "worker", "reviewer", "planner", "teams-builder", "teams-reviewer"]);
|
|
1009
|
+
if (builtInAgentNames.has(normalized)) {
|
|
1010
|
+
return { name: normalized, source: "bundled" };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
const userAgentsDir = join(process.env.LSD_CODING_AGENT_DIR || process.env.GSD_CODING_AGENT_DIR || join(process.env.HOME || "", ".lsd", "agent"), "agents");
|
|
1014
|
+
const projectCandidates = [".lsd", ".gsd", ".pi"].map((dir) => join(this._cwd, dir, "agents"));
|
|
1015
|
+
for (const [dirPath, source] of [[userAgentsDir, "user"], ...projectCandidates.map((p) => [p, "project"] as const)]) {
|
|
1016
|
+
if (!existsSync(dirPath)) continue;
|
|
1017
|
+
let entries: string[] = [];
|
|
1018
|
+
try {
|
|
1019
|
+
entries = readdirSync(dirPath);
|
|
1020
|
+
} catch {
|
|
1021
|
+
continue;
|
|
1022
|
+
}
|
|
1023
|
+
for (const entry of entries) {
|
|
1024
|
+
if (!entry.endsWith(".md")) continue;
|
|
1025
|
+
const filePath = join(dirPath, entry);
|
|
1026
|
+
try {
|
|
1027
|
+
if (!statSync(filePath).isFile()) continue;
|
|
1028
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
1029
|
+
const match = raw.match(/^---\s*[\r\n]+([\s\S]*?)[\r\n]+---/);
|
|
1030
|
+
const frontmatter = match?.[1] ?? "";
|
|
1031
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
1032
|
+
const name = nameMatch?.[1]?.trim().replace(/^['"]|['"]$/g, "");
|
|
1033
|
+
if (name?.toLowerCase() === normalized) {
|
|
1034
|
+
return { name, source };
|
|
1035
|
+
}
|
|
1036
|
+
} catch {
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
return undefined;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1004
1045
|
private _formatMissingSkillMessage(skillName: string): string {
|
|
1046
|
+
const knownSubagent = this._findKnownSubagentByName(skillName);
|
|
1047
|
+
if (knownSubagent) {
|
|
1048
|
+
return `\"${knownSubagent.name}\" is an available subagent (${knownSubagent.source}), not a skill. Use the subagent tool directly with { agent: \"${knownSubagent.name}\", task: \"...\" }.`;
|
|
1049
|
+
}
|
|
1005
1050
|
const availableSkills = this.resourceLoader.getSkills().skills.map((skill) => skill.name).join(", ") || "(none)";
|
|
1006
1051
|
return `Skill "${skillName}" not found. Available skills: ${availableSkills}`;
|
|
1007
1052
|
}
|
|
@@ -1059,6 +1104,7 @@ export class AgentSession {
|
|
|
1059
1104
|
"Scout-first reconnaissance policy: when you need architecture context across multiple files or folders, do not map the subsystem by reading file-after-file yourself. Launch the scout subagent first, then continue with lsp and targeted reads.",
|
|
1060
1105
|
"If your next step would be broad exploration rather than a targeted lookup, prefer scout before doing more read/find/grep sweeps yourself.",
|
|
1061
1106
|
"When you choose scout, call subagent directly with valid parameters: { agent, task } for one scout, or { tasks: [{ agent, task }, ...] } for parallel scouts.",
|
|
1107
|
+
"If the user explicitly names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly rather than the Skill tool or ad-hoc discovery.",
|
|
1062
1108
|
"Scout is reconnaissance-only. Do not use it as the reviewer, auditor, or final issue-ranker; use it to map files, ownership, and likely hotspots for later evaluation.",
|
|
1063
1109
|
"If the work spans multiple loosely-coupled subsystems, prefer parallel scout subagents so each scout maps one area and the parent model reads the summaries instead of the raw files.",
|
|
1064
1110
|
"For broad review or audit requests, use scout only as a prep step to map architecture and hotspots; the parent model or a reviewer should make the final judgments.",
|
|
@@ -1354,7 +1400,7 @@ export class AgentSession {
|
|
|
1354
1400
|
name: "Skill",
|
|
1355
1401
|
label: "Skill",
|
|
1356
1402
|
description:
|
|
1357
|
-
"Execute a skill within the main conversation. Use this tool
|
|
1403
|
+
"Execute a listed skill within the main conversation. Use this tool only for actual available skills or explicit /skill:name-style requests — not for launching subagents like scout, worker, reviewer, or planner. Returns the expanded skill block and appends args after it.",
|
|
1358
1404
|
parameters: skillSchema,
|
|
1359
1405
|
execute: async (_toolCallId, params: unknown) => {
|
|
1360
1406
|
const input = params as { skill: string; args?: string };
|
|
@@ -2371,7 +2417,9 @@ export class AgentSession {
|
|
|
2371
2417
|
|
|
2372
2418
|
const nextActiveToolNames = options?.activeToolNames
|
|
2373
2419
|
? [...options.activeToolNames]
|
|
2374
|
-
:
|
|
2420
|
+
: this.settingsManager.getToolProfile() === "full"
|
|
2421
|
+
? Array.from(toolRegistry.keys())
|
|
2422
|
+
: [...previousActiveToolNames];
|
|
2375
2423
|
|
|
2376
2424
|
if (options?.includeAllExtensionTools) {
|
|
2377
2425
|
for (const tool of wrappedExtensionTools) {
|
|
@@ -279,7 +279,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|
|
279
279
|
? ["hashline_read", "bash", "hashline_edit", "write", "lsp", "bg_shell", "tool_search", "tool_enable", "Skill", "subagent", "await_subagent", "ask_user_questions"]
|
|
280
280
|
: ["read", "bash", "edit", "write", "lsp", "bg_shell", "tool_search", "tool_enable", "Skill", "subagent", "await_subagent", "ask_user_questions"];
|
|
281
281
|
const defaultActiveToolNames: string[] = toolProfile === "full"
|
|
282
|
-
?
|
|
282
|
+
? []
|
|
283
283
|
: balancedToolNames;
|
|
284
284
|
const initialActiveToolNames: string[] = options.tools
|
|
285
285
|
? options.tools.map((t) => t.name).filter((n): n is string => typeof n === "string" && n in allTools)
|
|
@@ -85,6 +85,24 @@ describe("Skill tool", () => {
|
|
|
85
85
|
);
|
|
86
86
|
});
|
|
87
87
|
|
|
88
|
+
it("describes Skill as distinct from subagents", async () => {
|
|
89
|
+
const session = await createSession();
|
|
90
|
+
const tool = session.state.tools.find((entry) => entry.name === "Skill");
|
|
91
|
+
assert.ok(tool, "Skill tool should be registered");
|
|
92
|
+
assert.match(tool.description ?? "", /not for launching subagents like scout, worker, reviewer, or planner/i);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it("returns a helpful redirect when a known subagent name is passed to Skill", async () => {
|
|
96
|
+
const session = await createSession();
|
|
97
|
+
const tool = session.state.tools.find((entry) => entry.name === "Skill");
|
|
98
|
+
assert.ok(tool, "Skill tool should be registered");
|
|
99
|
+
|
|
100
|
+
const result = await tool.execute("call-subagent", { skill: "scout" });
|
|
101
|
+
const message = result.content[0]?.type === "text" ? result.content[0].text : "";
|
|
102
|
+
assert.match(message, /"scout" is an available subagent/i);
|
|
103
|
+
assert.match(message, /Use the subagent tool directly with \{ agent: "scout", task: "\.\.\." \}\./i);
|
|
104
|
+
});
|
|
105
|
+
|
|
88
106
|
it("returns a helpful error for unknown skills", async () => {
|
|
89
107
|
writeSkill(testDir, "swift-testing", "Use for Swift Testing assertions and verification patterns.");
|
|
90
108
|
const session = await createSession();
|
|
@@ -324,6 +324,7 @@ export function formatSkillsForPrompt(skills: Skill[]): string {
|
|
|
324
324
|
const lines = [
|
|
325
325
|
"\n\nThe following skills provide specialized instructions for specific tasks.",
|
|
326
326
|
"Use the Skill tool with the exact skill name from <available_skills> when the task matches its description.",
|
|
327
|
+
"Use the Skill tool only for listed skills — not for launching subagents like scout, worker, reviewer, or planner.",
|
|
327
328
|
"If the Skill tool reports an unknown skill, do not guess: use an exact name from <available_skills> or tell the user the skill is unavailable.",
|
|
328
329
|
"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.",
|
|
329
330
|
"",
|
|
@@ -172,6 +172,9 @@ export function buildSystemPrompt(options: BuildSystemPromptOptions = {}): strin
|
|
|
172
172
|
addGuideline(
|
|
173
173
|
"Call the subagent tool directly. For one scout use { agent, task }. For several scouts use parallel mode with { tasks: [{ agent, task }, ...] }.",
|
|
174
174
|
);
|
|
175
|
+
addGuideline(
|
|
176
|
+
"When the user names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly rather than the Skill tool or ad-hoc search.",
|
|
177
|
+
);
|
|
175
178
|
addGuideline(
|
|
176
179
|
"Scout is for mapping and reconnaissance only — not for final review, audit, or ranked issue lists. Use it to identify relevant files, subsystems, and likely hotspots for later evaluation.",
|
|
177
180
|
);
|
|
@@ -74,6 +74,7 @@ test("buildSystemPrompt: encourages scout-first reconnaissance when subagent is
|
|
|
74
74
|
assert.match(prompt, /multiple scout subagents in parallel/i);
|
|
75
75
|
assert.match(prompt, /broad review or audit requests, use scout only as a prep step/i);
|
|
76
76
|
assert.match(prompt, /Skip scout only when the task is clearly narrow/i);
|
|
77
|
+
assert.match(prompt, /When the user names a subagent such as scout, worker, reviewer, or planner, invoke the subagent tool directly/i);
|
|
77
78
|
});
|
|
78
79
|
|
|
79
80
|
// ─── Regression: no backslash paths in LLM-visible text ────────────────────
|
package/pkg/package.json
CHANGED
|
@@ -275,15 +275,21 @@ async function enablePlanModeWithModelSwitch(
|
|
|
275
275
|
next: Partial<Pick<PlanModeState, "task" | "latestPlanPath" | "approvalStatus" | "previousMode" | "preplanModel" | "targetPermissionMode">> = {},
|
|
276
276
|
): Promise<void> {
|
|
277
277
|
enablePlanMode(pi, currentModel, next);
|
|
278
|
-
//
|
|
278
|
+
// Keep fallback behavior in before_agent_start for restored sessions or when
|
|
279
|
+
// immediate switching cannot be completed at entry-time.
|
|
279
280
|
reasoningModelSwitchDone = false;
|
|
280
281
|
if (!readAutoSwitchPlanModelSetting()) return;
|
|
281
|
-
|
|
282
|
+
|
|
283
|
+
const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
|
|
284
|
+
if (!reasoningModel) {
|
|
282
285
|
ctx.ui?.notify?.(
|
|
283
286
|
"OpusPlan: set a Plan reasoning model in /settings to auto-switch on entry",
|
|
284
287
|
"info",
|
|
285
288
|
);
|
|
289
|
+
return;
|
|
286
290
|
}
|
|
291
|
+
|
|
292
|
+
reasoningModelSwitchDone = await setModelIfNeeded(pi, ctx, reasoningModel);
|
|
287
293
|
}
|
|
288
294
|
|
|
289
295
|
function enablePlanMode(
|
|
@@ -329,13 +335,14 @@ function leavePlanMode(
|
|
|
329
335
|
return nextPermissionMode;
|
|
330
336
|
}
|
|
331
337
|
|
|
332
|
-
async function setModelIfNeeded(pi: ExtensionAPI, ctx: any, modelRef: ModelRef | undefined): Promise<
|
|
333
|
-
if (!modelRef) return;
|
|
338
|
+
async function setModelIfNeeded(pi: ExtensionAPI, ctx: any, modelRef: ModelRef | undefined): Promise<boolean> {
|
|
339
|
+
if (!modelRef) return false;
|
|
334
340
|
const currentModel = parseQualifiedModelRef(ctx?.model ? `${ctx.model.provider}/${ctx.model.id}` : undefined);
|
|
335
|
-
if (sameModel(currentModel, modelRef)) return;
|
|
341
|
+
if (sameModel(currentModel, modelRef)) return true;
|
|
336
342
|
const model = resolveModelFromContext(ctx, modelRef);
|
|
337
|
-
if (!model) return;
|
|
343
|
+
if (!model) return false;
|
|
338
344
|
await pi.setModel(model, { persist: false });
|
|
345
|
+
return true;
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
function buildExecutionKickoffMessage(options: { permissionMode: RestorablePermissionMode; executeWithSubagent?: boolean }): string {
|
|
@@ -640,8 +647,7 @@ export default function planCommand(pi: ExtensionAPI) {
|
|
|
640
647
|
if (!reasoningModelSwitchDone && readAutoSwitchPlanModelSetting()) {
|
|
641
648
|
const reasoningModel = parseQualifiedModelRef(readPlanModeReasoningModel());
|
|
642
649
|
if (reasoningModel) {
|
|
643
|
-
reasoningModelSwitchDone =
|
|
644
|
-
await setModelIfNeeded(pi, ctx, reasoningModel);
|
|
650
|
+
reasoningModelSwitchDone = await setModelIfNeeded(pi, ctx, reasoningModel);
|
|
645
651
|
}
|
|
646
652
|
}
|
|
647
653
|
return { systemPrompt: buildPlanModeSystemPrompt() };
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
import * as fs from "node:fs";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
7
8
|
import { getAgentDir, parseFrontmatter } from "@gsd/pi-coding-agent";
|
|
8
9
|
|
|
9
10
|
const PROJECT_AGENT_DIR_CANDIDATES = [".lsd", ".gsd", ".pi"] as const;
|
|
@@ -16,7 +17,7 @@ export interface AgentConfig {
|
|
|
16
17
|
tools?: string[];
|
|
17
18
|
model?: string;
|
|
18
19
|
systemPrompt: string;
|
|
19
|
-
source: "user" | "project";
|
|
20
|
+
source: "bundled" | "user" | "project";
|
|
20
21
|
filePath: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
@@ -35,7 +36,7 @@ function normalizeAgentModel(model: string | undefined): string | undefined {
|
|
|
35
36
|
return parts.length === 2 && parts.every(Boolean) ? trimmed : undefined;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function loadAgentsFromDir(dir: string, source: "user" | "project"): AgentConfig[] {
|
|
39
|
+
function loadAgentsFromDir(dir: string, source: "bundled" | "user" | "project"): AgentConfig[] {
|
|
39
40
|
const agents: AgentConfig[] = [];
|
|
40
41
|
|
|
41
42
|
if (!fs.existsSync(dir)) {
|
|
@@ -110,22 +111,35 @@ function findNearestProjectAgentsDir(cwd: string): string | null {
|
|
|
110
111
|
}
|
|
111
112
|
}
|
|
112
113
|
|
|
114
|
+
function getBundledAgentsDir(): string {
|
|
115
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
116
|
+
return path.resolve(here, "../../agents");
|
|
117
|
+
}
|
|
118
|
+
|
|
113
119
|
export function discoverAgents(cwd: string, scope: AgentScope): AgentDiscoveryResult {
|
|
114
120
|
const userDir = path.join(getAgentDir(), "agents");
|
|
121
|
+
const bundledDir = getBundledAgentsDir();
|
|
115
122
|
const projectAgentsDir = findNearestProjectAgentsDir(cwd);
|
|
116
123
|
|
|
124
|
+
const bundledAgents = scope === "project" ? [] : loadAgentsFromDir(bundledDir, "bundled");
|
|
117
125
|
const userAgents = scope === "project" ? [] : loadAgentsFromDir(userDir, "user");
|
|
118
126
|
const projectAgents = scope === "user" || !projectAgentsDir ? [] : loadAgentsFromDir(projectAgentsDir, "project");
|
|
119
127
|
|
|
120
128
|
const agentMap = new Map<string, AgentConfig>();
|
|
121
129
|
|
|
130
|
+
const addAgents = (items: AgentConfig[]) => {
|
|
131
|
+
for (const agent of items) agentMap.set(agent.name, agent);
|
|
132
|
+
};
|
|
133
|
+
|
|
122
134
|
if (scope === "both") {
|
|
123
|
-
|
|
124
|
-
|
|
135
|
+
addAgents(bundledAgents);
|
|
136
|
+
addAgents(userAgents);
|
|
137
|
+
addAgents(projectAgents);
|
|
125
138
|
} else if (scope === "user") {
|
|
126
|
-
|
|
139
|
+
addAgents(bundledAgents);
|
|
140
|
+
addAgents(userAgents);
|
|
127
141
|
} else {
|
|
128
|
-
|
|
142
|
+
addAgents(projectAgents);
|
|
129
143
|
}
|
|
130
144
|
|
|
131
145
|
return { agents: Array.from(agentMap.values()), projectAgentsDir };
|
|
@@ -25,9 +25,9 @@ import {
|
|
|
25
25
|
getAgentDir,
|
|
26
26
|
getMarkdownTheme,
|
|
27
27
|
} from "@gsd/pi-coding-agent";
|
|
28
|
-
import { Container, Markdown, Spacer, Text } from "@gsd/pi-tui";
|
|
28
|
+
import { Container, Key, Markdown, Spacer, Text } from "@gsd/pi-tui";
|
|
29
29
|
import { Type } from "@sinclair/typebox";
|
|
30
|
-
import { formatTokenCount } from "../shared/mod.js";
|
|
30
|
+
import { formatTokenCount, shortcutDesc } from "../shared/mod.js";
|
|
31
31
|
import { type AgentConfig, type AgentScope, discoverAgents } from "./agents.js";
|
|
32
32
|
import { buildSubagentProcessArgs, getBundledExtensionPathsFromEnv } from "./launch-helpers.js";
|
|
33
33
|
import {
|
|
@@ -314,7 +314,7 @@ interface UsageStats {
|
|
|
314
314
|
|
|
315
315
|
interface SingleResult {
|
|
316
316
|
agent: string;
|
|
317
|
-
agentSource: "user" | "project" | "unknown";
|
|
317
|
+
agentSource: "bundled" | "user" | "project" | "unknown";
|
|
318
318
|
task: string;
|
|
319
319
|
exitCode: number;
|
|
320
320
|
messages: Message[];
|
|
@@ -327,6 +327,20 @@ interface SingleResult {
|
|
|
327
327
|
backgroundJobId?: string;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
+
interface ForegroundSingleRunControl {
|
|
331
|
+
agentName: string;
|
|
332
|
+
task: string;
|
|
333
|
+
cwd: string;
|
|
334
|
+
abortController: AbortController;
|
|
335
|
+
resultPromise: Promise<{ summary: string; stderr: string; exitCode: number; model?: string }>;
|
|
336
|
+
adoptToBackground: (jobId: string) => boolean;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
interface ForegroundSingleRunHooks {
|
|
340
|
+
onStart?: (control: ForegroundSingleRunControl) => void;
|
|
341
|
+
onFinish?: () => void;
|
|
342
|
+
}
|
|
343
|
+
|
|
330
344
|
interface SubagentDetails {
|
|
331
345
|
mode: "single" | "parallel" | "chain";
|
|
332
346
|
agentScope: AgentScope;
|
|
@@ -501,7 +515,8 @@ async function runSingleAgent(
|
|
|
501
515
|
parentModel: { provider: string; id: string } | undefined,
|
|
502
516
|
signal: AbortSignal | undefined,
|
|
503
517
|
onUpdate: OnUpdateCallback | undefined,
|
|
504
|
-
makeDetails: (results: SingleResult[]) => SubagentDetails
|
|
518
|
+
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
519
|
+
foregroundHooks?: ForegroundSingleRunHooks,
|
|
505
520
|
): Promise<SingleResult> {
|
|
506
521
|
const agent = agents.find((a) => a.name === agentName);
|
|
507
522
|
|
|
@@ -551,6 +566,27 @@ async function runSingleAgent(
|
|
|
551
566
|
}
|
|
552
567
|
};
|
|
553
568
|
|
|
569
|
+
let wasAborted = false;
|
|
570
|
+
let deferTempPromptCleanup = false;
|
|
571
|
+
let tempPromptCleanupDone = false;
|
|
572
|
+
|
|
573
|
+
const cleanupTempPromptFiles = () => {
|
|
574
|
+
if (tempPromptCleanupDone) return;
|
|
575
|
+
tempPromptCleanupDone = true;
|
|
576
|
+
if (tmpPromptPath)
|
|
577
|
+
try {
|
|
578
|
+
fs.unlinkSync(tmpPromptPath);
|
|
579
|
+
} catch {
|
|
580
|
+
/* ignore */
|
|
581
|
+
}
|
|
582
|
+
if (tmpPromptDir)
|
|
583
|
+
try {
|
|
584
|
+
fs.rmdirSync(tmpPromptDir);
|
|
585
|
+
} catch {
|
|
586
|
+
/* ignore */
|
|
587
|
+
}
|
|
588
|
+
};
|
|
589
|
+
|
|
554
590
|
try {
|
|
555
591
|
if (agent.systemPrompt.trim()) {
|
|
556
592
|
const tmp = writePromptToTempFile(agent.name, agent.systemPrompt);
|
|
@@ -558,7 +594,6 @@ async function runSingleAgent(
|
|
|
558
594
|
tmpPromptPath = tmp.filePath;
|
|
559
595
|
}
|
|
560
596
|
const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel);
|
|
561
|
-
let wasAborted = false;
|
|
562
597
|
|
|
563
598
|
const exitCode = await new Promise<number>((resolve) => {
|
|
564
599
|
const bundledPaths = getBundledExtensionPathsFromEnv();
|
|
@@ -581,6 +616,7 @@ async function runSingleAgent(
|
|
|
581
616
|
let buffer = "";
|
|
582
617
|
let completionSeen = false;
|
|
583
618
|
let resolved = false;
|
|
619
|
+
let foregroundReleased = false;
|
|
584
620
|
const procAbortController = new AbortController();
|
|
585
621
|
let resolveBackgroundResult: ((value: { summary: string; stderr: string; exitCode: number; model?: string }) => void) | undefined;
|
|
586
622
|
let rejectBackgroundResult: ((reason?: unknown) => void) | undefined;
|
|
@@ -595,7 +631,27 @@ async function runSingleAgent(
|
|
|
595
631
|
resolve(code);
|
|
596
632
|
};
|
|
597
633
|
|
|
634
|
+
const adoptToBackground = (jobId: string): boolean => {
|
|
635
|
+
if (resolved || foregroundReleased) return false;
|
|
636
|
+
foregroundReleased = true;
|
|
637
|
+
deferTempPromptCleanup = true;
|
|
638
|
+
currentResult.backgroundJobId = jobId;
|
|
639
|
+
finishForeground(0);
|
|
640
|
+
return true;
|
|
641
|
+
};
|
|
598
642
|
|
|
643
|
+
backgroundResultPromise.finally(() => {
|
|
644
|
+
if (deferTempPromptCleanup) cleanupTempPromptFiles();
|
|
645
|
+
});
|
|
646
|
+
|
|
647
|
+
foregroundHooks?.onStart?.({
|
|
648
|
+
agentName,
|
|
649
|
+
task,
|
|
650
|
+
cwd: cwd ?? defaultCwd,
|
|
651
|
+
abortController: procAbortController,
|
|
652
|
+
resultPromise: backgroundResultPromise,
|
|
653
|
+
adoptToBackground,
|
|
654
|
+
});
|
|
599
655
|
|
|
600
656
|
proc.stdout.on("data", (data) => {
|
|
601
657
|
buffer += data.toString();
|
|
@@ -631,12 +687,14 @@ async function runSingleAgent(
|
|
|
631
687
|
exitCode: finalExitCode,
|
|
632
688
|
model: currentResult.model,
|
|
633
689
|
});
|
|
690
|
+
foregroundHooks?.onFinish?.();
|
|
634
691
|
finishForeground(finalExitCode);
|
|
635
692
|
});
|
|
636
693
|
|
|
637
694
|
proc.on("error", (error) => {
|
|
638
695
|
liveSubagentProcesses.delete(proc);
|
|
639
696
|
rejectBackgroundResult?.(error);
|
|
697
|
+
foregroundHooks?.onFinish?.();
|
|
640
698
|
finishForeground(1);
|
|
641
699
|
});
|
|
642
700
|
|
|
@@ -670,18 +728,7 @@ async function runSingleAgent(
|
|
|
670
728
|
if (wasAborted) throw new Error("Subagent was aborted");
|
|
671
729
|
return currentResult;
|
|
672
730
|
} finally {
|
|
673
|
-
if (
|
|
674
|
-
try {
|
|
675
|
-
fs.unlinkSync(tmpPromptPath);
|
|
676
|
-
} catch {
|
|
677
|
-
/* ignore */
|
|
678
|
-
}
|
|
679
|
-
if (tmpPromptDir)
|
|
680
|
-
try {
|
|
681
|
-
fs.rmdirSync(tmpPromptDir);
|
|
682
|
-
} catch {
|
|
683
|
-
/* ignore */
|
|
684
|
-
}
|
|
731
|
+
if (!deferTempPromptCleanup) cleanupTempPromptFiles();
|
|
685
732
|
}
|
|
686
733
|
}
|
|
687
734
|
|
|
@@ -754,6 +801,10 @@ const SubagentParams = Type.Object({
|
|
|
754
801
|
|
|
755
802
|
export default function(pi: ExtensionAPI) {
|
|
756
803
|
let bgManager: BackgroundJobManager | null = null;
|
|
804
|
+
const foregroundSubagentStatusKey = "foreground-subagent";
|
|
805
|
+
const foregroundSubagentHint = "Ctrl+B: move foreground subagent to background";
|
|
806
|
+
type ActiveForegroundSubagent = ForegroundSingleRunControl & { claimed: boolean };
|
|
807
|
+
let activeForegroundSubagent: ActiveForegroundSubagent | null = null;
|
|
757
808
|
|
|
758
809
|
function getBgManager(): BackgroundJobManager {
|
|
759
810
|
if (!bgManager) throw new Error("BackgroundJobManager not initialized.");
|
|
@@ -792,6 +843,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
792
843
|
|
|
793
844
|
|
|
794
845
|
pi.on("session_before_switch", async () => {
|
|
846
|
+
activeForegroundSubagent = null;
|
|
795
847
|
if (bgManager) {
|
|
796
848
|
for (const job of bgManager.getRunningJobs()) {
|
|
797
849
|
bgManager.cancel(job.id);
|
|
@@ -800,6 +852,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
800
852
|
});
|
|
801
853
|
|
|
802
854
|
pi.on("session_shutdown", async () => {
|
|
855
|
+
activeForegroundSubagent = null;
|
|
803
856
|
await stopLiveSubagents();
|
|
804
857
|
if (bgManager) {
|
|
805
858
|
bgManager.shutdown();
|
|
@@ -919,6 +972,49 @@ export default function(pi: ExtensionAPI) {
|
|
|
919
972
|
},
|
|
920
973
|
});
|
|
921
974
|
|
|
975
|
+
pi.registerShortcut(Key.ctrl("b"), {
|
|
976
|
+
description: shortcutDesc("Move foreground subagent to background", "/subagents list"),
|
|
977
|
+
handler: async (ctx) => {
|
|
978
|
+
const running = activeForegroundSubagent;
|
|
979
|
+
if (!running || running.claimed) return;
|
|
980
|
+
const manager = bgManager;
|
|
981
|
+
if (!manager) {
|
|
982
|
+
ctx.ui.notify("Background subagent manager is not available.", "error");
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
running.claimed = true;
|
|
987
|
+
let jobId: string;
|
|
988
|
+
try {
|
|
989
|
+
jobId = manager.adoptRunning(
|
|
990
|
+
running.agentName,
|
|
991
|
+
running.task,
|
|
992
|
+
running.cwd,
|
|
993
|
+
running.abortController,
|
|
994
|
+
running.resultPromise,
|
|
995
|
+
);
|
|
996
|
+
} catch (error) {
|
|
997
|
+
running.claimed = false;
|
|
998
|
+
ctx.ui.notify(error instanceof Error ? error.message : String(error), "error");
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
const released = running.adoptToBackground(jobId);
|
|
1003
|
+
if (!released) {
|
|
1004
|
+
running.claimed = false;
|
|
1005
|
+
manager.cancel(jobId);
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
activeForegroundSubagent = null;
|
|
1010
|
+
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
1011
|
+
ctx.ui.notify(
|
|
1012
|
+
`Moved ${running.agentName} to background as ${jobId}. Use /subagents wait ${jobId}, /subagents output ${jobId}, or /subagents cancel ${jobId}.`,
|
|
1013
|
+
"info",
|
|
1014
|
+
);
|
|
1015
|
+
},
|
|
1016
|
+
});
|
|
1017
|
+
|
|
922
1018
|
pi.registerTool({
|
|
923
1019
|
name: "await_subagent",
|
|
924
1020
|
label: "Await Background Subagent",
|
|
@@ -949,13 +1045,15 @@ export default function(pi: ExtensionAPI) {
|
|
|
949
1045
|
"Model selection can be overridden per call, otherwise it is inferred from the agent config, delegated-task preferences, or the current session model.",
|
|
950
1046
|
"Modes: single ({ agent, task }), parallel ({ tasks: [{agent, task},...] }), chain ({ chain: [{agent, task},...] } with {previous} placeholder).",
|
|
951
1047
|
"Agents are defined as .md files in the configured user agent directory (for LSD this is typically ~/.lsd/agent/agents/) or project-local .lsd/agents/, with legacy support for .gsd/agents/ and .pi/agents/.",
|
|
1048
|
+
"If the user asks for a named subagent such as scout, worker, reviewer, or planner, invoke this tool directly rather than the Skill tool.",
|
|
952
1049
|
"Use the /subagent command to list available agents and their descriptions.",
|
|
953
1050
|
"Set background: true (single mode only) to run detached — returns immediately with a sa_xxxx job ID. Completion is announced back into the session. Use await_subagent or /subagents to manage background jobs.",
|
|
954
1051
|
].join(" "),
|
|
955
1052
|
promptGuidelines: [
|
|
956
1053
|
"Use subagent to delegate self-contained tasks that benefit from an isolated context window.",
|
|
957
|
-
"
|
|
1054
|
+
"The subagent tool is available directly as a tool call — invoke it programmatically like any other tool, not via a slash command. Do NOT type '/scout' or '/subagent' in the chat; call this tool with the correct parameters instead.",
|
|
958
1055
|
"Valid call shapes: single mode uses { agent, task }, parallel mode uses { tasks: [{ agent, task }, ...] }, and chain mode uses { chain: [{ agent, task }, ...] }.",
|
|
1056
|
+
"If the user names a subagent such as scout, worker, reviewer, or planner, use this subagent tool directly rather than the Skill tool or ad-hoc search.",
|
|
959
1057
|
"Recon planning rule: use no scout for narrow known-file work, one scout for one broad unfamiliar subsystem, and parallel scouts only when the work spans multiple loosely-coupled subsystems.",
|
|
960
1058
|
"Use scout only for broad or unfamiliar codebase reconnaissance before you read many files yourself; save direct reads for targeted lookups once the relevant files are known.",
|
|
961
1059
|
"Do not use scout as the reviewer, auditor, or final judge. Scout should map architecture, files, ownership, and likely hotspots for another agent or the parent model to evaluate.",
|
|
@@ -1301,8 +1399,27 @@ export default function(pi: ExtensionAPI) {
|
|
|
1301
1399
|
signal,
|
|
1302
1400
|
onUpdate,
|
|
1303
1401
|
makeDetails("single"),
|
|
1402
|
+
!isolation
|
|
1403
|
+
? {
|
|
1404
|
+
onStart: (control) => {
|
|
1405
|
+
activeForegroundSubagent = { ...control, claimed: false };
|
|
1406
|
+
ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
|
|
1407
|
+
},
|
|
1408
|
+
onFinish: () => {
|
|
1409
|
+
activeForegroundSubagent = null;
|
|
1410
|
+
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
1411
|
+
},
|
|
1412
|
+
}
|
|
1413
|
+
: undefined,
|
|
1304
1414
|
);
|
|
1305
1415
|
|
|
1416
|
+
if (result.backgroundJobId) {
|
|
1417
|
+
return {
|
|
1418
|
+
content: [{ type: "text", text: `Moved ${result.agent} to background as **${result.backgroundJobId}**. Use \`await_subagent\`, \`/subagents wait ${result.backgroundJobId}\`, or \`/subagents output ${result.backgroundJobId}\`.` }],
|
|
1419
|
+
details: makeDetails("single")([result]),
|
|
1420
|
+
};
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1306
1423
|
// Capture and merge delta if isolated
|
|
1307
1424
|
if (isolation) {
|
|
1308
1425
|
const patches = await isolation.captureDelta();
|
|
@@ -1464,6 +1581,9 @@ export default function(pi: ExtensionAPI) {
|
|
|
1464
1581
|
text += `\n${renderDisplayItems(displayItems, COLLAPSED_ITEM_COUNT)}`;
|
|
1465
1582
|
if (displayItems.length > COLLAPSED_ITEM_COUNT) text += `\n${theme.fg("muted", "(Ctrl+O to expand)")}`;
|
|
1466
1583
|
}
|
|
1584
|
+
if (!isError && !r.backgroundJobId && !finalOutput) {
|
|
1585
|
+
text += `\n${theme.fg("muted", "Hint: Ctrl+B to move running foreground subagent to background")}`;
|
|
1586
|
+
}
|
|
1467
1587
|
const usageStr = formatUsageStats(r.usage, r.model);
|
|
1468
1588
|
if (usageStr) text += `\n${theme.fg("dim", usageStr)}`;
|
|
1469
1589
|
return new Text(text, 0, 0);
|