Archived
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:
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user