Migrate piSessionService to ModelRuntime (slice 4)

Pass modelRuntime to createAgentSessionServices instead of authStorage +
modelRegistry; carry ModelRuntime on PiAgentSession; make modelRuntime a
required PiSessionService dependency (sessiond already injects auth.runtime).
Switch anthropicSubscriptionWarning to readStoredCredential, and rederive
model reads (availableModels/setModel/syncCurrentModelAuthWarning) via the
runtime (getAvailableSnapshot/getModel/hasConfiguredAuth). sessiond.ts and
piSessionService.ts now typecheck; only slice-5 test/support files remain.
This commit is contained in:
Federico Jaramillo Martinez
2026-07-17 21:32:02 +02:00
parent 7543982e3b
commit 4ccd4f81fc
+25 -25
View File
@@ -1,20 +1,21 @@
import { statSync } from "node:fs"; import { statSync } from "node:fs";
import { join } from "node:path";
import { open, readFile, writeFile } from "node:fs/promises"; import { open, readFile, writeFile } from "node:fs/promises";
import type { ImageContent } from "@earendil-works/pi-ai"; import type { ImageContent } from "@earendil-works/pi-ai";
import type { StreamFn } from "@earendil-works/pi-agent-core"; import type { StreamFn } from "@earendil-works/pi-agent-core";
import { import {
AuthStorage,
createAgentSessionFromServices, createAgentSessionFromServices,
createAgentSessionRuntime, createAgentSessionRuntime,
createAgentSessionServices, createAgentSessionServices,
createEditToolDefinition, createEditToolDefinition,
defineTool, defineTool,
ModelRegistry, readStoredCredential,
SessionManager, SessionManager,
type AgentSessionRuntimeDiagnostic, type AgentSessionRuntimeDiagnostic,
type AgentSessionServices, type AgentSessionServices,
type CreateAgentSessionRuntimeFactory, type CreateAgentSessionRuntimeFactory,
type EditToolDetails, type EditToolDetails,
type ModelRuntime,
type ResourceDiagnostic, type ResourceDiagnostic,
} from "@earendil-works/pi-coding-agent"; } from "@earendil-works/pi-coding-agent";
import type { ClientArchiveSessionsResponse, ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionCleanupExecuteResponse, ClientSessionCleanupPreviewResponse, ClientSessionModel, ClientSessionStatus, ClientThinkingLevel, SessionStreamSnapshot, SessionUiEvent } from "../types.js"; import type { ClientArchiveSessionsResponse, ClientCommand, ClientCommandResult, ClientMessagePage, ClientSession, ClientSessionCleanupExecuteResponse, ClientSessionCleanupPreviewResponse, ClientSessionModel, ClientSessionStatus, ClientThinkingLevel, SessionStreamSnapshot, SessionUiEvent } from "../types.js";
@@ -26,7 +27,6 @@ import { SessionCommandService } from "./sessionCommandService.js";
import { SessionArchiveStore, type ArchivedSessionRecord, type ArchiveSessionInput } from "./sessionArchiveStore.js"; import { SessionArchiveStore, type ArchivedSessionRecord, type ArchiveSessionInput } from "./sessionArchiveStore.js";
import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, type SessionArchiveTreeCandidate } from "./sessionArchiveTree.js"; import { findArchiveCandidateByIdOrPrefix, planSessionArchiveTree, type SessionArchiveTreeCandidate } from "./sessionArchiveTree.js";
import type { ActiveSession } from "./sessionRuntimeStore.js"; import type { ActiveSession } from "./sessionRuntimeStore.js";
import { createModelRegistryForAgentDir, type AuthChange } from "./authService.js";
import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js"; import { deterministicSessionName, fallbackSessionName, generateShortSessionName } from "./sessionNameGenerator.js";
import { computeEditPreview, type EditPreviewResult } from "./editPreview.js"; import { computeEditPreview, type EditPreviewResult } from "./editPreview.js";
import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js"; import { attachmentsToInlineImages, saveAttachmentsToWorkspace } from "./attachmentService.js";
@@ -34,6 +34,7 @@ import { parsePromptAttachments } from "../../shared/promptAttachments.js";
import type { SavedPromptAttachment, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkFailure, SessionBulkMutationRef, SessionWarning } from "../../shared/apiTypes.js"; import type { SavedPromptAttachment, SessionBulkArchiveResponse, SessionBulkDeleteArchivedResponse, SessionBulkFailure, SessionBulkMutationRef, SessionWarning } from "../../shared/apiTypes.js";
import type { SessionRouteLookup, SessionRouteRef, SessionRouteService } from "./sessionService.js"; import type { SessionRouteLookup, SessionRouteRef, SessionRouteService } from "./sessionService.js";
import { type AuthChange } from "./authService.js";
import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js"; import { canonicalizeStoredCwd, cwdPathsEqual } from "../workingDirectory.js";
import type { WorkspaceActivityService } from "../activity/workspaceActivityService.js"; import type { WorkspaceActivityService } from "../activity/workspaceActivityService.js";
import { createSpawnSessionToolDefinition, type SpawnSessionInvocation, type SpawnSessionResult } from "./spawnSessionTool.js"; import { createSpawnSessionToolDefinition, type SpawnSessionInvocation, type SpawnSessionResult } from "./spawnSessionTool.js";
@@ -171,7 +172,6 @@ interface BulkDeletePlanItem {
} }
type AgentModel = NonNullable<SpawnSessionInvocation["model"]>; type AgentModel = NonNullable<SpawnSessionInvocation["model"]>;
type ModelRegistryInstance = ReturnType<typeof ModelRegistry.create>;
export interface PiSessionManager { export interface PiSessionManager {
getCwd(): string; getCwd(): string;
@@ -208,7 +208,7 @@ interface PiExtensionBindings {
} }
export interface PiAgentSession { export interface PiAgentSession {
modelRegistry: ModelRegistryInstance; modelRuntime: ModelRuntime;
/** /**
* Narrow read/write of the SDK `SettingsManager`, exposing only the warning * Narrow read/write of the SDK `SettingsManager`, exposing only the warning
* suppression flags consumed here (e.g. `anthropicExtraUsage`). Used to gate * suppression flags consumed here (e.g. `anthropicExtraUsage`). Used to gate
@@ -383,11 +383,12 @@ const ANTHROPIC_EXTRA_USAGE_DISMISS_ID = "anthropicExtraUsage";
* synchronous live status computation. * synchronous live status computation.
*/ */
export function anthropicSubscriptionWarning( export function anthropicSubscriptionWarning(
session: Pick<PiAgentSession, "model" | "modelRegistry" | "settingsManager">, session: Pick<PiAgentSession, "model" | "settingsManager">,
authPath?: string,
): SessionWarning | undefined { ): SessionWarning | undefined {
if (session.settingsManager.getWarnings().anthropicExtraUsage === false) return undefined; if (session.settingsManager.getWarnings().anthropicExtraUsage === false) return undefined;
if (session.model?.provider !== "anthropic") return undefined; if (session.model?.provider !== "anthropic") return undefined;
const credential = session.modelRegistry.authStorage.get("anthropic"); const credential = readStoredCredential("anthropic", authPath);
if (credential === undefined) return undefined; if (credential === undefined) return undefined;
const isSubscriptionAuth = credential.type === "oauth" const isSubscriptionAuth = credential.type === "oauth"
? true ? true
@@ -487,14 +488,13 @@ export function createPiWebCustomToolDefinitions(
} }
function createDefaultRuntimeFactory( function createDefaultRuntimeFactory(
authStorage: AuthStorage, modelRuntime: ModelRuntime,
modelRegistry: ModelRegistryInstance,
sessionManagers: Pick<PiSessionManagerGateway, "open">, sessionManagers: Pick<PiSessionManagerGateway, "open">,
spawn?: SpawnSessionFn, spawn?: SpawnSessionFn,
subsessions?: SubsessionToolDeps, subsessions?: SubsessionToolDeps,
): PiWebCreateAgentSessionRuntimeFactory { ): PiWebCreateAgentSessionRuntimeFactory {
return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => { return async ({ cwd, agentDir, sessionManager, sessionStartEvent, initialModel, delegationToolsEnabled }) => {
const services = await createAgentSessionServices({ cwd, agentDir, authStorage, modelRegistry }); const services = await createAgentSessionServices({ cwd, agentDir, modelRuntime });
const resolvedDelegationToolsEnabled = delegationToolsEnabled const resolvedDelegationToolsEnabled = delegationToolsEnabled
?? await sessionAllowsDelegationTools(sessionManager, sessionManagers); ?? await sessionAllowsDelegationTools(sessionManager, sessionManagers);
const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions); const customTools = createPiWebCustomToolDefinitions(cwd, resolvedDelegationToolsEnabled, spawn, subsessions);
@@ -539,7 +539,7 @@ export interface PiSessionServiceDependencies {
archiveStore?: SessionArchiveRepository; archiveStore?: SessionArchiveRepository;
createRuntime?: PiWebCreateAgentSessionRuntimeFactory; createRuntime?: PiWebCreateAgentSessionRuntimeFactory;
createAgentRuntime?: CreateAgentRuntime; createAgentRuntime?: CreateAgentRuntime;
modelRegistry?: ModelRegistryInstance; modelRuntime: ModelRuntime;
heartbeatIntervalMs?: number; heartbeatIntervalMs?: number;
workspaceActivity?: Pick<WorkspaceActivityService, "applySessionStatus" | "applySessionActivity" | "removeSession" | "reconcileSessionActivity">; workspaceActivity?: Pick<WorkspaceActivityService, "applySessionStatus" | "applySessionActivity" | "removeSession" | "reconcileSessionActivity">;
/** /**
@@ -589,7 +589,7 @@ export class PiSessionService implements SessionRouteService {
private readonly sessionManager: PiSessionManagerGateway; private readonly sessionManager: PiSessionManagerGateway;
private readonly createRuntime: PiWebCreateAgentSessionRuntimeFactory; private readonly createRuntime: PiWebCreateAgentSessionRuntimeFactory;
private readonly createAgentRuntime: CreateAgentRuntime; private readonly createAgentRuntime: CreateAgentRuntime;
private readonly modelRegistry: ModelRegistryInstance; private readonly modelRuntime: ModelRuntime;
private readonly workspaceActivity: Pick<WorkspaceActivityService, "applySessionStatus" | "applySessionActivity" | "removeSession" | "reconcileSessionActivity"> | undefined; private readonly workspaceActivity: Pick<WorkspaceActivityService, "applySessionStatus" | "applySessionActivity" | "removeSession" | "reconcileSessionActivity"> | undefined;
private readonly spawnTargets: SpawnTargetResolver | undefined; private readonly spawnTargets: SpawnTargetResolver | undefined;
private readonly logger: PiSessionLogger; private readonly logger: PiSessionLogger;
@@ -599,7 +599,7 @@ export class PiSessionService implements SessionRouteService {
this.archiveStore = deps.archiveStore ?? new SessionArchiveStore(); this.archiveStore = deps.archiveStore ?? new SessionArchiveStore();
this.agentDir = deps.agentDir; this.agentDir = deps.agentDir;
this.sessionManager = deps.sessionManager; this.sessionManager = deps.sessionManager;
this.modelRegistry = deps.modelRegistry ?? createModelRegistryForAgentDir(this.agentDir); this.modelRuntime = deps.modelRuntime;
this.spawnTargets = deps.spawnTargets; this.spawnTargets = deps.spawnTargets;
this.logger = deps.logger ?? noopLogger; this.logger = deps.logger ?? noopLogger;
this.now = deps.now ?? (() => new Date()); this.now = deps.now ?? (() => new Date());
@@ -607,8 +607,7 @@ export class PiSessionService implements SessionRouteService {
// also require the spawn capability (they share its project-scope resolver). // also require the spawn capability (they share its project-scope resolver).
const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true; const subsessionsActive = this.spawnTargets !== undefined && deps.subsessionsEnabled === true;
this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory( this.createRuntime = deps.createRuntime ?? createDefaultRuntimeFactory(
this.modelRegistry.authStorage, this.modelRuntime,
this.modelRegistry,
this.sessionManager, this.sessionManager,
this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input), this.spawnTargets === undefined ? undefined : (input) => this.spawnSession(input),
!subsessionsActive ? undefined : { !subsessionsActive ? undefined : {
@@ -1159,22 +1158,22 @@ export class PiSessionService implements SessionRouteService {
async availableModels(ref: PiSessionLookup): Promise<ClientSessionModel[]> { async availableModels(ref: PiSessionLookup): Promise<ClientSessionModel[]> {
const session = await this.getOrOpen(ref); const session = await this.getOrOpen(ref);
session.modelRegistry.refresh(); await session.modelRuntime.refresh();
const models = session.scopedModels.length > 0 const models = session.scopedModels.length > 0
? session.scopedModels.map((scoped) => scoped.model) ? session.scopedModels.map((scoped) => scoped.model)
: session.modelRegistry.getAvailable(); : session.modelRuntime.getAvailableSnapshot();
return models.map(modelToClientModel); return models.map(modelToClientModel);
} }
async setModel(ref: PiSessionLookup, provider: string, modelId: string): Promise<ClientSessionStatus> { async setModel(ref: PiSessionLookup, provider: string, modelId: string): Promise<ClientSessionStatus> {
await this.assertWritable(ref); await this.assertWritable(ref);
const session = await this.getOrOpen(ref); const session = await this.getOrOpen(ref);
session.modelRegistry.refresh(); await session.modelRuntime.refresh();
const candidates = session.scopedModels.length > 0 const candidates = session.scopedModels.length > 0
? session.scopedModels.map((scoped) => scoped.model) ? session.scopedModels.map((scoped) => scoped.model)
: session.modelRegistry.getAvailable(); : session.modelRuntime.getAvailableSnapshot();
const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId) const model = candidates.find((candidate) => candidate.provider === provider && candidate.id === modelId)
?? session.modelRegistry.find(provider, modelId); ?? session.modelRuntime.getModel(provider, modelId);
if (model === undefined) throw new Error(`Model not found: ${provider}/${modelId}`); if (model === undefined) throw new Error(`Model not found: ${provider}/${modelId}`);
await session.setModel(model); await session.setModel(model);
this.publishActivity(session, `model: ${model.id}`, "idle", model.provider); this.publishActivity(session, `model: ${model.id}`, "idle", model.provider);
@@ -2020,10 +2019,11 @@ export class PiSessionService implements SessionRouteService {
} }
applyAuthChange(change: AuthChange = {}): void { applyAuthChange(change: AuthChange = {}): void {
this.modelRegistry.refresh(); // The shared model runtime is refreshed by AuthService before it emits the
// change (and every session shares that runtime), so no refresh is needed
// here — this keeps the subscribe callback synchronous.
for (const active of this.active.values()) { for (const active of this.active.values()) {
const { session } = active.runtime; const { session } = active.runtime;
session.modelRegistry.refresh();
this.syncCurrentModelAuthWarning(session, change.removedProviderId); this.syncCurrentModelAuthWarning(session, change.removedProviderId);
this.publishStatus(session); this.publishStatus(session);
} }
@@ -2034,9 +2034,9 @@ export class PiSessionService implements SessionRouteService {
if (model === undefined) return; if (model === undefined) return;
if (model.provider === "unknown" && model.id === "unknown") return; if (model.provider === "unknown" && model.id === "unknown") return;
const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id); const warningKey = authLossWarningKey(session.sessionId, model.provider, model.id);
const registered = session.modelRegistry.find(model.provider, model.id); const registered = session.modelRuntime.getModel(model.provider, model.id);
if (registered === undefined) return; if (registered === undefined) return;
if (session.modelRegistry.hasConfiguredAuth(registered)) { if (session.modelRuntime.hasConfiguredAuth(model.provider)) {
this.authLossWarnings.delete(warningKey); this.authLossWarnings.delete(warningKey);
return; return;
} }
@@ -2182,7 +2182,7 @@ export class PiSessionService implements SessionRouteService {
private warningsForSession(session: PiAgentSession): SessionWarning[] { private warningsForSession(session: PiAgentSession): SessionWarning[] {
const runtime = this.active.get(session.sessionId)?.runtime; const runtime = this.active.get(session.sessionId)?.runtime;
const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime); const warnings = runtime === undefined ? [] : collectRuntimeWarnings(runtime);
const anthropic = anthropicSubscriptionWarning(session); const anthropic = anthropicSubscriptionWarning(session, join(this.agentDir, "auth.json"));
if (anthropic !== undefined) warnings.push(anthropic); if (anthropic !== undefined) warnings.push(anthropic);
return warnings; return warnings;
} }