kimi-code / packages /klient /src /core /facade /session.ts
SaylorTwift's picture
SaylorTwift HF Staff
Add files using upload-large-folder tool
4e23b01 verified
Raw
History Blame Contribute Delete
7.81 kB
/**
* 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<Record<string, McpServerConfig>>;
}
export interface SessionApprovalsFacade {
list(): Promise<readonly ApprovalRequest[]>;
decide(id: string, response: ApprovalResponse): Promise<void>;
}
export interface SessionQuestionsFacade {
list(): Promise<readonly QuestionRequest[]>;
answer(id: string, result: QuestionResult): Promise<void>;
dismiss(id: string): Promise<void>;
}
export interface SessionInteractionsFacade {
list(kind?: InteractionKind): Promise<readonly Interaction[]>;
respond(id: string, response: unknown): Promise<void>;
}
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<readonly SkillSummary[]>;
}
/**
* 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<SessionMeta>;
setTitle(title: string): Promise<void>;
/**
* 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<string | undefined>;
update(patch: SessionMetaPatch): Promise<void>;
setArchived(archived: boolean): Promise<void>;
status(): Promise<SessionStatus>;
close(): Promise<void>;
archive(): Promise<void>;
/** Re-materialize a closed session; `false` when it no longer exists. */
restore(opts?: SessionRestoreOptions): Promise<boolean>;
/** Permanently delete the session and its persisted data; throws when missing. */
delete(): Promise<void>;
fork(input?: { title?: string; metadata?: Record<string, unknown> }): Promise<SessionMeta>;
createChild(input?: { title?: string; metadata?: Record<string, unknown> }): Promise<SessionMeta>;
readonly approvals: SessionApprovalsFacade;
readonly questions: SessionQuestionsFacade;
readonly interactions: SessionInteractionsFacade;
readonly skills: SessionSkillsFacade;
/** Agent id → metadata for every agent registered in this session. */
agents(): Promise<Readonly<Record<string, AgentMeta>>>;
}
export function createSessionFacade(call: ScopedCaller, sessionId: string): SessionFacade {
const scope: ScopeRef = { sessionId };
const read = (): Promise<SessionMeta> =>
call(scope, 'sessionMetadata', 'read', []) as Promise<SessionMeta>;
const spawn = async (
method: 'fork' | 'createChild',
input: { title?: string; metadata?: Record<string, unknown> } = {},
): Promise<SessionMeta> => {
return call({}, 'sessionManager', method, [
{ sourceSessionId: sessionId, title: input.title, metadata: input.metadata },
]) as Promise<SessionMeta>;
};
return {
get: read,
setTitle: (title) => call(scope, 'sessionMetadata', 'setTitle', [title]) as Promise<void>,
generateTitle: (opts) =>
call(scope, 'sessionTitleService', 'generateTitle', [opts]) as Promise<
string | undefined
>,
update: (patch) => call(scope, 'sessionMetadata', 'update', [patch]) as Promise<void>,
setArchived: (archived) =>
call(scope, 'sessionMetadata', 'setArchived', [archived]) as Promise<void>,
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<void>,
archive: () => call({}, 'sessionManager', 'archive', [sessionId]) as Promise<void>,
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<void>,
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<void>,
},
questions: {
list: () =>
call(scope, 'sessionQuestionService', 'listPending', []) as Promise<
readonly QuestionRequest[]
>,
answer: (id, result) =>
call(scope, 'sessionQuestionService', 'answer', [id, result]) as Promise<void>,
dismiss: (id) => call(scope, 'sessionQuestionService', 'dismiss', [id]) as Promise<void>,
},
interactions: {
list: (kind) =>
call(scope, 'sessionInteractionService', 'listPending', [kind]) as Promise<
readonly Interaction[]
>,
respond: (id, response) =>
call(scope, 'sessionInteractionService', 'respond', [id, response]) as Promise<void>,
},
skills: {
list: () =>
call(scope, 'sessionSkillCatalog', 'list', []) as Promise<readonly SkillSummary[]>,
},
agents: async () => {
const meta = await read();
return meta.agents ?? {};
},
};
}