lsd-pi 1.2.4 → 1.3.2
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 +22 -16
- package/dist/app-paths.d.ts +4 -0
- package/dist/app-paths.js +4 -0
- package/dist/bedrock-auth.d.ts +4 -0
- package/dist/bedrock-auth.js +4 -0
- package/dist/bundled-extension-paths.d.ts +4 -0
- package/dist/bundled-extension-paths.js +4 -0
- package/dist/cli-theme.d.ts +2 -2
- package/dist/cli-theme.js +13 -14
- package/dist/cli.js +43 -3
- package/dist/codex-rotate-settings.d.ts +4 -0
- package/dist/codex-rotate-settings.js +4 -0
- package/dist/help-text.d.ts +4 -0
- package/dist/help-text.js +4 -0
- package/dist/lsd-brand.d.ts +4 -0
- package/dist/lsd-brand.js +4 -0
- package/dist/onboarding-llm.d.ts +5 -0
- package/dist/onboarding-llm.js +5 -0
- package/dist/project-sessions.d.ts +4 -0
- package/dist/project-sessions.js +4 -0
- package/dist/resources/agents/generic.md +1 -0
- package/dist/resources/agents/scout.md +8 -1
- package/dist/resources/agents/worker.md +1 -0
- package/dist/resources/extensions/ask-user-questions.js +70 -0
- package/dist/resources/extensions/bg-shell/bg-shell-tool.js +6 -16
- package/dist/resources/extensions/mac-tools/index.js +19 -34
- package/dist/resources/extensions/memory/index.js +20 -2
- package/dist/resources/extensions/shared/interview-ui.js +103 -20
- package/dist/resources/extensions/slash-commands/plan.js +18 -17
- package/dist/resources/extensions/slash-commands/tools.js +40 -4
- package/dist/resources/extensions/subagent/agent-switcher-component.js +208 -0
- package/dist/resources/extensions/subagent/agent-switcher-model.js +107 -0
- package/dist/resources/extensions/subagent/background-job-manager.js +11 -6
- package/dist/resources/extensions/subagent/background-runner.js +4 -0
- package/dist/resources/extensions/subagent/index.js +714 -21
- package/dist/resources/extensions/subagent/launch-helpers.js +19 -5
- package/dist/shared-paths.d.ts +4 -0
- package/dist/shared-paths.js +4 -0
- package/dist/shared-preferences.d.ts +4 -0
- package/dist/shared-preferences.js +4 -0
- package/dist/startup-model-validation.d.ts +1 -1
- package/dist/startup-timings.d.ts +4 -0
- package/dist/startup-timings.js +4 -0
- package/dist/update-check.d.ts +4 -0
- package/dist/update-check.js +4 -0
- package/dist/update-cmd.d.ts +4 -0
- package/dist/update-cmd.js +4 -0
- package/dist/welcome-screen.js +4 -4
- package/dist/wizard.d.ts +4 -0
- package/dist/wizard.js +4 -0
- package/package.json +1 -1
- package/packages/pi-agent-core/dist/agent.d.ts +9 -0
- package/packages/pi-agent-core/dist/agent.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/agent.js +89 -5
- package/packages/pi-agent-core/dist/agent.js.map +1 -1
- package/packages/pi-agent-core/dist/types.d.ts +13 -2
- package/packages/pi-agent-core/dist/types.d.ts.map +1 -1
- package/packages/pi-agent-core/dist/types.js.map +1 -1
- package/packages/pi-agent-core/src/agent.ts +110 -4
- package/packages/pi-agent-core/src/types.ts +12 -3
- package/packages/pi-ai/dist/adaptive/classifier.d.ts +29 -0
- package/packages/pi-ai/dist/adaptive/classifier.d.ts.map +1 -0
- package/packages/pi-ai/dist/adaptive/classifier.js +72 -0
- package/packages/pi-ai/dist/adaptive/classifier.js.map +1 -0
- package/packages/pi-ai/dist/adaptive/classifier.test.d.ts +2 -0
- package/packages/pi-ai/dist/adaptive/classifier.test.d.ts.map +1 -0
- package/packages/pi-ai/dist/adaptive/classifier.test.js +32 -0
- package/packages/pi-ai/dist/adaptive/classifier.test.js.map +1 -0
- package/packages/pi-ai/dist/index.d.ts +1 -0
- package/packages/pi-ai/dist/index.d.ts.map +1 -1
- package/packages/pi-ai/dist/index.js +1 -0
- package/packages/pi-ai/dist/index.js.map +1 -1
- package/packages/pi-ai/dist/providers/amazon-bedrock.js +0 -2
- package/packages/pi-ai/dist/providers/amazon-bedrock.js.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/anthropic-shared.js +0 -2
- package/packages/pi-ai/dist/providers/anthropic-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/azure-openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/google-gemini-cli.js +0 -4
- package/packages/pi-ai/dist/providers/google-gemini-cli.js.map +1 -1
- package/packages/pi-ai/dist/providers/google-vertex.js +0 -5
- package/packages/pi-ai/dist/providers/google-vertex.js.map +1 -1
- package/packages/pi-ai/dist/providers/google.js +0 -5
- package/packages/pi-ai/dist/providers/google.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-codex-responses.js +0 -2
- package/packages/pi-ai/dist/providers/openai-codex-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-completions.js +0 -1
- package/packages/pi-ai/dist/providers/openai-completions.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-responses.js.map +1 -1
- package/packages/pi-ai/dist/providers/openai-shared.d.ts +0 -1
- package/packages/pi-ai/dist/providers/openai-shared.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/openai-shared.js +0 -4
- package/packages/pi-ai/dist/providers/openai-shared.js.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.d.ts.map +1 -1
- package/packages/pi-ai/dist/providers/simple-options.js +0 -1
- package/packages/pi-ai/dist/providers/simple-options.js.map +1 -1
- package/packages/pi-ai/dist/types.d.ts +1 -2
- package/packages/pi-ai/dist/types.d.ts.map +1 -1
- package/packages/pi-ai/dist/types.js.map +1 -1
- package/packages/pi-ai/src/adaptive/classifier.test.ts +38 -0
- package/packages/pi-ai/src/adaptive/classifier.ts +107 -0
- package/packages/pi-ai/src/index.ts +1 -0
- package/packages/pi-ai/src/providers/amazon-bedrock.ts +0 -2
- package/packages/pi-ai/src/providers/anthropic-shared.ts +0 -2
- package/packages/pi-ai/src/providers/azure-openai-responses.ts +1 -1
- package/packages/pi-ai/src/providers/google-gemini-cli.ts +0 -4
- package/packages/pi-ai/src/providers/google-vertex.ts +0 -5
- package/packages/pi-ai/src/providers/google.ts +0 -5
- package/packages/pi-ai/src/providers/openai-codex-responses.ts +1 -3
- package/packages/pi-ai/src/providers/openai-completions.ts +1 -2
- package/packages/pi-ai/src/providers/openai-responses.ts +1 -1
- package/packages/pi-ai/src/providers/openai-shared.ts +0 -3
- package/packages/pi-ai/src/providers/simple-options.ts +0 -1
- package/packages/pi-ai/src/types.ts +1 -2
- package/packages/pi-coding-agent/dist/cli/args.js +2 -2
- package/packages/pi-coding-agent/dist/cli/args.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/agent-session.d.ts +7 -2
- 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 -20
- package/packages/pi-coding-agent/dist/core/agent-session.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/lsp/lsp.md +3 -1
- package/packages/pi-coding-agent/dist/core/sdk.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.js +32 -6
- package/packages/pi-coding-agent/dist/core/sdk.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/sdk.test.js +37 -0
- package/packages/pi-coding-agent/dist/core/sdk.test.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts +8 -0
- package/packages/pi-coding-agent/dist/core/session-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/session-manager.js +4 -0
- package/packages/pi-coding-agent/dist/core/session-manager.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts +12 -7
- package/packages/pi-coding-agent/dist/core/settings-manager.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/settings-manager.js +20 -2
- package/packages/pi-coding-agent/dist/core/settings-manager.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 +4 -1
- package/packages/pi-coding-agent/dist/core/skills.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.js +1 -1
- package/packages/pi-coding-agent/dist/core/slash-commands.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 +6 -2
- package/packages/pi-coding-agent/dist/core/system-prompt.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/grep.js +1 -1
- package/packages/pi-coding-agent/dist/core/tools/grep.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/index.js +2 -0
- package/packages/pi-coding-agent/dist/core/tools/index.js.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/pty.d.ts +10 -1
- package/packages/pi-coding-agent/dist/core/tools/pty.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/core/tools/pty.js +29 -3
- package/packages/pi-coding-agent/dist/core/tools/pty.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/embedded-terminal.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js +12 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/footer.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts +7 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js +23 -4
- package/packages/pi-coding-agent/dist/modes/interactive/components/settings-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js +1 -2
- package/packages/pi-coding-agent/dist/modes/interactive/components/thinking-selector.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js +9 -0
- package/packages/pi-coding-agent/dist/modes/interactive/controllers/chat-controller.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js +53 -2
- package/packages/pi-coding-agent/dist/modes/interactive/interactive-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts +2 -2
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js +10 -6
- package/packages/pi-coding-agent/dist/modes/interactive/theme/theme.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js +1 -1
- package/packages/pi-coding-agent/dist/modes/interactive/theme/themes.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/print-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/print-mode.js +6 -0
- package/packages/pi-coding-agent/dist/modes/print-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js +20 -0
- package/packages/pi-coding-agent/dist/modes/rpc/rpc-mode.js.map +1 -1
- package/packages/pi-coding-agent/dist/tests/path-display.test.js +15 -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/cli/args.ts +2 -2
- package/packages/pi-coding-agent/src/core/agent-session.ts +58 -21
- package/packages/pi-coding-agent/src/core/lsp/lsp.md +3 -1
- package/packages/pi-coding-agent/src/core/sdk.test.ts +45 -0
- package/packages/pi-coding-agent/src/core/sdk.ts +35 -6
- package/packages/pi-coding-agent/src/core/session-manager.ts +12 -0
- package/packages/pi-coding-agent/src/core/settings-manager.ts +32 -9
- package/packages/pi-coding-agent/src/core/skills.ts +4 -1
- package/packages/pi-coding-agent/src/core/slash-commands.ts +1 -1
- package/packages/pi-coding-agent/src/core/system-prompt.ts +8 -2
- package/packages/pi-coding-agent/src/core/tools/grep.ts +1 -1
- package/packages/pi-coding-agent/src/core/tools/index.ts +3 -0
- package/packages/pi-coding-agent/src/core/tools/pty.ts +45 -6
- package/packages/pi-coding-agent/src/modes/interactive/components/embedded-terminal.ts +1 -1
- package/packages/pi-coding-agent/src/modes/interactive/components/footer.ts +10 -2
- package/packages/pi-coding-agent/src/modes/interactive/components/settings-selector.ts +31 -7
- package/packages/pi-coding-agent/src/modes/interactive/components/thinking-selector.ts +1 -2
- package/packages/pi-coding-agent/src/modes/interactive/controllers/chat-controller.ts +9 -0
- package/packages/pi-coding-agent/src/modes/interactive/interactive-mode.ts +65 -3
- package/packages/pi-coding-agent/src/modes/interactive/theme/theme.ts +11 -7
- package/packages/pi-coding-agent/src/modes/interactive/theme/themes.ts +1 -1
- package/packages/pi-coding-agent/src/modes/print-mode.ts +6 -0
- package/packages/pi-coding-agent/src/modes/rpc/rpc-mode.ts +29 -0
- package/packages/pi-coding-agent/src/tests/path-display.test.ts +17 -0
- package/packages/pi-tui/dist/components/loader.d.ts +5 -2
- package/packages/pi-tui/dist/components/loader.d.ts.map +1 -1
- package/packages/pi-tui/dist/components/loader.js +33 -3
- package/packages/pi-tui/dist/components/loader.js.map +1 -1
- package/packages/pi-tui/src/components/loader.ts +31 -3
- package/packages/rpc-client/src/index.ts +1 -1
- package/packages/rpc-client/src/rpc-client.ts +29 -0
- package/packages/rpc-client/src/rpc-types.ts +1 -1
- package/pkg/dist/modes/interactive/theme/theme.d.ts +2 -2
- package/pkg/dist/modes/interactive/theme/theme.d.ts.map +1 -1
- package/pkg/dist/modes/interactive/theme/theme.js +10 -6
- package/pkg/dist/modes/interactive/theme/theme.js.map +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js +1 -1
- package/pkg/dist/modes/interactive/theme/themes.js.map +1 -1
- package/pkg/package.json +1 -1
- package/src/resources/agents/generic.md +1 -0
- package/src/resources/agents/scout.md +8 -1
- package/src/resources/agents/worker.md +1 -0
- package/src/resources/extensions/ask-user-questions.ts +88 -0
- package/src/resources/extensions/bg-shell/bg-shell-tool.ts +6 -16
- package/src/resources/extensions/mac-tools/index.ts +19 -34
- package/src/resources/extensions/memory/index.ts +22 -2
- package/src/resources/extensions/shared/interview-ui.ts +108 -15
- package/src/resources/extensions/shared/tests/ask-user-freetext.test.ts +61 -0
- package/src/resources/extensions/shared/tests/custom-ui-fallbacks.test.ts +46 -0
- package/src/resources/extensions/slash-commands/plan.ts +18 -19
- package/src/resources/extensions/slash-commands/tools.ts +43 -4
- package/src/resources/extensions/subagent/agent-switcher-component.ts +228 -0
- package/src/resources/extensions/subagent/agent-switcher-model.ts +160 -0
- package/src/resources/extensions/subagent/background-job-manager.ts +29 -6
- package/src/resources/extensions/subagent/background-runner.ts +8 -0
- package/src/resources/extensions/subagent/background-types.ts +4 -0
- package/src/resources/extensions/subagent/index.ts +834 -19
- package/src/resources/extensions/subagent/launch-helpers.ts +15 -4
|
@@ -18,7 +18,7 @@ import * as fs from "node:fs";
|
|
|
18
18
|
import * as os from "node:os";
|
|
19
19
|
import * as path from "node:path";
|
|
20
20
|
import type { AgentToolResult } from "@gsd/pi-agent-core";
|
|
21
|
-
import type { Message } from "@gsd/pi-ai";
|
|
21
|
+
import type { ImageContent, Message } from "@gsd/pi-ai";
|
|
22
22
|
import { StringEnum } from "@gsd/pi-ai";
|
|
23
23
|
import {
|
|
24
24
|
type ExtensionAPI,
|
|
@@ -49,6 +49,11 @@ import { loadEffectivePreferences } from "../shared/preferences.js";
|
|
|
49
49
|
import { CmuxClient, shellEscape } from "../cmux/index.js";
|
|
50
50
|
import { BackgroundJobManager, type BackgroundSubagentJob } from "./background-job-manager.js";
|
|
51
51
|
import { runSubagentInBackground } from "./background-runner.js";
|
|
52
|
+
import { showAgentSwitcher } from "./agent-switcher-component.js";
|
|
53
|
+
import {
|
|
54
|
+
buildAgentSwitchTargets,
|
|
55
|
+
type AgentSwitchTarget,
|
|
56
|
+
} from "./agent-switcher-model.js";
|
|
52
57
|
|
|
53
58
|
const MAX_PARALLEL_TASKS = 8;
|
|
54
59
|
const MAX_CONCURRENCY = 4;
|
|
@@ -56,6 +61,207 @@ const COLLAPSED_ITEM_COUNT = 10;
|
|
|
56
61
|
const DEFAULT_AWAIT_SUBAGENT_TIMEOUT_SECONDS = 120;
|
|
57
62
|
const liveSubagentProcesses = new Set<ChildProcess>();
|
|
58
63
|
|
|
64
|
+
type AgentSessionState = "running" | "completed" | "failed";
|
|
65
|
+
|
|
66
|
+
interface AgentSessionLink {
|
|
67
|
+
id: string;
|
|
68
|
+
agentName: string;
|
|
69
|
+
task: string;
|
|
70
|
+
parentSessionFile: string;
|
|
71
|
+
subagentSessionFile: string;
|
|
72
|
+
createdAt: number;
|
|
73
|
+
updatedAt: number;
|
|
74
|
+
state: AgentSessionState;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const agentSessionLinksById = new Map<string, AgentSessionLink>();
|
|
78
|
+
const agentSessionIdsByParent = new Map<string, string[]>();
|
|
79
|
+
const parentSessionByChild = new Map<string, string>();
|
|
80
|
+
|
|
81
|
+
interface LiveSubagentRuntime {
|
|
82
|
+
sessionFile?: string;
|
|
83
|
+
parentSessionFile?: string;
|
|
84
|
+
agentName: string;
|
|
85
|
+
isBusy: () => boolean;
|
|
86
|
+
sendPrompt: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
87
|
+
sendSteer: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
88
|
+
sendFollowUp: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const liveRuntimeBySessionFile = new Map<string, LiveSubagentRuntime>();
|
|
92
|
+
let agentSessionLinkCounter = 0;
|
|
93
|
+
|
|
94
|
+
function listSessionFiles(sessionDir: string): string[] {
|
|
95
|
+
if (!fs.existsSync(sessionDir)) return [];
|
|
96
|
+
try {
|
|
97
|
+
return fs
|
|
98
|
+
.readdirSync(sessionDir)
|
|
99
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
100
|
+
.map((name) => path.join(sessionDir, name));
|
|
101
|
+
} catch {
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function detectNewSubagentSessionFile(sessionDir: string, before: Set<string>, startedAt: number): string | undefined {
|
|
107
|
+
const after = listSessionFiles(sessionDir);
|
|
108
|
+
const created = after.filter((file) => !before.has(file));
|
|
109
|
+
const candidates = created.length > 0 ? created : after;
|
|
110
|
+
const ranked = candidates
|
|
111
|
+
.map((file) => {
|
|
112
|
+
let mtime = 0;
|
|
113
|
+
try {
|
|
114
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
115
|
+
} catch {
|
|
116
|
+
mtime = 0;
|
|
117
|
+
}
|
|
118
|
+
return { file, mtime };
|
|
119
|
+
})
|
|
120
|
+
.filter((entry) => entry.mtime >= startedAt - 5000)
|
|
121
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
122
|
+
return ranked[0]?.file;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function registerAgentSessionLink(link: Omit<AgentSessionLink, "id" | "createdAt" | "updatedAt">): AgentSessionLink {
|
|
126
|
+
const now = Date.now();
|
|
127
|
+
const id = `agent-${++agentSessionLinkCounter}`;
|
|
128
|
+
const full: AgentSessionLink = { ...link, id, createdAt: now, updatedAt: now };
|
|
129
|
+
agentSessionLinksById.set(id, full);
|
|
130
|
+
const list = agentSessionIdsByParent.get(link.parentSessionFile) ?? [];
|
|
131
|
+
list.push(id);
|
|
132
|
+
agentSessionIdsByParent.set(link.parentSessionFile, list);
|
|
133
|
+
parentSessionByChild.set(link.subagentSessionFile, link.parentSessionFile);
|
|
134
|
+
return full;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function updateAgentSessionLinkState(subagentSessionFile: string, state: AgentSessionState): void {
|
|
138
|
+
for (const link of agentSessionLinksById.values()) {
|
|
139
|
+
if (link.subagentSessionFile === subagentSessionFile) {
|
|
140
|
+
link.state = state;
|
|
141
|
+
link.updatedAt = Date.now();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function upsertAgentSessionLink(
|
|
148
|
+
agentName: string,
|
|
149
|
+
task: string,
|
|
150
|
+
parentSessionFile: string,
|
|
151
|
+
subagentSessionFile: string,
|
|
152
|
+
state: AgentSessionState,
|
|
153
|
+
): void {
|
|
154
|
+
const existingParent = parentSessionByChild.get(subagentSessionFile);
|
|
155
|
+
if (!existingParent) {
|
|
156
|
+
registerAgentSessionLink({
|
|
157
|
+
agentName,
|
|
158
|
+
task,
|
|
159
|
+
parentSessionFile,
|
|
160
|
+
subagentSessionFile,
|
|
161
|
+
state,
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
updateAgentSessionLinkState(subagentSessionFile, state);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function getAgentSessionLinksForParent(parentSessionFile: string): AgentSessionLink[] {
|
|
170
|
+
const ids = agentSessionIdsByParent.get(parentSessionFile) ?? [];
|
|
171
|
+
return ids
|
|
172
|
+
.map((id) => agentSessionLinksById.get(id))
|
|
173
|
+
.filter((entry): entry is AgentSessionLink => Boolean(entry))
|
|
174
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function readSessionHeader(sessionFile: string): {
|
|
178
|
+
parentSession?: string;
|
|
179
|
+
subagentName?: string;
|
|
180
|
+
subagentTask?: string;
|
|
181
|
+
subagentSystemPrompt?: string;
|
|
182
|
+
subagentTools?: string[];
|
|
183
|
+
} | null {
|
|
184
|
+
try {
|
|
185
|
+
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
186
|
+
const firstLine = content.split("\n").find((line) => line.trim().length > 0);
|
|
187
|
+
if (!firstLine) return null;
|
|
188
|
+
const parsed = JSON.parse(firstLine);
|
|
189
|
+
if (!parsed || parsed.type !== "session") return null;
|
|
190
|
+
return {
|
|
191
|
+
parentSession: typeof parsed.parentSession === "string" ? parsed.parentSession : undefined,
|
|
192
|
+
subagentName: typeof parsed.subagentName === "string" ? parsed.subagentName : undefined,
|
|
193
|
+
subagentTask: typeof parsed.subagentTask === "string" ? parsed.subagentTask : undefined,
|
|
194
|
+
subagentSystemPrompt: typeof parsed.subagentSystemPrompt === "string" ? parsed.subagentSystemPrompt : undefined,
|
|
195
|
+
subagentTools: Array.isArray(parsed.subagentTools)
|
|
196
|
+
? parsed.subagentTools.filter((tool: unknown): tool is string => typeof tool === "string")
|
|
197
|
+
: undefined,
|
|
198
|
+
};
|
|
199
|
+
} catch {
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function backfillAgentSessionLinksForParent(parentSessionFile: string, sessionDir: string): AgentSessionLink[] {
|
|
205
|
+
for (const sessionFile of listSessionFiles(sessionDir)) {
|
|
206
|
+
if (sessionFile === parentSessionFile) continue;
|
|
207
|
+
const header = readSessionHeader(sessionFile);
|
|
208
|
+
if (header?.parentSession !== parentSessionFile) continue;
|
|
209
|
+
const existingParent = parentSessionByChild.get(sessionFile);
|
|
210
|
+
if (!existingParent) {
|
|
211
|
+
registerAgentSessionLink({
|
|
212
|
+
agentName: header.subagentName ?? "subagent",
|
|
213
|
+
task: header.subagentTask ?? "Recovered from persisted session lineage",
|
|
214
|
+
parentSessionFile,
|
|
215
|
+
subagentSessionFile: sessionFile,
|
|
216
|
+
state: "completed",
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
return getAgentSessionLinksForParent(parentSessionFile);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
function formatSwitchTargetSummary(target: AgentSwitchTarget): string {
|
|
224
|
+
const current = target.isCurrent ? " (current)" : "";
|
|
225
|
+
if (target.kind === "parent") {
|
|
226
|
+
return `● parent — main session${current}`;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const icon = target.state === "running" ? "▶" : target.state === "failed" ? "✗" : "✓";
|
|
230
|
+
return `${icon} ${target.agentName} — ${target.taskPreview}${current}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function buildSwitchTargetsForParent(
|
|
234
|
+
parentSessionFile: string,
|
|
235
|
+
currentSessionFile: string,
|
|
236
|
+
currentCwd: string,
|
|
237
|
+
trackedLinks: AgentSessionLink[],
|
|
238
|
+
runningJobs: BackgroundSubagentJob[],
|
|
239
|
+
): AgentSwitchTarget[] {
|
|
240
|
+
return buildAgentSwitchTargets({
|
|
241
|
+
currentSessionFile,
|
|
242
|
+
rootParentSessionFile: parentSessionFile,
|
|
243
|
+
currentCwd,
|
|
244
|
+
trackedLinks: trackedLinks.map((link) => ({
|
|
245
|
+
id: link.id,
|
|
246
|
+
agentName: link.agentName,
|
|
247
|
+
task: link.task,
|
|
248
|
+
parentSessionFile: link.parentSessionFile,
|
|
249
|
+
subagentSessionFile: link.subagentSessionFile,
|
|
250
|
+
updatedAt: link.updatedAt,
|
|
251
|
+
state: link.state,
|
|
252
|
+
})),
|
|
253
|
+
runningJobs: runningJobs.map((job) => ({
|
|
254
|
+
id: job.id,
|
|
255
|
+
agentName: job.agentName,
|
|
256
|
+
task: job.task,
|
|
257
|
+
startedAt: job.startedAt,
|
|
258
|
+
parentSessionFile: job.parentSessionFile,
|
|
259
|
+
sessionFile: job.sessionFile,
|
|
260
|
+
cwd: job.cwd,
|
|
261
|
+
})),
|
|
262
|
+
});
|
|
263
|
+
}
|
|
264
|
+
|
|
59
265
|
const AwaitSubagentParams = Type.Object({
|
|
60
266
|
jobs: Type.Optional(
|
|
61
267
|
Type.Array(Type.String(), {
|
|
@@ -325,15 +531,31 @@ interface SingleResult {
|
|
|
325
531
|
errorMessage?: string;
|
|
326
532
|
step?: number;
|
|
327
533
|
backgroundJobId?: string;
|
|
534
|
+
sessionFile?: string;
|
|
535
|
+
parentSessionFile?: string;
|
|
328
536
|
}
|
|
329
537
|
|
|
538
|
+
type BackgroundResultPayload = {
|
|
539
|
+
summary: string;
|
|
540
|
+
stderr: string;
|
|
541
|
+
exitCode: number;
|
|
542
|
+
model?: string;
|
|
543
|
+
sessionFile?: string;
|
|
544
|
+
parentSessionFile?: string;
|
|
545
|
+
};
|
|
546
|
+
|
|
330
547
|
interface ForegroundSingleRunControl {
|
|
331
548
|
agentName: string;
|
|
332
549
|
task: string;
|
|
333
550
|
cwd: string;
|
|
551
|
+
parentSessionFile?: string;
|
|
334
552
|
abortController: AbortController;
|
|
335
|
-
resultPromise: Promise<
|
|
553
|
+
resultPromise: Promise<BackgroundResultPayload>;
|
|
336
554
|
adoptToBackground: (jobId: string) => boolean;
|
|
555
|
+
sendPrompt?: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
556
|
+
sendSteer?: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
557
|
+
sendFollowUp?: (text: string, images?: ImageContent[]) => Promise<void>;
|
|
558
|
+
isBusy?: () => boolean;
|
|
337
559
|
}
|
|
338
560
|
|
|
339
561
|
interface ForegroundSingleRunHooks {
|
|
@@ -447,7 +669,10 @@ function processSubagentEventLine(
|
|
|
447
669
|
line: string,
|
|
448
670
|
currentResult: SingleResult,
|
|
449
671
|
emitUpdate: () => void,
|
|
450
|
-
proc
|
|
672
|
+
proc: ChildProcess | undefined,
|
|
673
|
+
onSessionInfo?: (info: { sessionFile?: string; parentSessionFile?: string }) => void,
|
|
674
|
+
onEventType?: (eventType: string) => void,
|
|
675
|
+
onParsedEvent?: (event: any) => void,
|
|
451
676
|
): boolean {
|
|
452
677
|
if (!line.trim()) return false;
|
|
453
678
|
let event: any;
|
|
@@ -457,6 +682,29 @@ function processSubagentEventLine(
|
|
|
457
682
|
return false;
|
|
458
683
|
}
|
|
459
684
|
|
|
685
|
+
const eventType = typeof event.type === "string" ? event.type : "unknown";
|
|
686
|
+
onEventType?.(eventType);
|
|
687
|
+
onParsedEvent?.(event);
|
|
688
|
+
|
|
689
|
+
if (event.type === "subagent_session_info") {
|
|
690
|
+
let changed = false;
|
|
691
|
+
if (typeof event.sessionFile === "string" && event.sessionFile) {
|
|
692
|
+
if (currentResult.sessionFile !== event.sessionFile) changed = true;
|
|
693
|
+
currentResult.sessionFile = event.sessionFile;
|
|
694
|
+
}
|
|
695
|
+
if (typeof event.parentSessionFile === "string" && event.parentSessionFile) {
|
|
696
|
+
if (currentResult.parentSessionFile !== event.parentSessionFile) changed = true;
|
|
697
|
+
currentResult.parentSessionFile = event.parentSessionFile;
|
|
698
|
+
}
|
|
699
|
+
if (changed) {
|
|
700
|
+
onSessionInfo?.({
|
|
701
|
+
sessionFile: currentResult.sessionFile,
|
|
702
|
+
parentSessionFile: currentResult.parentSessionFile,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
return false;
|
|
706
|
+
}
|
|
707
|
+
|
|
460
708
|
if (proc && isSubagentPermissionRequest(event)) {
|
|
461
709
|
void handleSubagentPermissionRequest(event, proc);
|
|
462
710
|
return false;
|
|
@@ -516,6 +764,10 @@ async function runSingleAgent(
|
|
|
516
764
|
signal: AbortSignal | undefined,
|
|
517
765
|
onUpdate: OnUpdateCallback | undefined,
|
|
518
766
|
makeDetails: (results: SingleResult[]) => SubagentDetails,
|
|
767
|
+
parentSessionFile: string | undefined,
|
|
768
|
+
attachableSession: boolean,
|
|
769
|
+
onSessionInfo?: (info: { sessionFile?: string; parentSessionFile?: string }) => void,
|
|
770
|
+
onSubagentEvent?: (event: any, currentResult: SingleResult) => void,
|
|
519
771
|
foregroundHooks?: ForegroundSingleRunHooks,
|
|
520
772
|
): Promise<SingleResult> {
|
|
521
773
|
const agent = agents.find((a) => a.name === agentName);
|
|
@@ -555,6 +807,7 @@ async function runSingleAgent(
|
|
|
555
807
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
556
808
|
model: inferredModel,
|
|
557
809
|
step,
|
|
810
|
+
parentSessionFile,
|
|
558
811
|
};
|
|
559
812
|
|
|
560
813
|
const emitUpdate = () => {
|
|
@@ -593,12 +846,23 @@ async function runSingleAgent(
|
|
|
593
846
|
tmpPromptDir = tmp.dir;
|
|
594
847
|
tmpPromptPath = tmp.filePath;
|
|
595
848
|
}
|
|
596
|
-
const
|
|
849
|
+
const effectiveCwd = cwd ?? defaultCwd;
|
|
850
|
+
const subagentSessionDir = parentSessionFile ? path.dirname(parentSessionFile) : undefined;
|
|
851
|
+
const sessionFilesBefore = attachableSession && subagentSessionDir
|
|
852
|
+
? new Set(listSessionFiles(subagentSessionDir))
|
|
853
|
+
: undefined;
|
|
854
|
+
const launchStartedAt = Date.now();
|
|
855
|
+
|
|
856
|
+
const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel, {
|
|
857
|
+
noSession: !attachableSession,
|
|
858
|
+
parentSessionFile: parentSessionFile,
|
|
859
|
+
mode: attachableSession ? "rpc" : "json",
|
|
860
|
+
});
|
|
597
861
|
|
|
598
862
|
const exitCode = await new Promise<number>((resolve) => {
|
|
599
863
|
const bundledPaths = getBundledExtensionPathsFromEnv();
|
|
600
864
|
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
|
|
601
|
-
const cliPath = resolveSubagentCliPath(
|
|
865
|
+
const cliPath = resolveSubagentCliPath(effectiveCwd);
|
|
602
866
|
if (!cliPath) {
|
|
603
867
|
currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
|
|
604
868
|
resolve(1);
|
|
@@ -607,7 +871,7 @@ async function runSingleAgent(
|
|
|
607
871
|
const proc = spawn(
|
|
608
872
|
process.execPath,
|
|
609
873
|
[cliPath, ...extensionArgs, ...args],
|
|
610
|
-
{ cwd:
|
|
874
|
+
{ cwd: effectiveCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] },
|
|
611
875
|
);
|
|
612
876
|
// Keep stdin open so approval/classifier responses can be proxied back
|
|
613
877
|
// into the child process. Closing it here can leave the subagent stuck
|
|
@@ -617,14 +881,26 @@ async function runSingleAgent(
|
|
|
617
881
|
let completionSeen = false;
|
|
618
882
|
let resolved = false;
|
|
619
883
|
let foregroundReleased = false;
|
|
884
|
+
let isBusy = false;
|
|
885
|
+
let commandSeq = 0;
|
|
886
|
+
const pendingCommandResponses = new Map<string, { resolve: (data: any) => void; reject: (error: Error) => void }>();
|
|
620
887
|
const procAbortController = new AbortController();
|
|
621
|
-
let resolveBackgroundResult: ((value:
|
|
888
|
+
let resolveBackgroundResult: ((value: BackgroundResultPayload) => void) | undefined;
|
|
622
889
|
let rejectBackgroundResult: ((reason?: unknown) => void) | undefined;
|
|
623
|
-
const backgroundResultPromise = new Promise<
|
|
890
|
+
const backgroundResultPromise = new Promise<BackgroundResultPayload>((resolveBg, rejectBg) => {
|
|
624
891
|
resolveBackgroundResult = resolveBg;
|
|
625
892
|
rejectBackgroundResult = rejectBg;
|
|
626
893
|
});
|
|
627
894
|
|
|
895
|
+
const sendRpcCommand = async (command: Record<string, unknown>): Promise<any> => {
|
|
896
|
+
const id = `sa_cmd_${++commandSeq}`;
|
|
897
|
+
if (!proc.stdin) throw new Error("Subagent RPC stdin is not available.");
|
|
898
|
+
return new Promise((resolveCmd, rejectCmd) => {
|
|
899
|
+
pendingCommandResponses.set(id, { resolve: resolveCmd, reject: rejectCmd });
|
|
900
|
+
proc.stdin!.write(JSON.stringify({ id, ...command }) + "\n");
|
|
901
|
+
});
|
|
902
|
+
};
|
|
903
|
+
|
|
628
904
|
const finishForeground = (code: number) => {
|
|
629
905
|
if (resolved) return;
|
|
630
906
|
resolved = true;
|
|
@@ -648,9 +924,26 @@ async function runSingleAgent(
|
|
|
648
924
|
agentName,
|
|
649
925
|
task,
|
|
650
926
|
cwd: cwd ?? defaultCwd,
|
|
927
|
+
parentSessionFile,
|
|
651
928
|
abortController: procAbortController,
|
|
652
929
|
resultPromise: backgroundResultPromise,
|
|
653
930
|
adoptToBackground,
|
|
931
|
+
sendPrompt: attachableSession
|
|
932
|
+
? async (text: string, images?: ImageContent[]) => {
|
|
933
|
+
await sendRpcCommand({ type: "prompt", message: text, images });
|
|
934
|
+
}
|
|
935
|
+
: undefined,
|
|
936
|
+
sendSteer: attachableSession
|
|
937
|
+
? async (text: string, images?: ImageContent[]) => {
|
|
938
|
+
await sendRpcCommand({ type: "steer", message: text, images });
|
|
939
|
+
}
|
|
940
|
+
: undefined,
|
|
941
|
+
sendFollowUp: attachableSession
|
|
942
|
+
? async (text: string, images?: ImageContent[]) => {
|
|
943
|
+
await sendRpcCommand({ type: "follow_up", message: text, images });
|
|
944
|
+
}
|
|
945
|
+
: undefined,
|
|
946
|
+
isBusy: attachableSession ? () => isBusy : undefined,
|
|
654
947
|
});
|
|
655
948
|
|
|
656
949
|
proc.stdout.on("data", (data) => {
|
|
@@ -658,7 +951,30 @@ async function runSingleAgent(
|
|
|
658
951
|
const lines = buffer.split("\n");
|
|
659
952
|
buffer = lines.pop() || "";
|
|
660
953
|
for (const line of lines) {
|
|
661
|
-
|
|
954
|
+
const trimmed = line.trim();
|
|
955
|
+
if (!trimmed) continue;
|
|
956
|
+
if (attachableSession) {
|
|
957
|
+
try {
|
|
958
|
+
const parsed = JSON.parse(trimmed);
|
|
959
|
+
if (parsed?.type === "response" && typeof parsed.id === "string" && pendingCommandResponses.has(parsed.id)) {
|
|
960
|
+
const pending = pendingCommandResponses.get(parsed.id)!;
|
|
961
|
+
pendingCommandResponses.delete(parsed.id);
|
|
962
|
+
if (parsed.success === false) {
|
|
963
|
+
pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : "Subagent RPC command failed."));
|
|
964
|
+
} else {
|
|
965
|
+
pending.resolve(parsed.data);
|
|
966
|
+
}
|
|
967
|
+
continue;
|
|
968
|
+
}
|
|
969
|
+
} catch {
|
|
970
|
+
// Fall through to generic event processing.
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (processSubagentEventLine(trimmed, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
|
|
975
|
+
if (eventType === "agent_start") isBusy = true;
|
|
976
|
+
if (eventType === "agent_end") isBusy = false;
|
|
977
|
+
}, (event) => onSubagentEvent?.(event, currentResult))) {
|
|
662
978
|
completionSeen = true;
|
|
663
979
|
try {
|
|
664
980
|
proc.kill("SIGTERM");
|
|
@@ -676,16 +992,33 @@ async function runSingleAgent(
|
|
|
676
992
|
proc.on("close", (code) => {
|
|
677
993
|
liveSubagentProcesses.delete(proc);
|
|
678
994
|
if (buffer.trim()) {
|
|
679
|
-
const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc)
|
|
995
|
+
const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
|
|
996
|
+
if (eventType === "agent_start") isBusy = true;
|
|
997
|
+
if (eventType === "agent_end") isBusy = false;
|
|
998
|
+
}, (event) => onSubagentEvent?.(event, currentResult));
|
|
680
999
|
completionSeen = completionSeen || completedOnFlush;
|
|
681
1000
|
}
|
|
1001
|
+
isBusy = false;
|
|
1002
|
+
for (const pending of pendingCommandResponses.values()) {
|
|
1003
|
+
pending.reject(new Error("Subagent process closed before command response."));
|
|
1004
|
+
}
|
|
1005
|
+
pendingCommandResponses.clear();
|
|
1006
|
+
|
|
682
1007
|
const finalExitCode = completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0);
|
|
683
1008
|
currentResult.exitCode = finalExitCode;
|
|
1009
|
+
|
|
1010
|
+
if (attachableSession && sessionFilesBefore && subagentSessionDir && !currentResult.sessionFile) {
|
|
1011
|
+
const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
|
|
1012
|
+
if (detected) currentResult.sessionFile = detected;
|
|
1013
|
+
}
|
|
1014
|
+
|
|
684
1015
|
resolveBackgroundResult?.({
|
|
685
1016
|
summary: getFinalOutput(currentResult.messages),
|
|
686
1017
|
stderr: currentResult.stderr,
|
|
687
1018
|
exitCode: finalExitCode,
|
|
688
1019
|
model: currentResult.model,
|
|
1020
|
+
sessionFile: currentResult.sessionFile,
|
|
1021
|
+
parentSessionFile: currentResult.parentSessionFile,
|
|
689
1022
|
});
|
|
690
1023
|
foregroundHooks?.onFinish?.();
|
|
691
1024
|
finishForeground(finalExitCode);
|
|
@@ -693,12 +1026,32 @@ async function runSingleAgent(
|
|
|
693
1026
|
|
|
694
1027
|
proc.on("error", (error) => {
|
|
695
1028
|
liveSubagentProcesses.delete(proc);
|
|
1029
|
+
isBusy = false;
|
|
1030
|
+
for (const pending of pendingCommandResponses.values()) {
|
|
1031
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
1032
|
+
}
|
|
1033
|
+
pendingCommandResponses.clear();
|
|
696
1034
|
rejectBackgroundResult?.(error);
|
|
697
1035
|
foregroundHooks?.onFinish?.();
|
|
698
1036
|
finishForeground(1);
|
|
699
1037
|
});
|
|
700
1038
|
|
|
1039
|
+
if (attachableSession) {
|
|
1040
|
+
void sendRpcCommand({ type: "prompt", message: task }).catch((error) => {
|
|
1041
|
+
currentResult.stderr += error instanceof Error ? error.message : String(error);
|
|
1042
|
+
try {
|
|
1043
|
+
proc.kill("SIGTERM");
|
|
1044
|
+
} catch {
|
|
1045
|
+
/* ignore */
|
|
1046
|
+
}
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
|
|
701
1050
|
const killProc = () => {
|
|
1051
|
+
// If the process has been adopted to the background (e.g. via Ctrl+B or
|
|
1052
|
+
// /agent attach_live), foregroundReleased is true and the process should
|
|
1053
|
+
// survive the parent session's abort — don't kill it.
|
|
1054
|
+
if (foregroundReleased) return;
|
|
702
1055
|
wasAborted = true;
|
|
703
1056
|
procAbortController.abort();
|
|
704
1057
|
proc.kill("SIGTERM");
|
|
@@ -725,6 +1078,12 @@ async function runSingleAgent(
|
|
|
725
1078
|
});
|
|
726
1079
|
|
|
727
1080
|
currentResult.exitCode = exitCode;
|
|
1081
|
+
if (attachableSession && sessionFilesBefore && subagentSessionDir) {
|
|
1082
|
+
const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
|
|
1083
|
+
if (detected) {
|
|
1084
|
+
currentResult.sessionFile = detected;
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
728
1087
|
if (wasAborted) throw new Error("Subagent was aborted");
|
|
729
1088
|
return currentResult;
|
|
730
1089
|
} finally {
|
|
@@ -805,15 +1164,68 @@ export default function(pi: ExtensionAPI) {
|
|
|
805
1164
|
const foregroundSubagentHint = "Ctrl+B: move foreground subagent to background";
|
|
806
1165
|
type ActiveForegroundSubagent = ForegroundSingleRunControl & { claimed: boolean };
|
|
807
1166
|
let activeForegroundSubagent: ActiveForegroundSubagent | null = null;
|
|
1167
|
+
let activeSessionFileForUi: string | undefined;
|
|
1168
|
+
const liveStreamBufferBySession = new Map<string, string>();
|
|
1169
|
+
|
|
1170
|
+
function flushLiveStream(sessionFile: string): void {
|
|
1171
|
+
const buffered = liveStreamBufferBySession.get(sessionFile);
|
|
1172
|
+
if (!buffered || !buffered.trim()) return;
|
|
1173
|
+
liveStreamBufferBySession.set(sessionFile, "");
|
|
1174
|
+
pi.sendMessage(
|
|
1175
|
+
{
|
|
1176
|
+
customType: "live_subagent_stream",
|
|
1177
|
+
content: buffered,
|
|
1178
|
+
display: true,
|
|
1179
|
+
},
|
|
1180
|
+
{ deliverAs: "followUp" },
|
|
1181
|
+
);
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function pushLiveStreamDelta(sessionFile: string, delta: string): void {
|
|
1185
|
+
const prev = liveStreamBufferBySession.get(sessionFile) ?? "";
|
|
1186
|
+
const next = prev + delta;
|
|
1187
|
+
liveStreamBufferBySession.set(sessionFile, next);
|
|
1188
|
+
if (next.length >= 120 || next.includes("\n")) {
|
|
1189
|
+
flushLiveStream(sessionFile);
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function getCurrentSessionSubagentMetadata(sessionFile: string | undefined) {
|
|
1194
|
+
if (!sessionFile) return null;
|
|
1195
|
+
return readSessionHeader(sessionFile);
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
function applyCurrentSessionSubagentTools(ctx: any): void {
|
|
1199
|
+
const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
|
|
1200
|
+
if (metadata?.subagentTools && metadata.subagentTools.length > 0) {
|
|
1201
|
+
ctx.setActiveTools(metadata.subagentTools);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
808
1204
|
|
|
809
1205
|
function getBgManager(): BackgroundJobManager {
|
|
810
1206
|
if (!bgManager) throw new Error("BackgroundJobManager not initialized.");
|
|
811
1207
|
return bgManager;
|
|
812
1208
|
}
|
|
813
1209
|
|
|
814
|
-
pi.on("session_start", async () => {
|
|
1210
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
1211
|
+
activeSessionFileForUi = ctx.sessionManager.getSessionFile();
|
|
815
1212
|
bgManager = new BackgroundJobManager({
|
|
816
1213
|
onJobComplete: (job) => {
|
|
1214
|
+
if (job.sessionFile && job.parentSessionFile) {
|
|
1215
|
+
const existingParent = parentSessionByChild.get(job.sessionFile);
|
|
1216
|
+
if (!existingParent) {
|
|
1217
|
+
registerAgentSessionLink({
|
|
1218
|
+
agentName: job.agentName,
|
|
1219
|
+
task: job.task,
|
|
1220
|
+
parentSessionFile: job.parentSessionFile,
|
|
1221
|
+
subagentSessionFile: job.sessionFile,
|
|
1222
|
+
state: job.status === "failed" ? "failed" : "completed",
|
|
1223
|
+
});
|
|
1224
|
+
} else {
|
|
1225
|
+
updateAgentSessionLinkState(job.sessionFile, job.status === "failed" ? "failed" : "completed");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
|
|
817
1229
|
if (job.awaited) return;
|
|
818
1230
|
const statusEmoji = job.status === "completed" ? "✓" : job.status === "cancelled" ? "✗ cancelled" : "✗ failed";
|
|
819
1231
|
const elapsed = ((Date.now() - job.startedAt) / 1000).toFixed(1);
|
|
@@ -839,25 +1251,82 @@ export default function(pi: ExtensionAPI) {
|
|
|
839
1251
|
);
|
|
840
1252
|
},
|
|
841
1253
|
});
|
|
1254
|
+
applyCurrentSessionSubagentTools(ctx);
|
|
842
1255
|
});
|
|
843
1256
|
|
|
1257
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
1258
|
+
activeSessionFileForUi = ctx.sessionManager.getSessionFile();
|
|
1259
|
+
applyCurrentSessionSubagentTools(ctx);
|
|
1260
|
+
});
|
|
844
1261
|
|
|
845
|
-
pi.on("
|
|
846
|
-
|
|
847
|
-
if (
|
|
848
|
-
|
|
849
|
-
|
|
1262
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
1263
|
+
const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
|
|
1264
|
+
if (!metadata?.subagentSystemPrompt) return;
|
|
1265
|
+
const subagentName = metadata.subagentName ?? "subagent";
|
|
1266
|
+
const taskNote = metadata.subagentTask
|
|
1267
|
+
? `Original delegated task: ${metadata.subagentTask}`
|
|
1268
|
+
: "Continue operating as the delegated subagent for this session.";
|
|
1269
|
+
const antiRecursion = [
|
|
1270
|
+
`You are already the ${subagentName} subagent for this session.`,
|
|
1271
|
+
"Do not spawn or delegate to another subagent with the same name as yourself.",
|
|
1272
|
+
`If the user asks you to continue ${subagentName} work, do that work directly in this session.`,
|
|
1273
|
+
taskNote,
|
|
1274
|
+
"IMPORTANT: There is NO human available to answer questions in this session. Do NOT call ask_user_questions. Make all decisions autonomously based on the task and context.",
|
|
1275
|
+
].join("\n");
|
|
1276
|
+
return {
|
|
1277
|
+
systemPrompt: `${event.systemPrompt}\n\n${antiRecursion}\n\n${metadata.subagentSystemPrompt}`,
|
|
1278
|
+
};
|
|
1279
|
+
});
|
|
1280
|
+
pi.on("input", async (event, ctx) => {
|
|
1281
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1282
|
+
if (!sessionFile) return;
|
|
1283
|
+
const runtime = liveRuntimeBySessionFile.get(sessionFile);
|
|
1284
|
+
if (!runtime) return;
|
|
1285
|
+
|
|
1286
|
+
const text = event.text?.trim();
|
|
1287
|
+
if (!text) return { action: "handled" as const };
|
|
1288
|
+
|
|
1289
|
+
const isSlashCommand = text.startsWith("/");
|
|
1290
|
+
if (isSlashCommand) return;
|
|
1291
|
+
|
|
1292
|
+
try {
|
|
1293
|
+
if (runtime.isBusy()) {
|
|
1294
|
+
await runtime.sendSteer(text, event.images);
|
|
1295
|
+
ctx.ui.notify(`Sent steer to running subagent ${runtime.agentName}.`, "info");
|
|
1296
|
+
} else {
|
|
1297
|
+
await runtime.sendPrompt(text, event.images);
|
|
1298
|
+
ctx.ui.notify(`Sent prompt to live subagent ${runtime.agentName}.`, "info");
|
|
850
1299
|
}
|
|
1300
|
+
return { action: "handled" as const };
|
|
1301
|
+
} catch (error) {
|
|
1302
|
+
ctx.ui.notify(
|
|
1303
|
+
`Failed to send input to live subagent ${runtime.agentName}: ${error instanceof Error ? error.message : String(error)}`,
|
|
1304
|
+
"error",
|
|
1305
|
+
);
|
|
1306
|
+
return { action: "handled" as const };
|
|
851
1307
|
}
|
|
852
1308
|
});
|
|
853
1309
|
|
|
1310
|
+
|
|
1311
|
+
pi.on("session_before_switch", async () => {
|
|
1312
|
+
if (activeSessionFileForUi) flushLiveStream(activeSessionFileForUi);
|
|
1313
|
+
activeForegroundSubagent = null;
|
|
1314
|
+
});
|
|
1315
|
+
|
|
854
1316
|
pi.on("session_shutdown", async () => {
|
|
1317
|
+
if (activeSessionFileForUi) flushLiveStream(activeSessionFileForUi);
|
|
1318
|
+
activeSessionFileForUi = undefined;
|
|
855
1319
|
activeForegroundSubagent = null;
|
|
856
1320
|
await stopLiveSubagents();
|
|
857
1321
|
if (bgManager) {
|
|
858
1322
|
bgManager.shutdown();
|
|
859
1323
|
bgManager = null;
|
|
860
1324
|
}
|
|
1325
|
+
agentSessionLinksById.clear();
|
|
1326
|
+
agentSessionIdsByParent.clear();
|
|
1327
|
+
parentSessionByChild.clear();
|
|
1328
|
+
liveRuntimeBySessionFile.clear();
|
|
1329
|
+
liveStreamBufferBySession.clear();
|
|
861
1330
|
});
|
|
862
1331
|
|
|
863
1332
|
// /subagents command
|
|
@@ -972,6 +1441,221 @@ export default function(pi: ExtensionAPI) {
|
|
|
972
1441
|
},
|
|
973
1442
|
});
|
|
974
1443
|
|
|
1444
|
+
// /agent command - switch to the parent or a tracked subagent session
|
|
1445
|
+
pi.registerCommand("agent", {
|
|
1446
|
+
description: "Switch focus to parent/subagent sessions (/agent picker, /agent <id|index|name>, /agent parent)",
|
|
1447
|
+
handler: async (args: string, ctx) => {
|
|
1448
|
+
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
1449
|
+
if (!currentSessionFile) {
|
|
1450
|
+
ctx.ui.notify("Current session is in-memory only; /agent requires a persisted session file.", "warning");
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const arg = args.trim();
|
|
1455
|
+
const parentSessionFile = parentSessionByChild.get(currentSessionFile);
|
|
1456
|
+
const currentParent = parentSessionFile ?? currentSessionFile;
|
|
1457
|
+
const currentSessionDir = path.dirname(currentParent);
|
|
1458
|
+
|
|
1459
|
+
let tracked = getAgentSessionLinksForParent(currentParent).filter((entry) => fs.existsSync(entry.subagentSessionFile));
|
|
1460
|
+
if (tracked.length === 0) {
|
|
1461
|
+
tracked = backfillAgentSessionLinksForParent(currentParent, currentSessionDir)
|
|
1462
|
+
.filter((entry) => fs.existsSync(entry.subagentSessionFile));
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
const runningJobs = bgManager?.getRunningJobs() ?? [];
|
|
1466
|
+
const switchTargets = buildSwitchTargetsForParent(
|
|
1467
|
+
currentParent,
|
|
1468
|
+
currentSessionFile,
|
|
1469
|
+
ctx.cwd,
|
|
1470
|
+
tracked,
|
|
1471
|
+
runningJobs,
|
|
1472
|
+
);
|
|
1473
|
+
|
|
1474
|
+
const applySwitchTarget = async (target: AgentSwitchTarget): Promise<void> => {
|
|
1475
|
+
if (target.selectionAction === "blocked") {
|
|
1476
|
+
ctx.ui.notify(target.blockedReason ?? "That target cannot be selected yet.", "warning");
|
|
1477
|
+
return;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
if (target.selectionAction === "attach_live") {
|
|
1481
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1482
|
+
ctx.ui.notify(`Live subagent session file is missing: ${target.sessionFile}`, "error");
|
|
1483
|
+
return;
|
|
1484
|
+
}
|
|
1485
|
+
const liveRuntime = liveRuntimeBySessionFile.get(target.sessionFile);
|
|
1486
|
+
if (!liveRuntime) {
|
|
1487
|
+
ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
|
|
1488
|
+
return;
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// Adopt the foreground subagent to background before switching sessions.
|
|
1492
|
+
// switchSession calls abort() which would fire the tool signal and SIGTERM
|
|
1493
|
+
// the running subagent process. Adopting to background detaches the process
|
|
1494
|
+
// from the foreground abort chain so it survives the session switch.
|
|
1495
|
+
const foreground = activeForegroundSubagent;
|
|
1496
|
+
if (foreground && !foreground.claimed && bgManager) {
|
|
1497
|
+
foreground.claimed = true;
|
|
1498
|
+
try {
|
|
1499
|
+
const jobId = bgManager.adoptRunning(
|
|
1500
|
+
foreground.agentName,
|
|
1501
|
+
foreground.task,
|
|
1502
|
+
foreground.cwd,
|
|
1503
|
+
foreground.abortController,
|
|
1504
|
+
foreground.resultPromise,
|
|
1505
|
+
{
|
|
1506
|
+
parentSessionFile: foreground.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
|
|
1507
|
+
},
|
|
1508
|
+
);
|
|
1509
|
+
const released = foreground.adoptToBackground(jobId);
|
|
1510
|
+
if (!released) {
|
|
1511
|
+
foreground.claimed = false;
|
|
1512
|
+
bgManager.cancel(jobId);
|
|
1513
|
+
} else {
|
|
1514
|
+
activeForegroundSubagent = null;
|
|
1515
|
+
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
1516
|
+
}
|
|
1517
|
+
} catch {
|
|
1518
|
+
foreground.claimed = false;
|
|
1519
|
+
}
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1523
|
+
if (switched.cancelled) {
|
|
1524
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
ctx.ui.notify(`Attached to running subagent ${target.agentName}. Prompts in this session are routed live (busy => steer, idle => prompt). Use /agent parent to return.`, "info");
|
|
1528
|
+
return;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
if (target.kind === "parent") {
|
|
1532
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1533
|
+
ctx.ui.notify(`Parent session file not found: ${target.sessionFile}`, "error");
|
|
1534
|
+
return;
|
|
1535
|
+
}
|
|
1536
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1537
|
+
if (switched.cancelled) {
|
|
1538
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
ctx.ui.notify("Switched to parent session.", "info");
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1546
|
+
ctx.ui.notify(`Subagent session file is missing: ${target.sessionFile}`, "error");
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1551
|
+
if (switched.cancelled) {
|
|
1552
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
updateAgentSessionLinkState(target.sessionFile, target.state === "failed" ? "failed" : "completed");
|
|
1556
|
+
ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
|
|
1557
|
+
};
|
|
1558
|
+
|
|
1559
|
+
if (!arg) {
|
|
1560
|
+
const subagentTargets = switchTargets.filter((target) => target.kind === "subagent");
|
|
1561
|
+
if (ctx.hasUI) {
|
|
1562
|
+
if (subagentTargets.length === 0 && !parentSessionFile) {
|
|
1563
|
+
ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
const selected = await showAgentSwitcher(ctx, switchTargets);
|
|
1568
|
+
if (!selected) return;
|
|
1569
|
+
await applySwitchTarget(selected);
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
if (subagentTargets.length === 0 && !parentSessionFile) {
|
|
1574
|
+
ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
|
|
1578
|
+
const lines = ["Agent switch targets:"];
|
|
1579
|
+
switchTargets.forEach((target, index) => {
|
|
1580
|
+
lines.push(`${index + 1}. ${formatSwitchTargetSummary(target)}`);
|
|
1581
|
+
});
|
|
1582
|
+
lines.push("", "Use `/agent <index|id|name>` for explicit targeting, or `/agent parent`.");
|
|
1583
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
1584
|
+
return;
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
if (arg === "parent" || arg === "main") {
|
|
1588
|
+
if (!parentSessionFile) {
|
|
1589
|
+
ctx.ui.notify("You are already in the parent/main session.", "info");
|
|
1590
|
+
return;
|
|
1591
|
+
}
|
|
1592
|
+
if (!fs.existsSync(parentSessionFile)) {
|
|
1593
|
+
ctx.ui.notify(`Parent session file not found: ${parentSessionFile}`, "error");
|
|
1594
|
+
return;
|
|
1595
|
+
}
|
|
1596
|
+
const switched = await ctx.switchSession(parentSessionFile);
|
|
1597
|
+
if (switched.cancelled) {
|
|
1598
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
ctx.ui.notify("Switched to parent session.", "info");
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
|
|
1605
|
+
let target: AgentSessionLink | undefined;
|
|
1606
|
+
if (/^\d+$/.test(arg)) {
|
|
1607
|
+
const index = Number.parseInt(arg, 10) - 1;
|
|
1608
|
+
target = tracked[index];
|
|
1609
|
+
}
|
|
1610
|
+
if (!target) {
|
|
1611
|
+
target = tracked.find((entry) => entry.id === arg);
|
|
1612
|
+
}
|
|
1613
|
+
if (!target) {
|
|
1614
|
+
target = tracked.find((entry) => entry.agentName === arg);
|
|
1615
|
+
}
|
|
1616
|
+
if (!target) {
|
|
1617
|
+
target = tracked.find((entry) => path.basename(entry.subagentSessionFile) === arg);
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
if (!target) {
|
|
1621
|
+
const runningTarget = switchTargets.find((entry) => entry.id === arg && entry.kind === "subagent");
|
|
1622
|
+
if (runningTarget?.state === "running") {
|
|
1623
|
+
ctx.ui.notify(runningTarget.blockedReason ?? "Selected subagent is still running. Live attach is not implemented yet.", "warning");
|
|
1624
|
+
return;
|
|
1625
|
+
}
|
|
1626
|
+
ctx.ui.notify(`Unknown subagent target: ${arg}. Run /agent to list available targets.`, "warning");
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
if (!fs.existsSync(target.subagentSessionFile)) {
|
|
1631
|
+
ctx.ui.notify(`Subagent session file is missing: ${target.subagentSessionFile}`, "error");
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
if (target.state === "running") {
|
|
1636
|
+
const liveRuntime = liveRuntimeBySessionFile.get(target.subagentSessionFile);
|
|
1637
|
+
if (!liveRuntime) {
|
|
1638
|
+
ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
|
|
1639
|
+
return;
|
|
1640
|
+
}
|
|
1641
|
+
const switched = await ctx.switchSession(target.subagentSessionFile);
|
|
1642
|
+
if (switched.cancelled) {
|
|
1643
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
ctx.ui.notify(`Attached to running subagent ${target.agentName}. Prompts in this session are routed live (busy => steer, idle => prompt). Use /agent parent to return.`, "info");
|
|
1647
|
+
return;
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
const switched = await ctx.switchSession(target.subagentSessionFile);
|
|
1651
|
+
if (switched.cancelled) {
|
|
1652
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
updateAgentSessionLinkState(target.subagentSessionFile, target.state === "failed" ? "failed" : "completed");
|
|
1656
|
+
ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
|
|
1657
|
+
},
|
|
1658
|
+
});
|
|
975
1659
|
pi.registerShortcut(Key.ctrl("b"), {
|
|
976
1660
|
description: shortcutDesc("Move foreground subagent to background", "/subagents list"),
|
|
977
1661
|
handler: async (ctx) => {
|
|
@@ -992,6 +1676,9 @@ export default function(pi: ExtensionAPI) {
|
|
|
992
1676
|
running.cwd,
|
|
993
1677
|
running.abortController,
|
|
994
1678
|
running.resultPromise,
|
|
1679
|
+
{
|
|
1680
|
+
parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
|
|
1681
|
+
},
|
|
995
1682
|
);
|
|
996
1683
|
} catch (error) {
|
|
997
1684
|
running.claimed = false;
|
|
@@ -1063,7 +1750,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1063
1750
|
"For broad review or audit requests, use scout only as a prep step; the parent model or a reviewer should make the final judgments.",
|
|
1064
1751
|
"Skip scout when the user already named the exact file/function to inspect or the task is obviously narrow.",
|
|
1065
1752
|
"Use parallel mode when tasks are independent and don't need each other's output.",
|
|
1066
|
-
"
|
|
1753
|
+
"Default to foreground (background: false) for single-mode subagents. Only set background: true when the user explicitly asks to run it in the background or to keep chatting while it runs.",
|
|
1067
1754
|
"If the user wants to wait for a background subagent result, use await_subagent.",
|
|
1068
1755
|
],
|
|
1069
1756
|
parameters: SubagentParams,
|
|
@@ -1075,6 +1762,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1075
1762
|
const confirmProjectAgents = params.confirmProjectAgents ?? false;
|
|
1076
1763
|
const cmuxClient = CmuxClient.fromPreferences(loadEffectivePreferences()?.preferences);
|
|
1077
1764
|
const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
|
|
1765
|
+
const invokingSessionFile = ctx.sessionManager.getSessionFile();
|
|
1078
1766
|
|
|
1079
1767
|
// Resolve isolation mode
|
|
1080
1768
|
const isolationMode = readIsolationMode();
|
|
@@ -1175,6 +1863,10 @@ export default function(pi: ExtensionAPI) {
|
|
|
1175
1863
|
signal,
|
|
1176
1864
|
chainUpdate,
|
|
1177
1865
|
makeDetails("chain"),
|
|
1866
|
+
invokingSessionFile,
|
|
1867
|
+
false,
|
|
1868
|
+
undefined,
|
|
1869
|
+
undefined,
|
|
1178
1870
|
);
|
|
1179
1871
|
results.push(result);
|
|
1180
1872
|
|
|
@@ -1263,6 +1955,9 @@ export default function(pi: ExtensionAPI) {
|
|
|
1263
1955
|
}
|
|
1264
1956
|
},
|
|
1265
1957
|
makeDetails("parallel"),
|
|
1958
|
+
invokingSessionFile,
|
|
1959
|
+
false,
|
|
1960
|
+
undefined,
|
|
1266
1961
|
);
|
|
1267
1962
|
let result = await runTask();
|
|
1268
1963
|
|
|
@@ -1337,8 +2032,10 @@ export default function(pi: ExtensionAPI) {
|
|
|
1337
2032
|
params.task,
|
|
1338
2033
|
params.cwd,
|
|
1339
2034
|
params.model,
|
|
1340
|
-
{ defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined },
|
|
2035
|
+
{ defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, parentSessionFile: invokingSessionFile },
|
|
1341
2036
|
async (bgSignal) => {
|
|
2037
|
+
let liveSessionFile: string | undefined;
|
|
2038
|
+
let liveRuntime: LiveSubagentRuntime | undefined;
|
|
1342
2039
|
const result = await runSingleAgent(
|
|
1343
2040
|
ctx.cwd,
|
|
1344
2041
|
agents,
|
|
@@ -1351,12 +2048,63 @@ export default function(pi: ExtensionAPI) {
|
|
|
1351
2048
|
bgSignal,
|
|
1352
2049
|
undefined, // no streaming updates for background jobs
|
|
1353
2050
|
makeDetails("single"),
|
|
2051
|
+
invokingSessionFile,
|
|
2052
|
+
true,
|
|
2053
|
+
(info) => {
|
|
2054
|
+
if (!invokingSessionFile || !info.sessionFile) return;
|
|
2055
|
+
upsertAgentSessionLink(
|
|
2056
|
+
params.agent!,
|
|
2057
|
+
params.task!,
|
|
2058
|
+
invokingSessionFile,
|
|
2059
|
+
info.sessionFile,
|
|
2060
|
+
"running",
|
|
2061
|
+
);
|
|
2062
|
+
liveSessionFile = info.sessionFile;
|
|
2063
|
+
if (liveRuntime) {
|
|
2064
|
+
liveRuntime.sessionFile = info.sessionFile;
|
|
2065
|
+
liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
|
|
2066
|
+
liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
|
|
2067
|
+
}
|
|
2068
|
+
},
|
|
2069
|
+
(event, partial) => {
|
|
2070
|
+
const sessionFile = partial.sessionFile;
|
|
2071
|
+
if (!sessionFile || activeSessionFileForUi !== sessionFile) return;
|
|
2072
|
+
if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
2073
|
+
const delta = String(event.assistantMessageEvent.delta ?? "");
|
|
2074
|
+
if (delta) pushLiveStreamDelta(sessionFile, delta);
|
|
2075
|
+
}
|
|
2076
|
+
if (event?.type === "message_end") {
|
|
2077
|
+
flushLiveStream(sessionFile);
|
|
2078
|
+
}
|
|
2079
|
+
},
|
|
2080
|
+
{
|
|
2081
|
+
onStart: (control) => {
|
|
2082
|
+
if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy) return;
|
|
2083
|
+
liveRuntime = {
|
|
2084
|
+
sessionFile: liveSessionFile,
|
|
2085
|
+
parentSessionFile: invokingSessionFile,
|
|
2086
|
+
agentName: params.agent!,
|
|
2087
|
+
isBusy: control.isBusy,
|
|
2088
|
+
sendPrompt: control.sendPrompt,
|
|
2089
|
+
sendSteer: control.sendSteer,
|
|
2090
|
+
sendFollowUp: control.sendFollowUp,
|
|
2091
|
+
};
|
|
2092
|
+
if (liveSessionFile) {
|
|
2093
|
+
liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
|
|
2094
|
+
}
|
|
2095
|
+
},
|
|
2096
|
+
onFinish: () => {
|
|
2097
|
+
if (liveSessionFile) liveRuntimeBySessionFile.delete(liveSessionFile);
|
|
2098
|
+
},
|
|
2099
|
+
},
|
|
1354
2100
|
);
|
|
1355
2101
|
return {
|
|
1356
2102
|
exitCode: result.exitCode,
|
|
1357
2103
|
finalOutput: getFinalOutput(result.messages),
|
|
1358
2104
|
stderr: result.stderr,
|
|
1359
2105
|
model: result.model,
|
|
2106
|
+
sessionFile: result.sessionFile,
|
|
2107
|
+
parentSessionFile: result.parentSessionFile,
|
|
1360
2108
|
};
|
|
1361
2109
|
},
|
|
1362
2110
|
);
|
|
@@ -1387,6 +2135,8 @@ export default function(pi: ExtensionAPI) {
|
|
|
1387
2135
|
isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
|
|
1388
2136
|
}
|
|
1389
2137
|
|
|
2138
|
+
let liveSessionFile: string | undefined;
|
|
2139
|
+
let liveRuntime: LiveSubagentRuntime | undefined;
|
|
1390
2140
|
const result = await runSingleAgent(
|
|
1391
2141
|
ctx.cwd,
|
|
1392
2142
|
agents,
|
|
@@ -1399,20 +2149,83 @@ export default function(pi: ExtensionAPI) {
|
|
|
1399
2149
|
signal,
|
|
1400
2150
|
onUpdate,
|
|
1401
2151
|
makeDetails("single"),
|
|
2152
|
+
invokingSessionFile,
|
|
2153
|
+
!isolation,
|
|
2154
|
+
!isolation
|
|
2155
|
+
? (info) => {
|
|
2156
|
+
if (!invokingSessionFile || !info.sessionFile) return;
|
|
2157
|
+
upsertAgentSessionLink(
|
|
2158
|
+
params.agent!,
|
|
2159
|
+
params.task!,
|
|
2160
|
+
invokingSessionFile,
|
|
2161
|
+
info.sessionFile,
|
|
2162
|
+
"running",
|
|
2163
|
+
);
|
|
2164
|
+
liveSessionFile = info.sessionFile;
|
|
2165
|
+
if (liveRuntime) {
|
|
2166
|
+
liveRuntime.sessionFile = info.sessionFile;
|
|
2167
|
+
liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
|
|
2168
|
+
liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
: undefined,
|
|
2172
|
+
!isolation
|
|
2173
|
+
? (event, partial) => {
|
|
2174
|
+
const sessionFile = partial.sessionFile;
|
|
2175
|
+
if (!sessionFile || activeSessionFileForUi !== sessionFile) return;
|
|
2176
|
+
if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
2177
|
+
const delta = String(event.assistantMessageEvent.delta ?? "");
|
|
2178
|
+
if (delta) pushLiveStreamDelta(sessionFile, delta);
|
|
2179
|
+
}
|
|
2180
|
+
if (event?.type === "message_end") {
|
|
2181
|
+
flushLiveStream(sessionFile);
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
: undefined,
|
|
1402
2185
|
!isolation
|
|
1403
2186
|
? {
|
|
1404
2187
|
onStart: (control) => {
|
|
1405
2188
|
activeForegroundSubagent = { ...control, claimed: false };
|
|
1406
2189
|
ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
|
|
2190
|
+
|
|
2191
|
+
if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy) return;
|
|
2192
|
+
liveRuntime = {
|
|
2193
|
+
sessionFile: liveSessionFile,
|
|
2194
|
+
parentSessionFile: invokingSessionFile,
|
|
2195
|
+
agentName: params.agent!,
|
|
2196
|
+
isBusy: control.isBusy,
|
|
2197
|
+
sendPrompt: control.sendPrompt,
|
|
2198
|
+
sendSteer: control.sendSteer,
|
|
2199
|
+
sendFollowUp: control.sendFollowUp,
|
|
2200
|
+
};
|
|
2201
|
+
if (liveSessionFile && liveRuntime) {
|
|
2202
|
+
liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
|
|
2203
|
+
}
|
|
1407
2204
|
},
|
|
1408
2205
|
onFinish: () => {
|
|
1409
2206
|
activeForegroundSubagent = null;
|
|
1410
2207
|
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
2208
|
+
if (liveSessionFile) liveRuntimeBySessionFile.delete(liveSessionFile);
|
|
1411
2209
|
},
|
|
1412
2210
|
}
|
|
1413
2211
|
: undefined,
|
|
1414
2212
|
);
|
|
1415
2213
|
|
|
2214
|
+
if (result.sessionFile && invokingSessionFile) {
|
|
2215
|
+
const existingParent = parentSessionByChild.get(result.sessionFile);
|
|
2216
|
+
if (!existingParent) {
|
|
2217
|
+
registerAgentSessionLink({
|
|
2218
|
+
agentName: result.agent,
|
|
2219
|
+
task: result.task,
|
|
2220
|
+
parentSessionFile: invokingSessionFile,
|
|
2221
|
+
subagentSessionFile: result.sessionFile,
|
|
2222
|
+
state: result.exitCode === 0 ? "completed" : "failed",
|
|
2223
|
+
});
|
|
2224
|
+
} else {
|
|
2225
|
+
updateAgentSessionLinkState(result.sessionFile, result.exitCode === 0 ? "completed" : "failed");
|
|
2226
|
+
}
|
|
2227
|
+
}
|
|
2228
|
+
|
|
1416
2229
|
if (result.backgroundJobId) {
|
|
1417
2230
|
return {
|
|
1418
2231
|
content: [{ type: "text", text: `Moved ${result.agent} to background as **${result.backgroundJobId}**. Use \`await_subagent\`, \`/subagents wait ${result.backgroundJobId}\`, or \`/subagents output ${result.backgroundJobId}\`.` }],
|
|
@@ -1429,11 +2242,12 @@ export default function(pi: ExtensionAPI) {
|
|
|
1429
2242
|
}
|
|
1430
2243
|
|
|
1431
2244
|
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
2245
|
+
const agentSwitchHint = result.sessionFile ? "\n\nTip: run `/agent` to switch focus to this subagent session." : "";
|
|
1432
2246
|
if (isError) {
|
|
1433
2247
|
const errorMsg =
|
|
1434
2248
|
result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
1435
2249
|
return {
|
|
1436
|
-
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
2250
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}${agentSwitchHint}` }],
|
|
1437
2251
|
details: makeDetails("single")([result]),
|
|
1438
2252
|
isError: true,
|
|
1439
2253
|
};
|
|
@@ -1444,6 +2258,7 @@ export default function(pi: ExtensionAPI) {
|
|
|
1444
2258
|
if (mergeResult && !mergeResult.success) {
|
|
1445
2259
|
outputText += `\n\n⚠ Patch merge failed: ${mergeResult.error || "unknown error"}`;
|
|
1446
2260
|
}
|
|
2261
|
+
if (agentSwitchHint) outputText += agentSwitchHint;
|
|
1447
2262
|
return {
|
|
1448
2263
|
content: [{ type: "text", text: outputText }],
|
|
1449
2264
|
details: makeDetails("single")([result]),
|