import type { QueryEngineHandle, IteratorHandle, ProviderHandle, ToolRegistryHandle, TeamHandle, } from '@zseven-w/agent-native'; import type { ClientSideConnection } from '@agentclientprotocol/sdk'; import type { LayoutPhase } from './agent-tool-guard'; import { abortEngine, destroyIterator, destroyQueryEngine, destroyToolRegistry, destroyProvider, abortTeam, destroyTeam, } from '@zseven-w/agent-native'; export interface NativeAgentSession { type: 'native'; engine?: QueryEngineHandle; team?: TeamHandle; iter?: IteratorHandle; provider: ProviderHandle; tools?: ToolRegistryHandle; memberHandles?: Array<{ provider: ProviderHandle; tools: ToolRegistryHandle }>; createdAt: number; lastActivity: number; /** toolCallId → memberId — routes async tool results to the correct member engine. */ toolOwners: Map; /** toolCallId → tool name — used for session-level tool guards and state updates. */ toolNames: Map; /** memberId → role — used for delegation-time skill resolution. */ memberRoles: Map; /** Session-local layout progress for builtin single-agent guardrails. */ layoutPhase: LayoutPhase; layoutRootId: string | null; } export interface AcpAgentSession { type: 'acp'; acpSessionId: string; acpAgentId: string; connection: ClientSideConnection; createdAt: number; lastActivity: number; toolNames: Map; toolOwners: Map; layoutPhase: LayoutPhase; layoutRootId: string | null; } export type AgentSession = NativeAgentSession | AcpAgentSession; /** Create a native session with required defaults. */ export function createSession( fields: Omit< NativeAgentSession, 'type' | 'toolOwners' | 'toolNames' | 'memberRoles' | 'layoutPhase' | 'layoutRootId' > & Partial< Pick< NativeAgentSession, 'toolOwners' | 'toolNames' | 'memberRoles' | 'layoutPhase' | 'layoutRootId' > >, ): NativeAgentSession { return { type: 'native', ...fields, toolOwners: fields.toolOwners ?? new Map(), toolNames: fields.toolNames ?? new Map(), memberRoles: fields.memberRoles ?? new Map(), layoutPhase: fields.layoutPhase ?? 'idle', layoutRootId: fields.layoutRootId ?? null, }; } /** Create an ACP session with required defaults. */ export function createAcpSession(fields: { acpSessionId: string; acpAgentId: string; connection: ClientSideConnection; }): AcpAgentSession { return { type: 'acp', ...fields, createdAt: Date.now(), lastActivity: Date.now(), toolNames: new Map(), toolOwners: new Map(), layoutPhase: 'idle', layoutRootId: null, }; } export const agentSessions = new Map(); /** Mark a session as active so long-running external tool callbacks are not expired. */ export function touchSession(session: Pick, now = Date.now()): void { session.lastActivity = now; } /** Idempotent cleanup — nullifies handles after destroying to prevent double-free. */ export function cleanup(session: AgentSession): void { if (session.type === 'acp') return; // ACP connections managed by acp-connection-manager if (session.iter) { destroyIterator(session.iter); session.iter = undefined; } if (session.team) { abortTeam(session.team); destroyTeam(session.team); session.team = undefined; } if (session.engine) { destroyQueryEngine(session.engine); session.engine = undefined; } if (session.memberHandles) { for (const mh of session.memberHandles) { destroyToolRegistry(mh.tools); destroyProvider(mh.provider); } session.memberHandles = undefined; } if (session.tools) { destroyToolRegistry(session.tools); session.tools = undefined; } if (session.provider) { destroyProvider(session.provider); (session as any).provider = undefined; } } /** Abort a session — makes pending nextEvent resolve null. */ export function abortSession(session: AgentSession): void { if (session.type === 'acp') { try { (session.connection as any).cancel?.({ sessionId: session.acpSessionId }); } catch {} return; } if (session.team) abortTeam(session.team); else if (session.engine) abortEngine(session.engine); } // Cleanup stale sessions every 60s (5-minute TTL from last activity) setInterval(() => { try { const now = Date.now(); for (const [id, session] of agentSessions) { if (now - session.lastActivity > 5 * 60_000) { abortSession(session); cleanup(session); agentSessions.delete(id); } } } catch { /* ignore cleanup errors */ } }, 60_000);