/** * The session facade — one `klient.session(id)` handle aggregating the * session-scope services (metadata, activity, approvals, questions, * interactions) plus the app-scope lifecycle service for close/archive/ * restore/delete/fork/createChild. `agents()` reads the metadata registry (agent * handles are not serializable, so no agent-lifecycle channel exists on the * wire). */ import type { ApprovalRequest, ApprovalResponse, } from '@moonshot-ai/agent-core-v2/agent/interaction/approval'; import type { Interaction, InteractionKind, } from '@moonshot-ai/agent-core-v2/human/interaction/interaction'; import type { QuestionRequest, QuestionResult, } from '@moonshot-ai/agent-core-v2/agent/interaction/question'; import type { AgentMeta, SessionMeta, SessionMetaPatch, } from '@moonshot-ai/agent-core-v2/session/sessionMetadata/sessionMetadata'; import type { SkillSummary } from '@moonshot-ai/agent-core-v2/features/skill/catalog/types'; import type { ScopeRef } from '../channel.js'; import type { McpServerConfig } from '../../contract/mcp.js'; import type { ScopedCaller } from './global.js'; export type { ScopedCaller } from './global.js'; /** What `sessionLifecycleService.create` and `sessionManager.restore` leave on the wire. */ interface HandleWire { readonly id: string; } /** * Options for `SessionFacade.restore` — mirrors the engine's * `ResumeSessionOptions`. `mcpServers` injects ephemeral per-session MCP * servers when restore re-materializes a cold session (ignored when the * session is already live). */ export interface SessionRestoreOptions { readonly additionalDirs?: readonly string[]; readonly mcpServers?: Readonly>; } export interface SessionApprovalsFacade { list(): Promise; decide(id: string, response: ApprovalResponse): Promise; } export interface SessionQuestionsFacade { list(): Promise; answer(id: string, result: QuestionResult): Promise; dismiss(id: string): Promise; } export interface SessionInteractionsFacade { list(kind?: InteractionKind): Promise; respond(id: string, response: unknown): Promise; } export interface SessionSkillsFacade { /** * Every skill in the session-merged catalog as a plain summary (the * catalog's readiness is resolved engine-side). Subscribe to * `session.events` `'skills.changed'` for updates. */ list(): Promise; } /** * Derived session lifecycle phase. The facade reads the engine's session * activity view (busy + pending interaction) and maps it onto the v1 * precedence: pending approvals and questions first, then busy, then idle. */ export type SessionStatus = 'running' | 'idle' | 'awaiting_approval' | 'awaiting_question'; export interface SessionFacade { get(): Promise; setTitle(title: string): Promise; /** * Generate and apply a title from the main agent's first prompts via the * managed `chat_title` tool. `undefined` when generation is unavailable * (no managed OAuth login, no prompt yet, or a custom title is set). * `force` regenerates anyway, overwriting a generated or custom title. * `source` picks the conversation excerpt: `user_prompts` (default), * `first_turn` (opening prompt + first reply; strict), or `digest` * (head+tail of a multi-turn conversation). */ generateTitle(opts?: { force?: boolean; source?: 'user_prompts' | 'first_turn' | 'digest'; }): Promise; update(patch: SessionMetaPatch): Promise; setArchived(archived: boolean): Promise; status(): Promise; close(): Promise; archive(): Promise; /** Re-materialize a closed session; `false` when it no longer exists. */ restore(opts?: SessionRestoreOptions): Promise; /** Permanently delete the session and its persisted data; throws when missing. */ delete(): Promise; fork(input?: { title?: string; metadata?: Record }): Promise; createChild(input?: { title?: string; metadata?: Record }): Promise; readonly approvals: SessionApprovalsFacade; readonly questions: SessionQuestionsFacade; readonly interactions: SessionInteractionsFacade; readonly skills: SessionSkillsFacade; /** Agent id → metadata for every agent registered in this session. */ agents(): Promise>>; } export function createSessionFacade(call: ScopedCaller, sessionId: string): SessionFacade { const scope: ScopeRef = { sessionId }; const read = (): Promise => call(scope, 'sessionMetadata', 'read', []) as Promise; const spawn = async ( method: 'fork' | 'createChild', input: { title?: string; metadata?: Record } = {}, ): Promise => { return call({}, 'sessionManager', method, [ { sourceSessionId: sessionId, title: input.title, metadata: input.metadata }, ]) as Promise; }; return { get: read, setTitle: (title) => call(scope, 'sessionMetadata', 'setTitle', [title]) as Promise, generateTitle: (opts) => call(scope, 'sessionTitleService', 'generateTitle', [opts]) as Promise< string | undefined >, update: (patch) => call(scope, 'sessionMetadata', 'update', [patch]) as Promise, setArchived: (archived) => call(scope, 'sessionMetadata', 'setArchived', [archived]) as Promise, status: async () => { const activity = (await call(scope, 'sessionActivityView', 'state', [])) as { readonly busy: boolean; readonly pendingInteraction: 'none' | 'approval' | 'question'; }; if (activity.pendingInteraction === 'approval') return 'awaiting_approval'; if (activity.pendingInteraction === 'question') return 'awaiting_question'; return activity.busy ? 'running' : 'idle'; }, close: () => call({}, 'sessionManager', 'close', [sessionId]) as Promise, archive: () => call({}, 'sessionManager', 'archive', [sessionId]) as Promise, restore: async (opts) => { const handle = (await call({}, 'sessionManager', 'restore', [sessionId, opts])) as HandleWire | null; return handle !== null && handle !== undefined; }, delete: () => call({}, 'sessionManager', 'delete', [sessionId]) as Promise, fork: (input) => spawn('fork', input), createChild: (input) => spawn('createChild', input), approvals: { list: () => call(scope, 'sessionApprovalService', 'listPending', []) as Promise< readonly ApprovalRequest[] >, decide: (id, response) => call(scope, 'sessionApprovalService', 'decide', [id, response]) as Promise, }, questions: { list: () => call(scope, 'sessionQuestionService', 'listPending', []) as Promise< readonly QuestionRequest[] >, answer: (id, result) => call(scope, 'sessionQuestionService', 'answer', [id, result]) as Promise, dismiss: (id) => call(scope, 'sessionQuestionService', 'dismiss', [id]) as Promise, }, interactions: { list: (kind) => call(scope, 'sessionInteractionService', 'listPending', [kind]) as Promise< readonly Interaction[] >, respond: (id, response) => call(scope, 'sessionInteractionService', 'respond', [id, response]) as Promise, }, skills: { list: () => call(scope, 'sessionSkillCatalog', 'list', []) as Promise, }, agents: async () => { const meta = await read(); return meta.agents ?? {}; }, }; }