refactor(coding-agent): add runtime host for session switching closes #2024

This commit is contained in:
Mario Zechner
2026-03-31 13:49:57 +02:00
parent a3bf1eb399
commit d86122cbd3
32 changed files with 1328 additions and 692 deletions

View File

@@ -12,7 +12,7 @@
*/
import * as crypto from "node:crypto";
import type { AgentSession } from "../../core/agent-session.js";
import type { AgentSessionRuntimeHost } from "../../core/agent-session-runtime.js";
import type {
ExtensionUIContext,
ExtensionUIDialogOptions,
@@ -43,8 +43,10 @@ export type {
* Run in RPC mode.
* Listens for JSON commands on stdin, outputs events and responses on stdout.
*/
export async function runRpcMode(session: AgentSession): Promise<never> {
export async function runRpcMode(runtimeHost: AgentSessionRuntimeHost): Promise<never> {
takeOverStdout();
let session = runtimeHost.session;
let unsubscribe: (() => void) | undefined;
const output = (obj: RpcResponse | RpcExtensionUIRequest | object) => {
writeRawStdout(serializeJsonLine(obj));
@@ -280,49 +282,61 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
},
});
// Set up extensions with RPC-based UI context
await session.bindExtensions({
uiContext: createExtensionUIContext(),
commandContextActions: {
waitForIdle: () => session.agent.waitForIdle(),
newSession: async (options) => {
// Delegate to AgentSession (handles setup + agent state sync)
const success = await session.newSession(options);
return { cancelled: !success };
const rebindSession = async (): Promise<void> => {
session = runtimeHost.session;
await session.bindExtensions({
uiContext: createExtensionUIContext(),
commandContextActions: {
waitForIdle: () => session.agent.waitForIdle(),
newSession: async (options) => {
const result = await runtimeHost.newSession(options);
if (!result.cancelled) {
await rebindSession();
}
return result;
},
fork: async (entryId) => {
const result = await runtimeHost.fork(entryId);
if (!result.cancelled) {
await rebindSession();
}
return { cancelled: result.cancelled };
},
navigateTree: async (targetId, options) => {
const result = await session.navigateTree(targetId, {
summarize: options?.summarize,
customInstructions: options?.customInstructions,
replaceInstructions: options?.replaceInstructions,
label: options?.label,
});
return { cancelled: result.cancelled };
},
switchSession: async (sessionPath) => {
const result = await runtimeHost.switchSession(sessionPath);
if (!result.cancelled) {
await rebindSession();
}
return result;
},
reload: async () => {
await session.reload();
},
},
fork: async (entryId) => {
const result = await session.fork(entryId);
return { cancelled: result.cancelled };
shutdownHandler: () => {
shutdownRequested = true;
},
navigateTree: async (targetId, options) => {
const result = await session.navigateTree(targetId, {
summarize: options?.summarize,
customInstructions: options?.customInstructions,
replaceInstructions: options?.replaceInstructions,
label: options?.label,
});
return { cancelled: result.cancelled };
onError: (err) => {
output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
},
switchSession: async (sessionPath) => {
const success = await session.switchSession(sessionPath);
return { cancelled: !success };
},
reload: async () => {
await session.reload();
},
},
shutdownHandler: () => {
shutdownRequested = true;
},
onError: (err) => {
output({ type: "extension_error", extensionPath: err.extensionPath, event: err.event, error: err.error });
},
});
});
// Output all agent events as JSON
session.subscribe((event) => {
output(event);
});
unsubscribe?.();
unsubscribe = session.subscribe((event) => {
output(event);
});
};
await rebindSession();
// Handle a single command
const handleCommand = async (command: RpcCommand): Promise<RpcResponse> => {
@@ -364,8 +378,11 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
case "new_session": {
const options = command.parentSession ? { parentSession: command.parentSession } : undefined;
const cancelled = !(await session.newSession(options));
return success(id, "new_session", { cancelled });
const result = await runtimeHost.newSession(options);
if (!result.cancelled) {
await rebindSession();
}
return success(id, "new_session", result);
}
// =================================================================
@@ -505,12 +522,18 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
}
case "switch_session": {
const cancelled = !(await session.switchSession(command.sessionPath));
return success(id, "switch_session", { cancelled });
const result = await runtimeHost.switchSession(command.sessionPath);
if (!result.cancelled) {
await rebindSession();
}
return success(id, "switch_session", result);
}
case "fork": {
const result = await session.fork(command.entryId);
const result = await runtimeHost.fork(command.entryId);
if (!result.cancelled) {
await rebindSession();
}
return success(id, "fork", { text: result.selectedText, cancelled: result.cancelled });
}
@@ -592,11 +615,8 @@ export async function runRpcMode(session: AgentSession): Promise<never> {
let detachInput = () => {};
async function shutdown(): Promise<never> {
const currentRunner = session.extensionRunner;
if (currentRunner?.hasHandlers("session_shutdown")) {
await currentRunner.emit({ type: "session_shutdown" });
}
unsubscribe?.();
await runtimeHost.dispose();
detachInput();
process.stdin.pause();
process.exit(0);