Files
sproutclaw/packages/coding-agent/src/core/agent-session-services.ts
Mario Zechner 9f9277ccdd 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
2026-04-03 20:14:12 +02:00

198 lines
6.5 KiB
TypeScript

import { join } from "node:path";
import type { ThinkingLevel } from "@mariozechner/pi-agent-core";
import type { Model } from "@mariozechner/pi-ai";
import { getAgentDir } from "../config.js";
import { AuthStorage } from "./auth-storage.js";
import type { SessionStartEvent, ToolDefinition } from "./extensions/index.js";
import { ModelRegistry } from "./model-registry.js";
import { DefaultResourceLoader, type DefaultResourceLoaderOptions, type ResourceLoader } from "./resource-loader.js";
import { type CreateAgentSessionResult, createAgentSession } from "./sdk.js";
import type { SessionManager } from "./session-manager.js";
import { SettingsManager } from "./settings-manager.js";
import type { Tool } from "./tools/index.js";
/**
* Non-fatal issues collected while creating services or sessions.
*
* Runtime creation returns diagnostics to the caller instead of printing or
* exiting. The app layer decides whether warnings should be shown and whether
* errors should abort startup.
*/
export interface AgentSessionRuntimeDiagnostic {
type: "info" | "warning" | "error";
message: string;
}
/**
* Inputs for creating cwd-bound runtime services.
*
* These services are recreated whenever the effective session cwd changes.
* CLI-provided resource paths should be resolved to absolute paths before they
* reach this function, so later cwd switches do not reinterpret them.
*/
export interface CreateAgentSessionServicesOptions {
cwd: string;
agentDir?: string;
authStorage?: AuthStorage;
settingsManager?: SettingsManager;
modelRegistry?: ModelRegistry;
extensionFlagValues?: Map<string, boolean | string>;
resourceLoaderOptions?: Omit<DefaultResourceLoaderOptions, "cwd" | "agentDir" | "settingsManager">;
}
/**
* Inputs for creating an AgentSession from already-created services.
*
* Use this after services exist and any cwd-bound model/tool/session options
* have been resolved against those services.
*/
export interface CreateAgentSessionFromServicesOptions {
services: AgentSessionServices;
sessionManager: SessionManager;
sessionStartEvent?: SessionStartEvent;
model?: Model<any>;
thinkingLevel?: ThinkingLevel;
scopedModels?: Array<{ model: Model<any>; thinkingLevel?: ThinkingLevel }>;
tools?: Tool[];
customTools?: ToolDefinition[];
}
/**
* Coherent cwd-bound runtime services for one effective session cwd.
*
* This is infrastructure only. The AgentSession itself is created separately so
* session options can be resolved against these services first.
*/
export interface AgentSessionServices {
cwd: string;
agentDir: string;
authStorage: AuthStorage;
settingsManager: SettingsManager;
modelRegistry: ModelRegistry;
resourceLoader: ResourceLoader;
diagnostics: AgentSessionRuntimeDiagnostic[];
}
function applyExtensionFlagValues(
resourceLoader: ResourceLoader,
extensionFlagValues: Map<string, boolean | string> | undefined,
): AgentSessionRuntimeDiagnostic[] {
if (!extensionFlagValues) {
return [];
}
const diagnostics: AgentSessionRuntimeDiagnostic[] = [];
const extensionsResult = resourceLoader.getExtensions();
const registeredFlags = new Map<string, { type: "boolean" | "string" }>();
for (const extension of extensionsResult.extensions) {
for (const [name, flag] of extension.flags) {
registeredFlags.set(name, { type: flag.type });
}
}
const unknownFlags: string[] = [];
for (const [name, value] of extensionFlagValues) {
const flag = registeredFlags.get(name);
if (!flag) {
unknownFlags.push(name);
continue;
}
if (flag.type === "boolean") {
extensionsResult.runtime.flagValues.set(name, true);
continue;
}
if (typeof value === "string") {
extensionsResult.runtime.flagValues.set(name, value);
continue;
}
diagnostics.push({
type: "error",
message: `Extension flag "--${name}" requires a value`,
});
}
if (unknownFlags.length > 0) {
diagnostics.push({
type: "error",
message: `Unknown option${unknownFlags.length === 1 ? "" : "s"}: ${unknownFlags.map((name) => `--${name}`).join(", ")}`,
});
}
return diagnostics;
}
/**
* Create cwd-bound runtime services.
*
* Returns services plus diagnostics. It does not create an AgentSession.
*/
export async function createAgentSessionServices(
options: CreateAgentSessionServicesOptions,
): Promise<AgentSessionServices> {
const cwd = options.cwd;
const agentDir = options.agentDir ?? getAgentDir();
const authStorage = options.authStorage ?? AuthStorage.create(join(agentDir, "auth.json"));
const settingsManager = options.settingsManager ?? SettingsManager.create(cwd, agentDir);
const modelRegistry = options.modelRegistry ?? ModelRegistry.create(authStorage, join(agentDir, "models.json"));
const resourceLoader = new DefaultResourceLoader({
...(options.resourceLoaderOptions ?? {}),
cwd,
agentDir,
settingsManager,
});
await resourceLoader.reload();
const diagnostics: AgentSessionRuntimeDiagnostic[] = [];
const extensionsResult = resourceLoader.getExtensions();
for (const { name, config, extensionPath } of extensionsResult.runtime.pendingProviderRegistrations) {
try {
modelRegistry.registerProvider(name, config);
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
diagnostics.push({
type: "error",
message: `Extension "${extensionPath}" error: ${message}`,
});
}
}
extensionsResult.runtime.pendingProviderRegistrations = [];
diagnostics.push(...applyExtensionFlagValues(resourceLoader, options.extensionFlagValues));
return {
cwd,
agentDir,
authStorage,
settingsManager,
modelRegistry,
resourceLoader,
diagnostics,
};
}
/**
* Create an AgentSession from previously created services.
*
* This keeps session creation separate from service creation so callers can
* resolve model, thinking, tools, and other session inputs against the target
* cwd before constructing the session.
*/
export async function createAgentSessionFromServices(
options: CreateAgentSessionFromServicesOptions,
): Promise<CreateAgentSessionResult> {
return createAgentSession({
cwd: options.services.cwd,
agentDir: options.services.agentDir,
authStorage: options.services.authStorage,
settingsManager: options.services.settingsManager,
modelRegistry: options.services.modelRegistry,
resourceLoader: options.services.resourceLoader,
sessionManager: options.sessionManager,
model: options.model,
thinkingLevel: options.thinkingLevel,
scopedModels: options.scopedModels,
tools: options.tools,
customTools: options.customTools,
sessionStartEvent: options.sessionStartEvent,
});
}