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
|
@@ -32,11 +32,167 @@ import { loadEffectivePreferences } from "../shared/preferences.js";
|
|
|
32
32
|
import { CmuxClient } from "../cmux/index.js";
|
|
33
33
|
import { BackgroundJobManager } from "./background-job-manager.js";
|
|
34
34
|
import { runSubagentInBackground } from "./background-runner.js";
|
|
35
|
+
import { showAgentSwitcher } from "./agent-switcher-component.js";
|
|
36
|
+
import { buildAgentSwitchTargets, } from "./agent-switcher-model.js";
|
|
35
37
|
const MAX_PARALLEL_TASKS = 8;
|
|
36
38
|
const MAX_CONCURRENCY = 4;
|
|
37
39
|
const COLLAPSED_ITEM_COUNT = 10;
|
|
38
40
|
const DEFAULT_AWAIT_SUBAGENT_TIMEOUT_SECONDS = 120;
|
|
39
41
|
const liveSubagentProcesses = new Set();
|
|
42
|
+
const agentSessionLinksById = new Map();
|
|
43
|
+
const agentSessionIdsByParent = new Map();
|
|
44
|
+
const parentSessionByChild = new Map();
|
|
45
|
+
const liveRuntimeBySessionFile = new Map();
|
|
46
|
+
let agentSessionLinkCounter = 0;
|
|
47
|
+
function listSessionFiles(sessionDir) {
|
|
48
|
+
if (!fs.existsSync(sessionDir))
|
|
49
|
+
return [];
|
|
50
|
+
try {
|
|
51
|
+
return fs
|
|
52
|
+
.readdirSync(sessionDir)
|
|
53
|
+
.filter((name) => name.endsWith(".jsonl"))
|
|
54
|
+
.map((name) => path.join(sessionDir, name));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
return [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function detectNewSubagentSessionFile(sessionDir, before, startedAt) {
|
|
61
|
+
const after = listSessionFiles(sessionDir);
|
|
62
|
+
const created = after.filter((file) => !before.has(file));
|
|
63
|
+
const candidates = created.length > 0 ? created : after;
|
|
64
|
+
const ranked = candidates
|
|
65
|
+
.map((file) => {
|
|
66
|
+
let mtime = 0;
|
|
67
|
+
try {
|
|
68
|
+
mtime = fs.statSync(file).mtimeMs;
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
mtime = 0;
|
|
72
|
+
}
|
|
73
|
+
return { file, mtime };
|
|
74
|
+
})
|
|
75
|
+
.filter((entry) => entry.mtime >= startedAt - 5000)
|
|
76
|
+
.sort((a, b) => b.mtime - a.mtime);
|
|
77
|
+
return ranked[0]?.file;
|
|
78
|
+
}
|
|
79
|
+
function registerAgentSessionLink(link) {
|
|
80
|
+
const now = Date.now();
|
|
81
|
+
const id = `agent-${++agentSessionLinkCounter}`;
|
|
82
|
+
const full = { ...link, id, createdAt: now, updatedAt: now };
|
|
83
|
+
agentSessionLinksById.set(id, full);
|
|
84
|
+
const list = agentSessionIdsByParent.get(link.parentSessionFile) ?? [];
|
|
85
|
+
list.push(id);
|
|
86
|
+
agentSessionIdsByParent.set(link.parentSessionFile, list);
|
|
87
|
+
parentSessionByChild.set(link.subagentSessionFile, link.parentSessionFile);
|
|
88
|
+
return full;
|
|
89
|
+
}
|
|
90
|
+
function updateAgentSessionLinkState(subagentSessionFile, state) {
|
|
91
|
+
for (const link of agentSessionLinksById.values()) {
|
|
92
|
+
if (link.subagentSessionFile === subagentSessionFile) {
|
|
93
|
+
link.state = state;
|
|
94
|
+
link.updatedAt = Date.now();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function upsertAgentSessionLink(agentName, task, parentSessionFile, subagentSessionFile, state) {
|
|
100
|
+
const existingParent = parentSessionByChild.get(subagentSessionFile);
|
|
101
|
+
if (!existingParent) {
|
|
102
|
+
registerAgentSessionLink({
|
|
103
|
+
agentName,
|
|
104
|
+
task,
|
|
105
|
+
parentSessionFile,
|
|
106
|
+
subagentSessionFile,
|
|
107
|
+
state,
|
|
108
|
+
});
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
updateAgentSessionLinkState(subagentSessionFile, state);
|
|
112
|
+
}
|
|
113
|
+
function getAgentSessionLinksForParent(parentSessionFile) {
|
|
114
|
+
const ids = agentSessionIdsByParent.get(parentSessionFile) ?? [];
|
|
115
|
+
return ids
|
|
116
|
+
.map((id) => agentSessionLinksById.get(id))
|
|
117
|
+
.filter((entry) => Boolean(entry))
|
|
118
|
+
.sort((a, b) => b.updatedAt - a.updatedAt);
|
|
119
|
+
}
|
|
120
|
+
function readSessionHeader(sessionFile) {
|
|
121
|
+
try {
|
|
122
|
+
const content = fs.readFileSync(sessionFile, "utf-8");
|
|
123
|
+
const firstLine = content.split("\n").find((line) => line.trim().length > 0);
|
|
124
|
+
if (!firstLine)
|
|
125
|
+
return null;
|
|
126
|
+
const parsed = JSON.parse(firstLine);
|
|
127
|
+
if (!parsed || parsed.type !== "session")
|
|
128
|
+
return null;
|
|
129
|
+
return {
|
|
130
|
+
parentSession: typeof parsed.parentSession === "string" ? parsed.parentSession : undefined,
|
|
131
|
+
subagentName: typeof parsed.subagentName === "string" ? parsed.subagentName : undefined,
|
|
132
|
+
subagentTask: typeof parsed.subagentTask === "string" ? parsed.subagentTask : undefined,
|
|
133
|
+
subagentSystemPrompt: typeof parsed.subagentSystemPrompt === "string" ? parsed.subagentSystemPrompt : undefined,
|
|
134
|
+
subagentTools: Array.isArray(parsed.subagentTools)
|
|
135
|
+
? parsed.subagentTools.filter((tool) => typeof tool === "string")
|
|
136
|
+
: undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function backfillAgentSessionLinksForParent(parentSessionFile, sessionDir) {
|
|
144
|
+
for (const sessionFile of listSessionFiles(sessionDir)) {
|
|
145
|
+
if (sessionFile === parentSessionFile)
|
|
146
|
+
continue;
|
|
147
|
+
const header = readSessionHeader(sessionFile);
|
|
148
|
+
if (header?.parentSession !== parentSessionFile)
|
|
149
|
+
continue;
|
|
150
|
+
const existingParent = parentSessionByChild.get(sessionFile);
|
|
151
|
+
if (!existingParent) {
|
|
152
|
+
registerAgentSessionLink({
|
|
153
|
+
agentName: header.subagentName ?? "subagent",
|
|
154
|
+
task: header.subagentTask ?? "Recovered from persisted session lineage",
|
|
155
|
+
parentSessionFile,
|
|
156
|
+
subagentSessionFile: sessionFile,
|
|
157
|
+
state: "completed",
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return getAgentSessionLinksForParent(parentSessionFile);
|
|
162
|
+
}
|
|
163
|
+
function formatSwitchTargetSummary(target) {
|
|
164
|
+
const current = target.isCurrent ? " (current)" : "";
|
|
165
|
+
if (target.kind === "parent") {
|
|
166
|
+
return `● parent — main session${current}`;
|
|
167
|
+
}
|
|
168
|
+
const icon = target.state === "running" ? "▶" : target.state === "failed" ? "✗" : "✓";
|
|
169
|
+
return `${icon} ${target.agentName} — ${target.taskPreview}${current}`;
|
|
170
|
+
}
|
|
171
|
+
function buildSwitchTargetsForParent(parentSessionFile, currentSessionFile, currentCwd, trackedLinks, runningJobs) {
|
|
172
|
+
return buildAgentSwitchTargets({
|
|
173
|
+
currentSessionFile,
|
|
174
|
+
rootParentSessionFile: parentSessionFile,
|
|
175
|
+
currentCwd,
|
|
176
|
+
trackedLinks: trackedLinks.map((link) => ({
|
|
177
|
+
id: link.id,
|
|
178
|
+
agentName: link.agentName,
|
|
179
|
+
task: link.task,
|
|
180
|
+
parentSessionFile: link.parentSessionFile,
|
|
181
|
+
subagentSessionFile: link.subagentSessionFile,
|
|
182
|
+
updatedAt: link.updatedAt,
|
|
183
|
+
state: link.state,
|
|
184
|
+
})),
|
|
185
|
+
runningJobs: runningJobs.map((job) => ({
|
|
186
|
+
id: job.id,
|
|
187
|
+
agentName: job.agentName,
|
|
188
|
+
task: job.task,
|
|
189
|
+
startedAt: job.startedAt,
|
|
190
|
+
parentSessionFile: job.parentSessionFile,
|
|
191
|
+
sessionFile: job.sessionFile,
|
|
192
|
+
cwd: job.cwd,
|
|
193
|
+
})),
|
|
194
|
+
});
|
|
195
|
+
}
|
|
40
196
|
const AwaitSubagentParams = Type.Object({
|
|
41
197
|
jobs: Type.Optional(Type.Array(Type.String(), {
|
|
42
198
|
description: "Subagent job IDs to wait for. Omit to wait for the next running background subagent.",
|
|
@@ -340,7 +496,7 @@ function resolveSubagentCliPath(defaultCwd) {
|
|
|
340
496
|
}
|
|
341
497
|
return null;
|
|
342
498
|
}
|
|
343
|
-
function processSubagentEventLine(line, currentResult, emitUpdate, proc) {
|
|
499
|
+
function processSubagentEventLine(line, currentResult, emitUpdate, proc, onSessionInfo, onEventType, onParsedEvent) {
|
|
344
500
|
if (!line.trim())
|
|
345
501
|
return false;
|
|
346
502
|
let event;
|
|
@@ -350,6 +506,29 @@ function processSubagentEventLine(line, currentResult, emitUpdate, proc) {
|
|
|
350
506
|
catch {
|
|
351
507
|
return false;
|
|
352
508
|
}
|
|
509
|
+
const eventType = typeof event.type === "string" ? event.type : "unknown";
|
|
510
|
+
onEventType?.(eventType);
|
|
511
|
+
onParsedEvent?.(event);
|
|
512
|
+
if (event.type === "subagent_session_info") {
|
|
513
|
+
let changed = false;
|
|
514
|
+
if (typeof event.sessionFile === "string" && event.sessionFile) {
|
|
515
|
+
if (currentResult.sessionFile !== event.sessionFile)
|
|
516
|
+
changed = true;
|
|
517
|
+
currentResult.sessionFile = event.sessionFile;
|
|
518
|
+
}
|
|
519
|
+
if (typeof event.parentSessionFile === "string" && event.parentSessionFile) {
|
|
520
|
+
if (currentResult.parentSessionFile !== event.parentSessionFile)
|
|
521
|
+
changed = true;
|
|
522
|
+
currentResult.parentSessionFile = event.parentSessionFile;
|
|
523
|
+
}
|
|
524
|
+
if (changed) {
|
|
525
|
+
onSessionInfo?.({
|
|
526
|
+
sessionFile: currentResult.sessionFile,
|
|
527
|
+
parentSessionFile: currentResult.parentSessionFile,
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
return false;
|
|
531
|
+
}
|
|
353
532
|
if (proc && isSubagentPermissionRequest(event)) {
|
|
354
533
|
void handleSubagentPermissionRequest(event, proc);
|
|
355
534
|
return false;
|
|
@@ -394,7 +573,7 @@ async function waitForFile(filePath, signal, timeoutMs = 30 * 60 * 1000) {
|
|
|
394
573
|
}
|
|
395
574
|
return false;
|
|
396
575
|
}
|
|
397
|
-
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, foregroundHooks) {
|
|
576
|
+
async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, modelOverride, parentModel, signal, onUpdate, makeDetails, parentSessionFile, attachableSession, onSessionInfo, onSubagentEvent, foregroundHooks) {
|
|
398
577
|
const agent = agents.find((a) => a.name === agentName);
|
|
399
578
|
if (!agent) {
|
|
400
579
|
const available = agents.map((a) => `"${a.name}"`).join(", ") || "none";
|
|
@@ -425,6 +604,7 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
425
604
|
usage: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, cost: 0, contextTokens: 0, turns: 0 },
|
|
426
605
|
model: inferredModel,
|
|
427
606
|
step,
|
|
607
|
+
parentSessionFile,
|
|
428
608
|
};
|
|
429
609
|
const emitUpdate = () => {
|
|
430
610
|
if (onUpdate) {
|
|
@@ -462,17 +642,27 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
462
642
|
tmpPromptDir = tmp.dir;
|
|
463
643
|
tmpPromptPath = tmp.filePath;
|
|
464
644
|
}
|
|
465
|
-
const
|
|
645
|
+
const effectiveCwd = cwd ?? defaultCwd;
|
|
646
|
+
const subagentSessionDir = parentSessionFile ? path.dirname(parentSessionFile) : undefined;
|
|
647
|
+
const sessionFilesBefore = attachableSession && subagentSessionDir
|
|
648
|
+
? new Set(listSessionFiles(subagentSessionDir))
|
|
649
|
+
: undefined;
|
|
650
|
+
const launchStartedAt = Date.now();
|
|
651
|
+
const args = buildSubagentProcessArgs(agent, task, tmpPromptPath, inferredModel, {
|
|
652
|
+
noSession: !attachableSession,
|
|
653
|
+
parentSessionFile: parentSessionFile,
|
|
654
|
+
mode: attachableSession ? "rpc" : "json",
|
|
655
|
+
});
|
|
466
656
|
const exitCode = await new Promise((resolve) => {
|
|
467
657
|
const bundledPaths = getBundledExtensionPathsFromEnv();
|
|
468
658
|
const extensionArgs = bundledPaths.flatMap((p) => ["--extension", p]);
|
|
469
|
-
const cliPath = resolveSubagentCliPath(
|
|
659
|
+
const cliPath = resolveSubagentCliPath(effectiveCwd);
|
|
470
660
|
if (!cliPath) {
|
|
471
661
|
currentResult.stderr += "Unable to resolve LSD/GSD CLI path for subagent launch.";
|
|
472
662
|
resolve(1);
|
|
473
663
|
return;
|
|
474
664
|
}
|
|
475
|
-
const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd:
|
|
665
|
+
const proc = spawn(process.execPath, [cliPath, ...extensionArgs, ...args], { cwd: effectiveCwd, shell: false, stdio: ["pipe", "pipe", "pipe"] });
|
|
476
666
|
// Keep stdin open so approval/classifier responses can be proxied back
|
|
477
667
|
// into the child process. Closing it here can leave the subagent stuck
|
|
478
668
|
// after its first turn when it requests permission for a tool call.
|
|
@@ -481,6 +671,9 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
481
671
|
let completionSeen = false;
|
|
482
672
|
let resolved = false;
|
|
483
673
|
let foregroundReleased = false;
|
|
674
|
+
let isBusy = false;
|
|
675
|
+
let commandSeq = 0;
|
|
676
|
+
const pendingCommandResponses = new Map();
|
|
484
677
|
const procAbortController = new AbortController();
|
|
485
678
|
let resolveBackgroundResult;
|
|
486
679
|
let rejectBackgroundResult;
|
|
@@ -488,6 +681,15 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
488
681
|
resolveBackgroundResult = resolveBg;
|
|
489
682
|
rejectBackgroundResult = rejectBg;
|
|
490
683
|
});
|
|
684
|
+
const sendRpcCommand = async (command) => {
|
|
685
|
+
const id = `sa_cmd_${++commandSeq}`;
|
|
686
|
+
if (!proc.stdin)
|
|
687
|
+
throw new Error("Subagent RPC stdin is not available.");
|
|
688
|
+
return new Promise((resolveCmd, rejectCmd) => {
|
|
689
|
+
pendingCommandResponses.set(id, { resolve: resolveCmd, reject: rejectCmd });
|
|
690
|
+
proc.stdin.write(JSON.stringify({ id, ...command }) + "\n");
|
|
691
|
+
});
|
|
692
|
+
};
|
|
491
693
|
const finishForeground = (code) => {
|
|
492
694
|
if (resolved)
|
|
493
695
|
return;
|
|
@@ -511,16 +713,60 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
511
713
|
agentName,
|
|
512
714
|
task,
|
|
513
715
|
cwd: cwd ?? defaultCwd,
|
|
716
|
+
parentSessionFile,
|
|
514
717
|
abortController: procAbortController,
|
|
515
718
|
resultPromise: backgroundResultPromise,
|
|
516
719
|
adoptToBackground,
|
|
720
|
+
sendPrompt: attachableSession
|
|
721
|
+
? async (text, images) => {
|
|
722
|
+
await sendRpcCommand({ type: "prompt", message: text, images });
|
|
723
|
+
}
|
|
724
|
+
: undefined,
|
|
725
|
+
sendSteer: attachableSession
|
|
726
|
+
? async (text, images) => {
|
|
727
|
+
await sendRpcCommand({ type: "steer", message: text, images });
|
|
728
|
+
}
|
|
729
|
+
: undefined,
|
|
730
|
+
sendFollowUp: attachableSession
|
|
731
|
+
? async (text, images) => {
|
|
732
|
+
await sendRpcCommand({ type: "follow_up", message: text, images });
|
|
733
|
+
}
|
|
734
|
+
: undefined,
|
|
735
|
+
isBusy: attachableSession ? () => isBusy : undefined,
|
|
517
736
|
});
|
|
518
737
|
proc.stdout.on("data", (data) => {
|
|
519
738
|
buffer += data.toString();
|
|
520
739
|
const lines = buffer.split("\n");
|
|
521
740
|
buffer = lines.pop() || "";
|
|
522
741
|
for (const line of lines) {
|
|
523
|
-
|
|
742
|
+
const trimmed = line.trim();
|
|
743
|
+
if (!trimmed)
|
|
744
|
+
continue;
|
|
745
|
+
if (attachableSession) {
|
|
746
|
+
try {
|
|
747
|
+
const parsed = JSON.parse(trimmed);
|
|
748
|
+
if (parsed?.type === "response" && typeof parsed.id === "string" && pendingCommandResponses.has(parsed.id)) {
|
|
749
|
+
const pending = pendingCommandResponses.get(parsed.id);
|
|
750
|
+
pendingCommandResponses.delete(parsed.id);
|
|
751
|
+
if (parsed.success === false) {
|
|
752
|
+
pending.reject(new Error(typeof parsed.error === "string" ? parsed.error : "Subagent RPC command failed."));
|
|
753
|
+
}
|
|
754
|
+
else {
|
|
755
|
+
pending.resolve(parsed.data);
|
|
756
|
+
}
|
|
757
|
+
continue;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
catch {
|
|
761
|
+
// Fall through to generic event processing.
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (processSubagentEventLine(trimmed, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
|
|
765
|
+
if (eventType === "agent_start")
|
|
766
|
+
isBusy = true;
|
|
767
|
+
if (eventType === "agent_end")
|
|
768
|
+
isBusy = false;
|
|
769
|
+
}, (event) => onSubagentEvent?.(event, currentResult))) {
|
|
524
770
|
completionSeen = true;
|
|
525
771
|
try {
|
|
526
772
|
proc.kill("SIGTERM");
|
|
@@ -537,27 +783,65 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
537
783
|
proc.on("close", (code) => {
|
|
538
784
|
liveSubagentProcesses.delete(proc);
|
|
539
785
|
if (buffer.trim()) {
|
|
540
|
-
const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc)
|
|
786
|
+
const completedOnFlush = processSubagentEventLine(buffer, currentResult, emitUpdate, proc, onSessionInfo, (eventType) => {
|
|
787
|
+
if (eventType === "agent_start")
|
|
788
|
+
isBusy = true;
|
|
789
|
+
if (eventType === "agent_end")
|
|
790
|
+
isBusy = false;
|
|
791
|
+
}, (event) => onSubagentEvent?.(event, currentResult));
|
|
541
792
|
completionSeen = completionSeen || completedOnFlush;
|
|
542
793
|
}
|
|
794
|
+
isBusy = false;
|
|
795
|
+
for (const pending of pendingCommandResponses.values()) {
|
|
796
|
+
pending.reject(new Error("Subagent process closed before command response."));
|
|
797
|
+
}
|
|
798
|
+
pendingCommandResponses.clear();
|
|
543
799
|
const finalExitCode = completionSeen && (code === null || code === 143 || code === 15) ? 0 : (code ?? 0);
|
|
544
800
|
currentResult.exitCode = finalExitCode;
|
|
801
|
+
if (attachableSession && sessionFilesBefore && subagentSessionDir && !currentResult.sessionFile) {
|
|
802
|
+
const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
|
|
803
|
+
if (detected)
|
|
804
|
+
currentResult.sessionFile = detected;
|
|
805
|
+
}
|
|
545
806
|
resolveBackgroundResult?.({
|
|
546
807
|
summary: getFinalOutput(currentResult.messages),
|
|
547
808
|
stderr: currentResult.stderr,
|
|
548
809
|
exitCode: finalExitCode,
|
|
549
810
|
model: currentResult.model,
|
|
811
|
+
sessionFile: currentResult.sessionFile,
|
|
812
|
+
parentSessionFile: currentResult.parentSessionFile,
|
|
550
813
|
});
|
|
551
814
|
foregroundHooks?.onFinish?.();
|
|
552
815
|
finishForeground(finalExitCode);
|
|
553
816
|
});
|
|
554
817
|
proc.on("error", (error) => {
|
|
555
818
|
liveSubagentProcesses.delete(proc);
|
|
819
|
+
isBusy = false;
|
|
820
|
+
for (const pending of pendingCommandResponses.values()) {
|
|
821
|
+
pending.reject(error instanceof Error ? error : new Error(String(error)));
|
|
822
|
+
}
|
|
823
|
+
pendingCommandResponses.clear();
|
|
556
824
|
rejectBackgroundResult?.(error);
|
|
557
825
|
foregroundHooks?.onFinish?.();
|
|
558
826
|
finishForeground(1);
|
|
559
827
|
});
|
|
828
|
+
if (attachableSession) {
|
|
829
|
+
void sendRpcCommand({ type: "prompt", message: task }).catch((error) => {
|
|
830
|
+
currentResult.stderr += error instanceof Error ? error.message : String(error);
|
|
831
|
+
try {
|
|
832
|
+
proc.kill("SIGTERM");
|
|
833
|
+
}
|
|
834
|
+
catch {
|
|
835
|
+
/* ignore */
|
|
836
|
+
}
|
|
837
|
+
});
|
|
838
|
+
}
|
|
560
839
|
const killProc = () => {
|
|
840
|
+
// If the process has been adopted to the background (e.g. via Ctrl+B or
|
|
841
|
+
// /agent attach_live), foregroundReleased is true and the process should
|
|
842
|
+
// survive the parent session's abort — don't kill it.
|
|
843
|
+
if (foregroundReleased)
|
|
844
|
+
return;
|
|
561
845
|
wasAborted = true;
|
|
562
846
|
procAbortController.abort();
|
|
563
847
|
proc.kill("SIGTERM");
|
|
@@ -586,6 +870,12 @@ async function runSingleAgent(defaultCwd, agents, agentName, task, cwd, step, mo
|
|
|
586
870
|
}
|
|
587
871
|
});
|
|
588
872
|
currentResult.exitCode = exitCode;
|
|
873
|
+
if (attachableSession && sessionFilesBefore && subagentSessionDir) {
|
|
874
|
+
const detected = detectNewSubagentSessionFile(subagentSessionDir, sessionFilesBefore, launchStartedAt);
|
|
875
|
+
if (detected) {
|
|
876
|
+
currentResult.sessionFile = detected;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
589
879
|
if (wasAborted)
|
|
590
880
|
throw new Error("Subagent was aborted");
|
|
591
881
|
return currentResult;
|
|
@@ -646,14 +936,62 @@ export default function (pi) {
|
|
|
646
936
|
const foregroundSubagentStatusKey = "foreground-subagent";
|
|
647
937
|
const foregroundSubagentHint = "Ctrl+B: move foreground subagent to background";
|
|
648
938
|
let activeForegroundSubagent = null;
|
|
939
|
+
let activeSessionFileForUi;
|
|
940
|
+
const liveStreamBufferBySession = new Map();
|
|
941
|
+
function flushLiveStream(sessionFile) {
|
|
942
|
+
const buffered = liveStreamBufferBySession.get(sessionFile);
|
|
943
|
+
if (!buffered || !buffered.trim())
|
|
944
|
+
return;
|
|
945
|
+
liveStreamBufferBySession.set(sessionFile, "");
|
|
946
|
+
pi.sendMessage({
|
|
947
|
+
customType: "live_subagent_stream",
|
|
948
|
+
content: buffered,
|
|
949
|
+
display: true,
|
|
950
|
+
}, { deliverAs: "followUp" });
|
|
951
|
+
}
|
|
952
|
+
function pushLiveStreamDelta(sessionFile, delta) {
|
|
953
|
+
const prev = liveStreamBufferBySession.get(sessionFile) ?? "";
|
|
954
|
+
const next = prev + delta;
|
|
955
|
+
liveStreamBufferBySession.set(sessionFile, next);
|
|
956
|
+
if (next.length >= 120 || next.includes("\n")) {
|
|
957
|
+
flushLiveStream(sessionFile);
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
function getCurrentSessionSubagentMetadata(sessionFile) {
|
|
961
|
+
if (!sessionFile)
|
|
962
|
+
return null;
|
|
963
|
+
return readSessionHeader(sessionFile);
|
|
964
|
+
}
|
|
965
|
+
function applyCurrentSessionSubagentTools(ctx) {
|
|
966
|
+
const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
|
|
967
|
+
if (metadata?.subagentTools && metadata.subagentTools.length > 0) {
|
|
968
|
+
ctx.setActiveTools(metadata.subagentTools);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
649
971
|
function getBgManager() {
|
|
650
972
|
if (!bgManager)
|
|
651
973
|
throw new Error("BackgroundJobManager not initialized.");
|
|
652
974
|
return bgManager;
|
|
653
975
|
}
|
|
654
|
-
pi.on("session_start", async () => {
|
|
976
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
977
|
+
activeSessionFileForUi = ctx.sessionManager.getSessionFile();
|
|
655
978
|
bgManager = new BackgroundJobManager({
|
|
656
979
|
onJobComplete: (job) => {
|
|
980
|
+
if (job.sessionFile && job.parentSessionFile) {
|
|
981
|
+
const existingParent = parentSessionByChild.get(job.sessionFile);
|
|
982
|
+
if (!existingParent) {
|
|
983
|
+
registerAgentSessionLink({
|
|
984
|
+
agentName: job.agentName,
|
|
985
|
+
task: job.task,
|
|
986
|
+
parentSessionFile: job.parentSessionFile,
|
|
987
|
+
subagentSessionFile: job.sessionFile,
|
|
988
|
+
state: job.status === "failed" ? "failed" : "completed",
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
else {
|
|
992
|
+
updateAgentSessionLinkState(job.sessionFile, job.status === "failed" ? "failed" : "completed");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
657
995
|
if (job.awaited)
|
|
658
996
|
return;
|
|
659
997
|
const statusEmoji = job.status === "completed" ? "✓" : job.status === "cancelled" ? "✗ cancelled" : "✗ failed";
|
|
@@ -676,22 +1014,80 @@ export default function (pi) {
|
|
|
676
1014
|
}, { deliverAs: "followUp" });
|
|
677
1015
|
},
|
|
678
1016
|
});
|
|
1017
|
+
applyCurrentSessionSubagentTools(ctx);
|
|
679
1018
|
});
|
|
680
|
-
pi.on("
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
1019
|
+
pi.on("session_switch", async (_event, ctx) => {
|
|
1020
|
+
activeSessionFileForUi = ctx.sessionManager.getSessionFile();
|
|
1021
|
+
applyCurrentSessionSubagentTools(ctx);
|
|
1022
|
+
});
|
|
1023
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
1024
|
+
const metadata = getCurrentSessionSubagentMetadata(ctx.sessionManager.getSessionFile());
|
|
1025
|
+
if (!metadata?.subagentSystemPrompt)
|
|
1026
|
+
return;
|
|
1027
|
+
const subagentName = metadata.subagentName ?? "subagent";
|
|
1028
|
+
const taskNote = metadata.subagentTask
|
|
1029
|
+
? `Original delegated task: ${metadata.subagentTask}`
|
|
1030
|
+
: "Continue operating as the delegated subagent for this session.";
|
|
1031
|
+
const antiRecursion = [
|
|
1032
|
+
`You are already the ${subagentName} subagent for this session.`,
|
|
1033
|
+
"Do not spawn or delegate to another subagent with the same name as yourself.",
|
|
1034
|
+
`If the user asks you to continue ${subagentName} work, do that work directly in this session.`,
|
|
1035
|
+
taskNote,
|
|
1036
|
+
"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.",
|
|
1037
|
+
].join("\n");
|
|
1038
|
+
return {
|
|
1039
|
+
systemPrompt: `${event.systemPrompt}\n\n${antiRecursion}\n\n${metadata.subagentSystemPrompt}`,
|
|
1040
|
+
};
|
|
1041
|
+
});
|
|
1042
|
+
pi.on("input", async (event, ctx) => {
|
|
1043
|
+
const sessionFile = ctx.sessionManager.getSessionFile();
|
|
1044
|
+
if (!sessionFile)
|
|
1045
|
+
return;
|
|
1046
|
+
const runtime = liveRuntimeBySessionFile.get(sessionFile);
|
|
1047
|
+
if (!runtime)
|
|
1048
|
+
return;
|
|
1049
|
+
const text = event.text?.trim();
|
|
1050
|
+
if (!text)
|
|
1051
|
+
return { action: "handled" };
|
|
1052
|
+
const isSlashCommand = text.startsWith("/");
|
|
1053
|
+
if (isSlashCommand)
|
|
1054
|
+
return;
|
|
1055
|
+
try {
|
|
1056
|
+
if (runtime.isBusy()) {
|
|
1057
|
+
await runtime.sendSteer(text, event.images);
|
|
1058
|
+
ctx.ui.notify(`Sent steer to running subagent ${runtime.agentName}.`, "info");
|
|
685
1059
|
}
|
|
1060
|
+
else {
|
|
1061
|
+
await runtime.sendPrompt(text, event.images);
|
|
1062
|
+
ctx.ui.notify(`Sent prompt to live subagent ${runtime.agentName}.`, "info");
|
|
1063
|
+
}
|
|
1064
|
+
return { action: "handled" };
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
ctx.ui.notify(`Failed to send input to live subagent ${runtime.agentName}: ${error instanceof Error ? error.message : String(error)}`, "error");
|
|
1068
|
+
return { action: "handled" };
|
|
686
1069
|
}
|
|
687
1070
|
});
|
|
1071
|
+
pi.on("session_before_switch", async () => {
|
|
1072
|
+
if (activeSessionFileForUi)
|
|
1073
|
+
flushLiveStream(activeSessionFileForUi);
|
|
1074
|
+
activeForegroundSubagent = null;
|
|
1075
|
+
});
|
|
688
1076
|
pi.on("session_shutdown", async () => {
|
|
1077
|
+
if (activeSessionFileForUi)
|
|
1078
|
+
flushLiveStream(activeSessionFileForUi);
|
|
1079
|
+
activeSessionFileForUi = undefined;
|
|
689
1080
|
activeForegroundSubagent = null;
|
|
690
1081
|
await stopLiveSubagents();
|
|
691
1082
|
if (bgManager) {
|
|
692
1083
|
bgManager.shutdown();
|
|
693
1084
|
bgManager = null;
|
|
694
1085
|
}
|
|
1086
|
+
agentSessionLinksById.clear();
|
|
1087
|
+
agentSessionIdsByParent.clear();
|
|
1088
|
+
parentSessionByChild.clear();
|
|
1089
|
+
liveRuntimeBySessionFile.clear();
|
|
1090
|
+
liveStreamBufferBySession.clear();
|
|
695
1091
|
});
|
|
696
1092
|
// /subagents command
|
|
697
1093
|
pi.registerCommand("subagents", {
|
|
@@ -795,6 +1191,191 @@ export default function (pi) {
|
|
|
795
1191
|
ctx.ui.notify(`Available agents (${discovery.agents.length}):\n${lines.join("\n")}`, "info");
|
|
796
1192
|
},
|
|
797
1193
|
});
|
|
1194
|
+
// /agent command - switch to the parent or a tracked subagent session
|
|
1195
|
+
pi.registerCommand("agent", {
|
|
1196
|
+
description: "Switch focus to parent/subagent sessions (/agent picker, /agent <id|index|name>, /agent parent)",
|
|
1197
|
+
handler: async (args, ctx) => {
|
|
1198
|
+
const currentSessionFile = ctx.sessionManager.getSessionFile();
|
|
1199
|
+
if (!currentSessionFile) {
|
|
1200
|
+
ctx.ui.notify("Current session is in-memory only; /agent requires a persisted session file.", "warning");
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1203
|
+
const arg = args.trim();
|
|
1204
|
+
const parentSessionFile = parentSessionByChild.get(currentSessionFile);
|
|
1205
|
+
const currentParent = parentSessionFile ?? currentSessionFile;
|
|
1206
|
+
const currentSessionDir = path.dirname(currentParent);
|
|
1207
|
+
let tracked = getAgentSessionLinksForParent(currentParent).filter((entry) => fs.existsSync(entry.subagentSessionFile));
|
|
1208
|
+
if (tracked.length === 0) {
|
|
1209
|
+
tracked = backfillAgentSessionLinksForParent(currentParent, currentSessionDir)
|
|
1210
|
+
.filter((entry) => fs.existsSync(entry.subagentSessionFile));
|
|
1211
|
+
}
|
|
1212
|
+
const runningJobs = bgManager?.getRunningJobs() ?? [];
|
|
1213
|
+
const switchTargets = buildSwitchTargetsForParent(currentParent, currentSessionFile, ctx.cwd, tracked, runningJobs);
|
|
1214
|
+
const applySwitchTarget = async (target) => {
|
|
1215
|
+
if (target.selectionAction === "blocked") {
|
|
1216
|
+
ctx.ui.notify(target.blockedReason ?? "That target cannot be selected yet.", "warning");
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
if (target.selectionAction === "attach_live") {
|
|
1220
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1221
|
+
ctx.ui.notify(`Live subagent session file is missing: ${target.sessionFile}`, "error");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
const liveRuntime = liveRuntimeBySessionFile.get(target.sessionFile);
|
|
1225
|
+
if (!liveRuntime) {
|
|
1226
|
+
ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
// Adopt the foreground subagent to background before switching sessions.
|
|
1230
|
+
// switchSession calls abort() which would fire the tool signal and SIGTERM
|
|
1231
|
+
// the running subagent process. Adopting to background detaches the process
|
|
1232
|
+
// from the foreground abort chain so it survives the session switch.
|
|
1233
|
+
const foreground = activeForegroundSubagent;
|
|
1234
|
+
if (foreground && !foreground.claimed && bgManager) {
|
|
1235
|
+
foreground.claimed = true;
|
|
1236
|
+
try {
|
|
1237
|
+
const jobId = bgManager.adoptRunning(foreground.agentName, foreground.task, foreground.cwd, foreground.abortController, foreground.resultPromise, {
|
|
1238
|
+
parentSessionFile: foreground.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
|
|
1239
|
+
});
|
|
1240
|
+
const released = foreground.adoptToBackground(jobId);
|
|
1241
|
+
if (!released) {
|
|
1242
|
+
foreground.claimed = false;
|
|
1243
|
+
bgManager.cancel(jobId);
|
|
1244
|
+
}
|
|
1245
|
+
else {
|
|
1246
|
+
activeForegroundSubagent = null;
|
|
1247
|
+
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
catch {
|
|
1251
|
+
foreground.claimed = false;
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1255
|
+
if (switched.cancelled) {
|
|
1256
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
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");
|
|
1260
|
+
return;
|
|
1261
|
+
}
|
|
1262
|
+
if (target.kind === "parent") {
|
|
1263
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1264
|
+
ctx.ui.notify(`Parent session file not found: ${target.sessionFile}`, "error");
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1268
|
+
if (switched.cancelled) {
|
|
1269
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1270
|
+
return;
|
|
1271
|
+
}
|
|
1272
|
+
ctx.ui.notify("Switched to parent session.", "info");
|
|
1273
|
+
return;
|
|
1274
|
+
}
|
|
1275
|
+
if (!fs.existsSync(target.sessionFile)) {
|
|
1276
|
+
ctx.ui.notify(`Subagent session file is missing: ${target.sessionFile}`, "error");
|
|
1277
|
+
return;
|
|
1278
|
+
}
|
|
1279
|
+
const switched = await ctx.switchSession(target.sessionFile);
|
|
1280
|
+
if (switched.cancelled) {
|
|
1281
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
updateAgentSessionLinkState(target.sessionFile, target.state === "failed" ? "failed" : "completed");
|
|
1285
|
+
ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
|
|
1286
|
+
};
|
|
1287
|
+
if (!arg) {
|
|
1288
|
+
const subagentTargets = switchTargets.filter((target) => target.kind === "subagent");
|
|
1289
|
+
if (ctx.hasUI) {
|
|
1290
|
+
if (subagentTargets.length === 0 && !parentSessionFile) {
|
|
1291
|
+
ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
|
|
1292
|
+
return;
|
|
1293
|
+
}
|
|
1294
|
+
const selected = await showAgentSwitcher(ctx, switchTargets);
|
|
1295
|
+
if (!selected)
|
|
1296
|
+
return;
|
|
1297
|
+
await applySwitchTarget(selected);
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (subagentTargets.length === 0 && !parentSessionFile) {
|
|
1301
|
+
ctx.ui.notify("No tracked subagent sessions for this parent session yet. Run a single-mode subagent first (foreground or background).", "info");
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
const lines = ["Agent switch targets:"];
|
|
1305
|
+
switchTargets.forEach((target, index) => {
|
|
1306
|
+
lines.push(`${index + 1}. ${formatSwitchTargetSummary(target)}`);
|
|
1307
|
+
});
|
|
1308
|
+
lines.push("", "Use `/agent <index|id|name>` for explicit targeting, or `/agent parent`.");
|
|
1309
|
+
ctx.ui.notify(lines.join("\n"), "info");
|
|
1310
|
+
return;
|
|
1311
|
+
}
|
|
1312
|
+
if (arg === "parent" || arg === "main") {
|
|
1313
|
+
if (!parentSessionFile) {
|
|
1314
|
+
ctx.ui.notify("You are already in the parent/main session.", "info");
|
|
1315
|
+
return;
|
|
1316
|
+
}
|
|
1317
|
+
if (!fs.existsSync(parentSessionFile)) {
|
|
1318
|
+
ctx.ui.notify(`Parent session file not found: ${parentSessionFile}`, "error");
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
const switched = await ctx.switchSession(parentSessionFile);
|
|
1322
|
+
if (switched.cancelled) {
|
|
1323
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
ctx.ui.notify("Switched to parent session.", "info");
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
let target;
|
|
1330
|
+
if (/^\d+$/.test(arg)) {
|
|
1331
|
+
const index = Number.parseInt(arg, 10) - 1;
|
|
1332
|
+
target = tracked[index];
|
|
1333
|
+
}
|
|
1334
|
+
if (!target) {
|
|
1335
|
+
target = tracked.find((entry) => entry.id === arg);
|
|
1336
|
+
}
|
|
1337
|
+
if (!target) {
|
|
1338
|
+
target = tracked.find((entry) => entry.agentName === arg);
|
|
1339
|
+
}
|
|
1340
|
+
if (!target) {
|
|
1341
|
+
target = tracked.find((entry) => path.basename(entry.subagentSessionFile) === arg);
|
|
1342
|
+
}
|
|
1343
|
+
if (!target) {
|
|
1344
|
+
const runningTarget = switchTargets.find((entry) => entry.id === arg && entry.kind === "subagent");
|
|
1345
|
+
if (runningTarget?.state === "running") {
|
|
1346
|
+
ctx.ui.notify(runningTarget.blockedReason ?? "Selected subagent is still running. Live attach is not implemented yet.", "warning");
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
ctx.ui.notify(`Unknown subagent target: ${arg}. Run /agent to list available targets.`, "warning");
|
|
1350
|
+
return;
|
|
1351
|
+
}
|
|
1352
|
+
if (!fs.existsSync(target.subagentSessionFile)) {
|
|
1353
|
+
ctx.ui.notify(`Subagent session file is missing: ${target.subagentSessionFile}`, "error");
|
|
1354
|
+
return;
|
|
1355
|
+
}
|
|
1356
|
+
if (target.state === "running") {
|
|
1357
|
+
const liveRuntime = liveRuntimeBySessionFile.get(target.subagentSessionFile);
|
|
1358
|
+
if (!liveRuntime) {
|
|
1359
|
+
ctx.ui.notify("Live runtime is no longer available for this subagent. It may have completed.", "warning");
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
const switched = await ctx.switchSession(target.subagentSessionFile);
|
|
1363
|
+
if (switched.cancelled) {
|
|
1364
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1365
|
+
return;
|
|
1366
|
+
}
|
|
1367
|
+
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");
|
|
1368
|
+
return;
|
|
1369
|
+
}
|
|
1370
|
+
const switched = await ctx.switchSession(target.subagentSessionFile);
|
|
1371
|
+
if (switched.cancelled) {
|
|
1372
|
+
ctx.ui.notify("Session switch was cancelled.", "warning");
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
updateAgentSessionLinkState(target.subagentSessionFile, target.state === "failed" ? "failed" : "completed");
|
|
1376
|
+
ctx.ui.notify(`Switched to subagent ${target.agentName}. This resumes the saved subagent session; use /agent parent to return.`, "info");
|
|
1377
|
+
},
|
|
1378
|
+
});
|
|
798
1379
|
pi.registerShortcut(Key.ctrl("b"), {
|
|
799
1380
|
description: shortcutDesc("Move foreground subagent to background", "/subagents list"),
|
|
800
1381
|
handler: async (ctx) => {
|
|
@@ -809,7 +1390,9 @@ export default function (pi) {
|
|
|
809
1390
|
running.claimed = true;
|
|
810
1391
|
let jobId;
|
|
811
1392
|
try {
|
|
812
|
-
jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise
|
|
1393
|
+
jobId = manager.adoptRunning(running.agentName, running.task, running.cwd, running.abortController, running.resultPromise, {
|
|
1394
|
+
parentSessionFile: running.parentSessionFile ?? ctx.sessionManager.getSessionFile(),
|
|
1395
|
+
});
|
|
813
1396
|
}
|
|
814
1397
|
catch (error) {
|
|
815
1398
|
running.claimed = false;
|
|
@@ -873,7 +1456,7 @@ export default function (pi) {
|
|
|
873
1456
|
"For broad review or audit requests, use scout only as a prep step; the parent model or a reviewer should make the final judgments.",
|
|
874
1457
|
"Skip scout when the user already named the exact file/function to inspect or the task is obviously narrow.",
|
|
875
1458
|
"Use parallel mode when tasks are independent and don't need each other's output.",
|
|
876
|
-
"
|
|
1459
|
+
"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.",
|
|
877
1460
|
"If the user wants to wait for a background subagent result, use await_subagent.",
|
|
878
1461
|
],
|
|
879
1462
|
parameters: SubagentParams,
|
|
@@ -884,6 +1467,7 @@ export default function (pi) {
|
|
|
884
1467
|
const confirmProjectAgents = params.confirmProjectAgents ?? false;
|
|
885
1468
|
const cmuxClient = CmuxClient.fromPreferences(loadEffectivePreferences()?.preferences);
|
|
886
1469
|
const cmuxSplitsEnabled = cmuxClient.getConfig().splits;
|
|
1470
|
+
const invokingSessionFile = ctx.sessionManager.getSessionFile();
|
|
887
1471
|
// Resolve isolation mode
|
|
888
1472
|
const isolationMode = readIsolationMode();
|
|
889
1473
|
const useIsolation = Boolean(params.isolated) && isolationMode !== "none";
|
|
@@ -960,7 +1544,7 @@ export default function (pi) {
|
|
|
960
1544
|
}
|
|
961
1545
|
}
|
|
962
1546
|
: undefined;
|
|
963
|
-
const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"));
|
|
1547
|
+
const result = await runSingleAgent(ctx.cwd, agents, step.agent, taskWithContext, step.cwd, i + 1, step.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, chainUpdate, makeDetails("chain"), invokingSessionFile, false, undefined, undefined);
|
|
964
1548
|
results.push(result);
|
|
965
1549
|
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
966
1550
|
if (isError) {
|
|
@@ -1027,7 +1611,7 @@ export default function (pi) {
|
|
|
1027
1611
|
allResults[index] = partial.details.results[0];
|
|
1028
1612
|
emitParallelUpdate();
|
|
1029
1613
|
}
|
|
1030
|
-
}, makeDetails("parallel"));
|
|
1614
|
+
}, makeDetails("parallel"), invokingSessionFile, false, undefined);
|
|
1031
1615
|
let result = await runTask();
|
|
1032
1616
|
// Auto-retry failed tasks (likely API rate limit or transient error)
|
|
1033
1617
|
const isFailed = result.exitCode !== 0 || (result.messages.length === 0 && !signal?.aborted);
|
|
@@ -1084,14 +1668,61 @@ export default function (pi) {
|
|
|
1084
1668
|
const bgInferredModel = resolveSubagentModel({ name: agentForBg.name, model: bgResolvedModelCfg }, { overrideModel: params.model, parentModel: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined });
|
|
1085
1669
|
let jobId;
|
|
1086
1670
|
try {
|
|
1087
|
-
jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined }, async (bgSignal) => {
|
|
1671
|
+
jobId = runSubagentInBackground(manager, agents, params.agent, params.task, params.cwd, params.model, { defaultCwd: ctx.cwd, model: ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, parentSessionFile: invokingSessionFile }, async (bgSignal) => {
|
|
1672
|
+
let liveSessionFile;
|
|
1673
|
+
let liveRuntime;
|
|
1088
1674
|
const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, bgSignal, undefined, // no streaming updates for background jobs
|
|
1089
|
-
makeDetails("single"))
|
|
1675
|
+
makeDetails("single"), invokingSessionFile, true, (info) => {
|
|
1676
|
+
if (!invokingSessionFile || !info.sessionFile)
|
|
1677
|
+
return;
|
|
1678
|
+
upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
|
|
1679
|
+
liveSessionFile = info.sessionFile;
|
|
1680
|
+
if (liveRuntime) {
|
|
1681
|
+
liveRuntime.sessionFile = info.sessionFile;
|
|
1682
|
+
liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
|
|
1683
|
+
liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
|
|
1684
|
+
}
|
|
1685
|
+
}, (event, partial) => {
|
|
1686
|
+
const sessionFile = partial.sessionFile;
|
|
1687
|
+
if (!sessionFile || activeSessionFileForUi !== sessionFile)
|
|
1688
|
+
return;
|
|
1689
|
+
if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
1690
|
+
const delta = String(event.assistantMessageEvent.delta ?? "");
|
|
1691
|
+
if (delta)
|
|
1692
|
+
pushLiveStreamDelta(sessionFile, delta);
|
|
1693
|
+
}
|
|
1694
|
+
if (event?.type === "message_end") {
|
|
1695
|
+
flushLiveStream(sessionFile);
|
|
1696
|
+
}
|
|
1697
|
+
}, {
|
|
1698
|
+
onStart: (control) => {
|
|
1699
|
+
if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
|
|
1700
|
+
return;
|
|
1701
|
+
liveRuntime = {
|
|
1702
|
+
sessionFile: liveSessionFile,
|
|
1703
|
+
parentSessionFile: invokingSessionFile,
|
|
1704
|
+
agentName: params.agent,
|
|
1705
|
+
isBusy: control.isBusy,
|
|
1706
|
+
sendPrompt: control.sendPrompt,
|
|
1707
|
+
sendSteer: control.sendSteer,
|
|
1708
|
+
sendFollowUp: control.sendFollowUp,
|
|
1709
|
+
};
|
|
1710
|
+
if (liveSessionFile) {
|
|
1711
|
+
liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
|
|
1712
|
+
}
|
|
1713
|
+
},
|
|
1714
|
+
onFinish: () => {
|
|
1715
|
+
if (liveSessionFile)
|
|
1716
|
+
liveRuntimeBySessionFile.delete(liveSessionFile);
|
|
1717
|
+
},
|
|
1718
|
+
});
|
|
1090
1719
|
return {
|
|
1091
1720
|
exitCode: result.exitCode,
|
|
1092
1721
|
finalOutput: getFinalOutput(result.messages),
|
|
1093
1722
|
stderr: result.stderr,
|
|
1094
1723
|
model: result.model,
|
|
1724
|
+
sessionFile: result.sessionFile,
|
|
1725
|
+
parentSessionFile: result.parentSessionFile,
|
|
1095
1726
|
};
|
|
1096
1727
|
});
|
|
1097
1728
|
}
|
|
@@ -1117,18 +1748,77 @@ export default function (pi) {
|
|
|
1117
1748
|
const taskId = crypto.randomUUID();
|
|
1118
1749
|
isolation = await createIsolation(effectiveCwd, taskId, isolationMode);
|
|
1119
1750
|
}
|
|
1120
|
-
|
|
1751
|
+
let liveSessionFile;
|
|
1752
|
+
let liveRuntime;
|
|
1753
|
+
const result = await runSingleAgent(ctx.cwd, agents, params.agent, params.task, isolation ? isolation.workDir : params.cwd, undefined, params.model, ctx.model ? { provider: ctx.model.provider, id: ctx.model.id } : undefined, signal, onUpdate, makeDetails("single"), invokingSessionFile, !isolation, !isolation
|
|
1754
|
+
? (info) => {
|
|
1755
|
+
if (!invokingSessionFile || !info.sessionFile)
|
|
1756
|
+
return;
|
|
1757
|
+
upsertAgentSessionLink(params.agent, params.task, invokingSessionFile, info.sessionFile, "running");
|
|
1758
|
+
liveSessionFile = info.sessionFile;
|
|
1759
|
+
if (liveRuntime) {
|
|
1760
|
+
liveRuntime.sessionFile = info.sessionFile;
|
|
1761
|
+
liveRuntime.parentSessionFile = info.parentSessionFile ?? invokingSessionFile;
|
|
1762
|
+
liveRuntimeBySessionFile.set(info.sessionFile, liveRuntime);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
: undefined, !isolation
|
|
1766
|
+
? (event, partial) => {
|
|
1767
|
+
const sessionFile = partial.sessionFile;
|
|
1768
|
+
if (!sessionFile || activeSessionFileForUi !== sessionFile)
|
|
1769
|
+
return;
|
|
1770
|
+
if (event?.type === "message_update" && event.assistantMessageEvent?.type === "text_delta") {
|
|
1771
|
+
const delta = String(event.assistantMessageEvent.delta ?? "");
|
|
1772
|
+
if (delta)
|
|
1773
|
+
pushLiveStreamDelta(sessionFile, delta);
|
|
1774
|
+
}
|
|
1775
|
+
if (event?.type === "message_end") {
|
|
1776
|
+
flushLiveStream(sessionFile);
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
: undefined, !isolation
|
|
1121
1780
|
? {
|
|
1122
1781
|
onStart: (control) => {
|
|
1123
1782
|
activeForegroundSubagent = { ...control, claimed: false };
|
|
1124
1783
|
ctx.ui.setStatus(foregroundSubagentStatusKey, foregroundSubagentHint);
|
|
1784
|
+
if (!control.sendPrompt || !control.sendSteer || !control.sendFollowUp || !control.isBusy)
|
|
1785
|
+
return;
|
|
1786
|
+
liveRuntime = {
|
|
1787
|
+
sessionFile: liveSessionFile,
|
|
1788
|
+
parentSessionFile: invokingSessionFile,
|
|
1789
|
+
agentName: params.agent,
|
|
1790
|
+
isBusy: control.isBusy,
|
|
1791
|
+
sendPrompt: control.sendPrompt,
|
|
1792
|
+
sendSteer: control.sendSteer,
|
|
1793
|
+
sendFollowUp: control.sendFollowUp,
|
|
1794
|
+
};
|
|
1795
|
+
if (liveSessionFile && liveRuntime) {
|
|
1796
|
+
liveRuntimeBySessionFile.set(liveSessionFile, liveRuntime);
|
|
1797
|
+
}
|
|
1125
1798
|
},
|
|
1126
1799
|
onFinish: () => {
|
|
1127
1800
|
activeForegroundSubagent = null;
|
|
1128
1801
|
ctx.ui.setStatus(foregroundSubagentStatusKey, undefined);
|
|
1802
|
+
if (liveSessionFile)
|
|
1803
|
+
liveRuntimeBySessionFile.delete(liveSessionFile);
|
|
1129
1804
|
},
|
|
1130
1805
|
}
|
|
1131
1806
|
: undefined);
|
|
1807
|
+
if (result.sessionFile && invokingSessionFile) {
|
|
1808
|
+
const existingParent = parentSessionByChild.get(result.sessionFile);
|
|
1809
|
+
if (!existingParent) {
|
|
1810
|
+
registerAgentSessionLink({
|
|
1811
|
+
agentName: result.agent,
|
|
1812
|
+
task: result.task,
|
|
1813
|
+
parentSessionFile: invokingSessionFile,
|
|
1814
|
+
subagentSessionFile: result.sessionFile,
|
|
1815
|
+
state: result.exitCode === 0 ? "completed" : "failed",
|
|
1816
|
+
});
|
|
1817
|
+
}
|
|
1818
|
+
else {
|
|
1819
|
+
updateAgentSessionLinkState(result.sessionFile, result.exitCode === 0 ? "completed" : "failed");
|
|
1820
|
+
}
|
|
1821
|
+
}
|
|
1132
1822
|
if (result.backgroundJobId) {
|
|
1133
1823
|
return {
|
|
1134
1824
|
content: [{ type: "text", text: `Moved ${result.agent} to background as **${result.backgroundJobId}**. Use \`await_subagent\`, \`/subagents wait ${result.backgroundJobId}\`, or \`/subagents output ${result.backgroundJobId}\`.` }],
|
|
@@ -1143,10 +1833,11 @@ export default function (pi) {
|
|
|
1143
1833
|
}
|
|
1144
1834
|
}
|
|
1145
1835
|
const isError = result.exitCode !== 0 || result.stopReason === "error" || result.stopReason === "aborted";
|
|
1836
|
+
const agentSwitchHint = result.sessionFile ? "\n\nTip: run `/agent` to switch focus to this subagent session." : "";
|
|
1146
1837
|
if (isError) {
|
|
1147
1838
|
const errorMsg = result.errorMessage || result.stderr || getFinalOutput(result.messages) || "(no output)";
|
|
1148
1839
|
return {
|
|
1149
|
-
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}` }],
|
|
1840
|
+
content: [{ type: "text", text: `Agent ${result.stopReason || "failed"}: ${errorMsg}${agentSwitchHint}` }],
|
|
1150
1841
|
details: makeDetails("single")([result]),
|
|
1151
1842
|
isError: true,
|
|
1152
1843
|
};
|
|
@@ -1155,6 +1846,8 @@ export default function (pi) {
|
|
|
1155
1846
|
if (mergeResult && !mergeResult.success) {
|
|
1156
1847
|
outputText += `\n\n⚠ Patch merge failed: ${mergeResult.error || "unknown error"}`;
|
|
1157
1848
|
}
|
|
1849
|
+
if (agentSwitchHint)
|
|
1850
|
+
outputText += agentSwitchHint;
|
|
1158
1851
|
return {
|
|
1159
1852
|
content: [{ type: "text", text: outputText }],
|
|
1160
1853
|
details: makeDetails("single")([result]),
|