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:
@@ -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 {
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
*/
|
||||
|
||||
import type { AssistantMessage, ImageContent } from "@mariozechner/pi-ai";
|
||||
import type { AgentSessionRuntimeHost } from "../core/agent-session-runtime.js";
|
||||
import type { AgentSessionRuntime } from "../core/agent-session-runtime.js";
|
||||
import { flushRawStdout, writeRawStdout } from "../core/output-guard.js";
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export interface PrintModeOptions {
|
||||
* Run in print (single-shot) mode.
|
||||
* Sends prompts to the agent and outputs the result.
|
||||
*/
|
||||
export async function runPrintMode(runtimeHost: AgentSessionRuntimeHost, options: PrintModeOptions): Promise<number> {
|
||||
export async function runPrintMode(runtimeHost: AgentSessionRuntime, options: PrintModeOptions): Promise<number> {
|
||||
const { mode, messages = [], initialMessage, initialImages } = options;
|
||||
let exitCode = 0;
|
||||
let session = runtimeHost.session;
|
||||
@@ -124,6 +124,9 @@ export async function runPrintMode(runtimeHost: AgentSessionRuntimeHost, options
|
||||
}
|
||||
|
||||
return exitCode;
|
||||
} catch (error: unknown) {
|
||||
console.error(error instanceof Error ? error.message : String(error));
|
||||
return 1;
|
||||
} finally {
|
||||
unsubscribe?.();
|
||||
await runtimeHost.dispose();
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
*/
|
||||
|
||||
import * as crypto from "node:crypto";
|
||||
import type { AgentSessionRuntimeHost } from "../../core/agent-session-runtime.js";
|
||||
import type { AgentSessionRuntime } from "../../core/agent-session-runtime.js";
|
||||
import type {
|
||||
ExtensionUIContext,
|
||||
ExtensionUIDialogOptions,
|
||||
@@ -43,7 +43,7 @@ export type {
|
||||
* Run in RPC mode.
|
||||
* Listens for JSON commands on stdin, outputs events and responses on stdout.
|
||||
*/
|
||||
export async function runRpcMode(runtimeHost: AgentSessionRuntimeHost): Promise<never> {
|
||||
export async function runRpcMode(runtimeHost: AgentSessionRuntime): Promise<never> {
|
||||
takeOverStdout();
|
||||
let session = runtimeHost.session;
|
||||
let unsubscribe: (() => void) | undefined;
|
||||
@@ -628,29 +628,49 @@ export async function runRpcMode(runtimeHost: AgentSessionRuntimeHost): Promise<
|
||||
}
|
||||
|
||||
const handleInputLine = async (line: string) => {
|
||||
let parsed: unknown;
|
||||
try {
|
||||
const parsed = JSON.parse(line);
|
||||
parsed = JSON.parse(line);
|
||||
} catch (parseError: unknown) {
|
||||
output(
|
||||
error(
|
||||
undefined,
|
||||
"parse",
|
||||
`Failed to parse command: ${parseError instanceof Error ? parseError.message : String(parseError)}`,
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle extension UI responses
|
||||
if (parsed.type === "extension_ui_response") {
|
||||
const response = parsed as RpcExtensionUIResponse;
|
||||
const pending = pendingExtensionRequests.get(response.id);
|
||||
if (pending) {
|
||||
pendingExtensionRequests.delete(response.id);
|
||||
pending.resolve(response);
|
||||
}
|
||||
return;
|
||||
// Handle extension UI responses
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
"type" in parsed &&
|
||||
parsed.type === "extension_ui_response"
|
||||
) {
|
||||
const response = parsed as RpcExtensionUIResponse;
|
||||
const pending = pendingExtensionRequests.get(response.id);
|
||||
if (pending) {
|
||||
pendingExtensionRequests.delete(response.id);
|
||||
pending.resolve(response);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle regular commands
|
||||
const command = parsed as RpcCommand;
|
||||
const command = parsed as RpcCommand;
|
||||
try {
|
||||
const response = await handleCommand(command);
|
||||
output(response);
|
||||
|
||||
// Check for deferred shutdown request (idle between commands)
|
||||
await checkShutdownRequested();
|
||||
} catch (e: any) {
|
||||
output(error(undefined, "parse", `Failed to parse command: ${e.message}`));
|
||||
} catch (commandError: unknown) {
|
||||
output(
|
||||
error(
|
||||
command.id,
|
||||
command.type,
|
||||
commandError instanceof Error ? commandError.message : String(commandError),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user