/** * Agent Manager - Manages Agent instances per session * * Each session gets its own Agent instance for true isolation * and concurrent processing across sessions. */ import { Agent, type AgentMessage, type AgentEvent, type ThinkingLevel } from '@earendil-works/pi-agent-core'; import type { AgentInstanceGateway } from './agent-instance-gateway.js'; import { type Config } from '../config/schema.js'; import { type EffectiveAgentProfile } from '../config/agent-profile.js'; import type { ModelManager } from './models/manager.js'; import type { SkillInstallToolOptions, SkillInstallToolResult, MarketplaceSkillInstallToolOptions, MarketplaceSkillInstallToolResult } from './tools/skill-install-tool.js'; import { type AgentCapabilityCatalogEntry, type AgentCapabilitySessionState } from './capabilities/index.js'; import type { GatewayClarifyRequestFn } from './tools/clarify-tool.js'; import type { ExtensionRegistryImpl as ExtensionRegistry } from '../extensions/index.js'; import type { MessageBus } from '../infra/bus/index.js'; import type { AutomationService } from '../automations/index.js'; import type { SessionStore } from '../session/store.js'; import type { NotesService } from '../notes/index.js'; import type { ProjectService } from '../projects/index.js'; import type { SessionContext } from './session/session-context.js'; import type { Skill, SkillDiagnostic, SkillMarkdownPreviewPayload, SkillRuntimeStatus } from './skills/types.js'; import { type SkillHubLockEntry } from './skills/hub-lock.js'; import type { MemoryManager } from './memory/manager.js'; import type { UserContextPlan } from './memory/context/types.js'; export interface SkillCatalogEntry { directoryId: string; name: string; description: string; category?: string; source: Skill['source']; origin: Skill['origin']['id']; path: string; managed: boolean; writable: boolean; /** User toggle in ~/.xopc/skills.json (`entries[name].enabled`). Default true. */ enabled: boolean; /** When true, skill is never injected into `` (SKILL.md frontmatter). */ disableModelInvocation: boolean; /** Hub install provenance when under ~/.xopc/skills and listed in skills-lock.json. */ hub?: SkillHubLockEntry; } export interface SkillCatalogRuntimeMeta { version: string; loadedAt: number; diagnostics: SkillDiagnostic[]; status: SkillRuntimeStatus; } export interface SkillCatalogSnapshot extends SkillCatalogRuntimeMeta { catalog: SkillCatalogEntry[]; } export type AgentSkillUnavailableReason = 'agent-denied' | 'disabled' | 'requirements-unmet' | 'model-invocation-disabled'; export interface AgentSkillAvailabilityEntry extends SkillCatalogEntry { availableForCurrentAgent: boolean; unavailableReason: AgentSkillUnavailableReason | null; } export interface AgentSkillAvailabilityPayload { agentId: string; version: string; loadedAt: number; diagnostics: SkillDiagnostic[]; status: SkillRuntimeStatus; defaultsAllowlist?: string[]; agentAllowlist?: string[]; effectiveAllowlist?: string[]; skills: AgentSkillAvailabilityEntry[]; } export interface AgentManagerConfig { workspace: string; model?: string; config?: Config; extensionRegistry?: ExtensionRegistry; endpointTools?: import('../endpoint-tools/index.js').EndpointToolRuntime; hookRunner?: import('../extensions/index.js').ExtensionHookRunner; bus: MessageBus; getCurrentContext: () => SessionContext | null; /** Session persistence (enables `session_search` when set). */ getSessionStore?: () => SessionStore; /** Clears per-session profile default on teardown. */ getModelManager?: () => ModelManager; thinkingLevel?: ThinkingLevel; reasoningLevel?: 'off' | 'on' | 'stream'; verboseLevel?: 'off' | 'on' | 'full'; gatewayClarify?: { requestClarification: GatewayClarifyRequestFn; }; /** Gateway: exposes AutomationService for the `automation` tool. */ getAutomationService?: () => AutomationService | undefined; getBrowserRecipeService?: () => import('../browser/recipes/index.js').BrowserRecipeService | undefined; /** Gateway: exposes first-class xopc product objects for the `xopc_use` tool. */ getNotesService?: () => NotesService | undefined; getProjectService?: () => ProjectService | undefined; getLocalAppService?: () => import('../local-apps/index.js').LocalAppService | undefined; dispatchTaskRuns?: () => void; /** Gateway: starts persisted workflow runs (dedicated chat session per run). */ getWorkflowRunService?: () => import('../workflows/service/workflow-run-service.types.js').WorkflowRunServiceLike | undefined; /** Runtime notification for UI/CLI shells that cache skill catalogs. */ onSkillsUpdated?: (payload: { reason: 'disk' | 'config'; }) => void; /** Install a managed skill from an explicit source and refresh runtime state. */ installSkillFromSource?: (opts: SkillInstallToolOptions) => Promise; /** Install a managed skill from Store or ClawHub and refresh runtime state. */ installSkillFromMarketplace?: (opts: MarketplaceSkillInstallToolOptions) => Promise; /** Dynamic workspace verification context injected into the system prompt when edits are pending. */ getSelfVerifyPromptContext?: (sessionKey: string, agentId?: string) => string; /** * Runtime trust override. Persistent "do not trust" entries still take precedence. */ isWorkspaceTrusted?: (workspaceDir: string) => boolean | null | undefined; } export interface AgentInstance { agent: Agent; sessionKey: string; createdAt: number; lastUsedAt: number; effectiveProfile: EffectiveAgentProfile; resolvedWorkspacePath: string; /** Tool names registered on this agent (for skill indexing / tool gating). */ registeredToolNames: string[]; /** Capability packs activated by explicit skills or UI entry points in this session. */ activeCapabilities: Map; activeProjectContext?: string; activeSelfVerifyContext?: string; /** Declared env var names from skill_view; exec_command reads values from process.env at spawn time. */ skillEnvPassthroughKeys: Set; } export interface PreparedSkillTurn { text: string; activatedCapabilityNames: string[]; } export declare class AgentManager implements AgentInstanceGateway { private agents; private config; private toolsFactory; private mergedConfig; private activeCapabilityNames; private buildActiveCapabilityContext; private resolveCapabilityCatalogForInstance; /** Default agent workspace (effective profile for `getDefaultAgentId`). */ private baseWorkspacePath; /** Per-session absolute markdown workspace when `SessionAgentConfig.workingDirectoryOverride` is set. */ private sessionWorkspaceOverrides; private defaultModel; private credentialCache; private credentialResolver; private workspaceRuntimes; private userContext; private backgroundReview; private skillFilesystemWatcher; private skillDiskRefreshInProgress; private skillDiskRefreshPending; private lastExplicitSkillDiskRefreshAt; private skillsUpdatedTimer; private pendingSkillsUpdatedReason; private projectTrustStore; constructor(config: AgentManagerConfig); private isUserContextEnabledForSession; private computeBaseWorkspacePath; /** * Workspace root for inbound attachments / side effects for this session's agent id. * Uses in-memory session workspace overrides when the session has a persisted `workingDirectoryOverride`. */ getResolvedWorkspaceForSession(sessionKey: string): string; private getWorkspaceRuntimeForSession; private getCurrentWorkspaceRuntime; /** * Sync in-memory workspace override from session config (after load or PATCH). * Pass `null` to clear when the session has no `workingDirectoryOverride` on disk. */ setSessionWorkspaceOverride(sessionKey: string, absolutePath: string | null): void; /** Merged `thinkingDefault` for this session's agent id (defaults + `agents.list`). */ getThinkingDefaultForSession(sessionKey: string): import('./transcript/thinking-types.js').ThinkLevel | undefined; private pickDefaultModelRef; private resolveModelStringToModel; /** * Keep defaults in sync when config is hot-reloaded or saved from the UI. * * The previous implementation rebuilt the entire `AgentToolsFactory` (80+ lines * of dependency wiring) on every reload. The factory's deps are now built from * a single helper ({@link buildToolsFactoryDeps}) and read `this.*` through * closures, so existing instances automatically see the new config without * reconstruction. The browser is still shut down because its cached settings * (headless mode, backend choice) come from the config snapshot at connect time. */ updateAgentDefaults(config: Config): void; /** * Construct the dep bag passed to `AgentToolsFactory`. Closures reference * `this.*` so they remain valid across hot reloads (no rebuild needed). */ private buildToolsFactoryDeps; getMemoryManager(): MemoryManager; getMemoryManagerForSession(sessionKey: string): MemoryManager; /** Build the bounded, policy-filtered context used for this model turn. */ prepareUserTurnContext(userMessage: AgentMessage, sessionKey: string, turnId: string): Promise; /** * After a completed turn: sync external providers and queue next-turn prefetch. * Delegates to {@link UserContextCoordinator}. */ afterAgentTurn(sessionKey: string, userPlainText: string): Promise; /** * Call once per user turn before the main embedded agent turn. * Delegates to {@link BackgroundReviewCoordinator}. */ beginBackgroundReviewUserTurn(sessionKey: string): void; /** * After a successful main turn (after memory sync via `afterAgentTurn`), may run a quiet follow-up for memory/skills. * Delegates to {@link BackgroundReviewCoordinator}. */ scheduleBackgroundReviewAfterUserTurn(sessionKey: string): void; /** * Expand `/skill:name` user text into the full skill block for the current turn (WebChat, channels). */ expandSkillUserText(text: string): string; prepareSkillTurn(sessionKey: string, text: string): PreparedSkillTurn; withSkillCapabilities(sessionKey: string, capabilityNames: readonly string[], run: () => Promise): Promise; private buildSystemPromptForInstance; /** Structured SKILL.md preview for the gateway console. */ getSkillMarkdownSource(skillName: string): SkillMarkdownPreviewPayload | null; private isProjectWorkspaceTrusted; private resolveContextFilesForSession; private skillCatalogEntryFromSkill; getSkillCatalog(): SkillCatalogEntry[]; getSkillCatalogSnapshot(): SkillCatalogSnapshot; getAgentSkillAvailability(agentId: string): AgentSkillAvailabilityPayload; /** * After ~/.xopc/skills.json changes (enable/disable), refresh `` on active agents. */ refreshSkillsAfterSkillConfigChange(): void; refreshActionTrustPolicy(): void; refreshUserProfileContext(): void; /** * Reload skills from disk and refresh system prompt on all active Agent instances. */ refreshSkillsAfterDiskChange(source?: 'explicit' | 'watch' | 'trust'): void; refreshSkillsAfterTrustChange(): void; private scheduleSkillsUpdated; private applySkillsAfterDiskChange; /** * Get or create an Agent instance for a session */ getOrCreateAgent(sessionKey: string): Agent; /** * Get existing agent for a session (if any) */ getAgent(sessionKey: string): Agent | undefined; /** * Check if an agent exists for a session */ hasAgent(sessionKey: string): boolean; /** * Remove an agent instance */ removeAgent(sessionKey: string): boolean; /** * Get all active session keys */ getActiveSessions(): string[]; /** * Get agent count */ getAgentCount(): number; /** * Merge per-turn channel system prompt (e.g. Telegram group/topic override) into the agent. */ applyTurnChannelSystemPrompt(sessionKey: string, channelSystemPrompt: string): void; /** * Set thinking level for a session's agent */ setThinkingLevel(sessionKey: string, level: ThinkingLevel): void; /** * Dispose all agents */ dispose(): void; warmCredentialCache(): Promise; refreshCredentials(): Promise; private resolveApiKeyWithCache; private getSelfVerifyPromptContext; private composeDynamicProjectContext; getCapabilityCatalogForSession(sessionKey?: string): AgentCapabilityCatalogEntry[]; private createAgentForProfile; private resolveToolPolicy; private requestToolConfirmation; private refreshActiveProjectContextIfChanged; /** * Set model for a specific session */ setModelForSession(sessionKey: string, modelId: string): boolean; /** * Get last assistant content from a session's agent */ getLastAssistantContent(sessionKey: string): string | null; /** * Replace messages for a session's agent */ replaceMessages(sessionKey: string, messages: AgentMessage[]): boolean; /** * Get messages for a session's agent */ getMessages(sessionKey: string): AgentMessage[] | null; /** * Subscribe to agent events for a session */ subscribeToSession(sessionKey: string, callback: (event: AgentEvent) => void): (() => void) | null; }