refactor(coding-agent): replace AgentSessionRuntimeHost with closure-based AgentSessionRuntime

- Replace AgentSessionRuntimeHost and bootstrap abstractions with AgentSessionRuntime
- Runtime creation is now closure-based via CreateAgentSessionRuntimeFactory
- Factory closes over process-global fixed inputs, recreates cwd-bound services per effective cwd
- Session config (model, thinking, tools, scoped models) re-resolved per target cwd
- CLI resource paths resolved once at startup as absolute paths
- Swap lifecycle: teardown old, create next, apply next (hard fail on creation error)
- Unified diagnostics model (info/warning/error) for args, services, session resolution, resources
- No logging or process exits inside creation/parsing logic
- Removed session_directory support
- Removed session_switch and session_fork extension events (use session_start with reason)
- Moved package/config CLI to package-manager-cli.ts
- Fixed theme init for --resume session picker
- Fixed flaky reftable footer test (content-based polling)
- Fixed silent drop of unknown single-dash CLI flags
- Added error diagnostics for missing explicit CLI resource paths
- Updated SDK docs, examples, plans, exports, tests, changelog

fixes #2753
This commit is contained in:
Mario Zechner
2026-04-03 20:14:12 +02:00
parent 042066b982
commit 9f9277ccdd
38 changed files with 2180 additions and 1366 deletions

View File

@@ -47,7 +47,7 @@ import {
VERSION,
} from "../../config.js";
import { type AgentSession, type AgentSessionEvent, parseSkillBlock } from "../../core/agent-session.js";
import type { AgentSessionRuntimeHost } from "../../core/agent-session-runtime.js";
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js";
import type {
ExtensionContext,
ExtensionRunner,
@@ -107,6 +107,7 @@ import {
setRegisteredThemes,
setTheme,
setThemeInstance,
stopThemeWatcher,
Theme,
type ThemeColor,
theme,
@@ -145,7 +146,7 @@ export interface InteractiveModeOptions {
}
export class InteractiveMode {
private runtimeHost: AgentSessionRuntimeHost;
private runtimeHost: AgentSessionRuntime;
private ui: TUI;
private chatContainer: Container;
private pendingMessagesContainer: Container;
@@ -257,7 +258,7 @@ export class InteractiveMode {
}
constructor(
runtimeHost: AgentSessionRuntimeHost,
runtimeHost: AgentSessionRuntime,
private options: InteractiveModeOptions = {},
) {
this.runtimeHost = runtimeHost;
@@ -1177,23 +1178,31 @@ export class InteractiveMode {
this.loadingAnimation = undefined;
}
this.statusContainer.clear();
const result = await this.runtimeHost.newSession(options);
if (!result.cancelled) {
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.ui.requestRender();
try {
const result = await this.runtimeHost.newSession(options);
if (!result.cancelled) {
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.ui.requestRender();
}
return result;
} catch (error: unknown) {
return this.handleFatalRuntimeError("Failed to create session", error);
}
return result;
},
fork: async (entryId) => {
const result = await this.runtimeHost.fork(entryId);
if (!result.cancelled) {
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.editor.setText(result.selectedText ?? "");
this.showStatus("Forked to new session");
try {
const result = await this.runtimeHost.fork(entryId);
if (!result.cancelled) {
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.editor.setText(result.selectedText ?? "");
this.showStatus("Forked to new session");
}
return { cancelled: result.cancelled };
} catch (error: unknown) {
return this.handleFatalRuntimeError("Failed to fork session", error);
}
return { cancelled: result.cancelled };
},
navigateTree: async (targetId, options) => {
const result = await this.session.navigateTree(targetId, {
@@ -1275,6 +1284,14 @@ export class InteractiveMode {
this.updateTerminalTitle();
}
private async handleFatalRuntimeError(prefix: string, error: unknown): Promise<never> {
const message = error instanceof Error ? error.message : String(error);
this.showError(`${prefix}: ${message}`);
stopThemeWatcher();
this.stop();
process.exit(1);
}
private renderCurrentSessionState(): void {
this.chatContainer.clear();
this.pendingMessagesContainer.clear();
@@ -3817,13 +3834,17 @@ export class InteractiveMode {
this.loadingAnimation = undefined;
}
this.statusContainer.clear();
const result = await this.runtimeHost.switchSession(sessionPath);
if (result.cancelled) {
return;
try {
const result = await this.runtimeHost.switchSession(sessionPath);
if (result.cancelled) {
return;
}
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.showStatus("Resumed session");
} catch (error: unknown) {
await this.handleFatalRuntimeError("Failed to resume session", error);
}
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.showStatus("Resumed session");
}
private async showOAuthSelector(mode: "login" | "logout"): Promise<void> {
@@ -4088,7 +4109,7 @@ export class InteractiveMode {
this.renderCurrentSessionState();
this.showStatus(`Session imported from: ${inputPath}`);
} catch (error: unknown) {
this.showError(`Failed to import session: ${error instanceof Error ? error.message : "Unknown error"}`);
await this.handleFatalRuntimeError("Failed to import session", error);
}
}
@@ -4432,15 +4453,19 @@ export class InteractiveMode {
this.loadingAnimation = undefined;
}
this.statusContainer.clear();
const result = await this.runtimeHost.newSession();
if (result.cancelled) {
return;
try {
const result = await this.runtimeHost.newSession();
if (result.cancelled) {
return;
}
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
this.ui.requestRender();
} catch (error: unknown) {
await this.handleFatalRuntimeError("Failed to create session", error);
}
await this.handleRuntimeSessionChange();
this.renderCurrentSessionState();
this.chatContainer.addChild(new Spacer(1));
this.chatContainer.addChild(new Text(`${theme.fg("accent", "✓ New session started")}`, 1, 1));
this.ui.requestRender();
}
private handleDebugCommand(): void {