| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527 |
- /** Agent activation, composition, and model-selection policy owned by API Session. */
- import { mkdir } from 'node:fs/promises'
- import type { Context } from '@deepseek-ai/cordis'
- import { installModelSelection } from '@deepseek-ai/dsh-agent'
- import type {
- Agent, AgentOptions, AgentSetup, ModelSelection as AgentModelSelection, ModelSelectionRef,
- } from '@deepseek-ai/dsh-agent'
- import type {} from '@deepseek-ai/dsh-agent-default-model'
- import type {} from '@deepseek-ai/dsh-agent-presets'
- import { ReasoningEffortId } from '@deepseek-ai/dsh-llm'
- import type { Session, SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
- import { SessionQueryError, type SessionObservation } from '@deepseek-ai/dsh-session-query'
- import { TypertLookupFailure } from '@deepseek-ai/dsh-typert-protocol'
- import type {} from '@deepseek-ai/dsh-typert-registry'
- import type { ModelSelection, SessionError } from './types.ts'
- /** Cold Session identity absent from persistence. */
- export class ApiSessionNotFound extends Error {}
- /** Session identity whose lifecycle belongs to subagent routing. */
- export class ApiSessionSubagentOwnership extends Error {
- /** @param sessionId - identity reserved to subagent routing. */
- constructor(readonly sessionId: SessionId) {
- super(`session "${sessionId}" is a subagent session; use subagent delivery`)
- }
- }
- /** Explicit-id creation attempted to adopt a Session under another cwd. */
- export class ApiSessionCwdConflict extends Error {
- constructor(
- readonly sessionId: SessionId,
- readonly requestedCwd: string,
- readonly existingCwd: string | undefined,
- ) {
- super(
- existingCwd === undefined
- ? `session "${sessionId}" records no cwd and cannot be adopted for "${requestedCwd}"`
- : `session "${sessionId}" belongs to "${existingCwd}", not "${requestedCwd}"`,
- )
- }
- }
- /** Explicit-id creation attempted to adopt a Session under another preset. */
- export class ApiSessionPresetConflict extends Error {
- constructor(
- readonly sessionId: SessionId,
- readonly requestedPreset: string,
- readonly existingPreset: string | undefined,
- ) {
- super(
- existingPreset === undefined
- ? `session "${sessionId}" records no agent preset and cannot be adopted under "${requestedPreset}"`
- : `session "${sessionId}" runs agent preset "${existingPreset}", not "${requestedPreset}"`,
- )
- }
- }
- /** Failures produced while resolving one ordinary Session identity to its live Agent. */
- export type ApiSessionAgentError = Extract<
- SessionError,
- { readonly code: 'session-not-found' | 'agent-busy' | 'internal' }
- >
- /** Result of resolving one ordinary Session identity to its live Agent. */
- export type ApiSessionAgentResult =
- | { readonly agent: Agent }
- | { readonly error: ApiSessionAgentError }
- type InstalledSelection = ModelSelectionRef & {
- current: AgentModelSelection
- consume(provider: string, model: string, reasoningEffort: string | undefined): boolean
- }
- /**
- * Test whether generic Session routing must leave an identity to subagent routing.
- * @param ctx - Host context carrying the Agent ownership registry.
- * @param session - attached or live Session whose ownership is tested.
- * @param agent - live Agent when one exists for the Session.
- * @returns whether subagent routing owns the Session identity.
- */
- export function hasApiSessionSubagentOwner(
- ctx: Context,
- session: Pick<Session, 'header'>,
- agent: Agent | undefined,
- ): boolean {
- if (session.header.origin === 'subagent') return true
- const parentId = session.header.parentSession
- if (parentId === undefined || agent === undefined) return false
- const parent = ctx.agents.get(parentId)
- return parent !== undefined && ctx.agents.isOwnedBy(agent.id, parent)
- }
- /**
- * Build the stable caller-facing subagent ownership rejection.
- * @param sessionId - Session identity owned by subagent routing.
- * @returns a stable Session-domain failure.
- */
- export function apiSessionSubagentOwnershipError(sessionId: SessionId): ApiSessionAgentError {
- return {
- code: 'agent-busy',
- message: `session "${sessionId}" is owned by subagent routing`,
- details: { reason: 'use subagent delivery for this child session' },
- }
- }
- /**
- * Inspect one cold Session without repairing, resuming, or publishing it.
- * @param ctx - Host context carrying Session persistence.
- * @param sessionId - durable Session identity.
- * @param signal - optional cancellation for persistence reads.
- * @returns the persisted header and complete event prefix.
- */
- export async function inspectApiSession(
- ctx: Context,
- sessionId: SessionId,
- signal?: AbortSignal,
- ): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
- try {
- using observation = await ctx.sessionQuery.observeSession(sessionId, {
- ...(signal === undefined ? {} : { signal }),
- projectionMode: 'none',
- })
- if (observation.header.cwd === undefined) {
- throw new ApiSessionNotFound(`session "${sessionId}" not found`)
- }
- return { meta: observation.header, events: [...observation.events] }
- } catch (error: unknown) {
- if (error instanceof SessionQueryError
- && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
- throw new ApiSessionNotFound(`session "${sessionId}" not found`)
- }
- throw error
- }
- }
- /** Owns every operation that may create, resume, or configure a Web Agent. */
- export class ApiSessionAgentController {
- private readonly resumes = new Map<SessionId, Promise<Agent>>()
- private readonly creations = new Map<SessionId, Promise<Agent>>()
- private readonly selections = new WeakMap<Agent, InstalledSelection>()
- private readonly imageAdmissionChains = new WeakMap<Agent, Promise<void>>()
- /** @param ctx - Host context carrying Agent, model, persistence, and Typert services. */
- constructor(private readonly ctx: Context) {
- ctx.typert.lookups.configure('agent', async (sessionId: SessionId) => {
- const found = await this.resolveAgent(sessionId)
- if ('error' in found) throw new TypertLookupFailure(found.error)
- return found.agent
- })
- ctx.typert.lookups.configure('session', async (sessionId: SessionId) => {
- const found = await this.resolveAgent(sessionId)
- if ('error' in found) throw new TypertLookupFailure(found.error)
- return found.agent.session
- })
- ctx.typert.contexts.configureHost('agent', async (sessionId: SessionId) => {
- const found = await this.resolveAgent(sessionId)
- if ('error' in found) throw new TypertLookupFailure(found.error)
- return found.agent.ctx
- })
- }
- /**
- * Resolve or resume one ordinary Session, deduplicating concurrent resumes.
- * @param sessionId - ordinary Session identity.
- * @returns the live Agent or a stable Session-domain failure.
- */
- async resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult> {
- return this.resolve(sessionId)
- }
- /**
- * Resolve one ordinary Session from an already-retained exact observation.
- * @param observation - Host-owned observation whose preparation stays pinned through setup.
- * @returns the live Agent or a stable Session-domain failure.
- */
- async resolveObservedAgent(observation: SessionObservation): Promise<ApiSessionAgentResult> {
- return this.resolve(observation.header.id, observation)
- }
- private async resolve(
- sessionId: SessionId,
- observation?: SessionObservation,
- ): Promise<ApiSessionAgentResult> {
- const live = this.liveAgent(sessionId)
- if (live !== undefined) return live
- const attached = this.ctx.sessions.get(sessionId)
- if (attached !== undefined && hasApiSessionSubagentOwner(this.ctx, attached, undefined)) {
- return { error: apiSessionSubagentOwnershipError(sessionId) }
- }
- let resume = this.resumes.get(sessionId)
- if (resume === undefined) {
- resume = this.resume(sessionId, observation).finally(() => { this.resumes.delete(sessionId) })
- this.resumes.set(sessionId, resume)
- }
- try {
- return { agent: await resume }
- } catch (error: unknown) {
- if (error instanceof ApiSessionNotFound) {
- return {
- error: {
- code: 'session-not-found',
- message: error.message,
- details: { sessionId },
- },
- }
- }
- if (error instanceof ApiSessionSubagentOwnership) {
- return { error: apiSessionSubagentOwnershipError(error.sessionId) }
- }
- const raced = this.liveAgent(sessionId)
- if (raced !== undefined) return raced
- const racedSession = this.ctx.sessions.get(sessionId)
- if (racedSession !== undefined && hasApiSessionSubagentOwner(this.ctx, racedSession, undefined)) {
- return { error: apiSessionSubagentOwnershipError(sessionId) }
- }
- return {
- error: {
- code: 'internal',
- message: `resume failed for session "${sessionId}": ${String(error)}`,
- details: {},
- },
- }
- }
- }
- /**
- * Resolve one requested identity, creating or resuming it once.
- * @param sessionId - requested Session identity.
- * @param cwd - directory the Session must own.
- * @param checkPersistedIdentity - whether to inspect a cold identity before creation.
- * @param presetId - optional Agent preset the Session must own.
- * @returns the matching live ordinary Agent.
- */
- async ensureSession(
- sessionId: SessionId,
- cwd: string,
- checkPersistedIdentity: boolean,
- presetId?: string,
- ): Promise<Agent> {
- let creation = this.creations.get(sessionId)
- if (creation === undefined) {
- creation = this.createOrAdopt(sessionId, cwd, checkPersistedIdentity, presetId)
- .catch((error: unknown) => {
- const live = this.ctx.agents.get(sessionId)
- if (live !== undefined) {
- if (hasApiSessionSubagentOwner(this.ctx, live.session, live)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- return live
- }
- const attached = this.ctx.sessions.get(sessionId)
- if (attached !== undefined && hasApiSessionSubagentOwner(this.ctx, attached, undefined)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- throw error
- })
- .finally(() => { this.creations.delete(sessionId) })
- this.creations.set(sessionId, creation)
- }
- const agent = await creation
- if (hasApiSessionSubagentOwner(this.ctx, agent.session, agent)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- if (presetId !== undefined) {
- this.assertPresetUnchanged(sessionId, presetId, this.presetForSession(agent.session))
- }
- if (agent.session.header.cwd !== cwd) {
- throw new ApiSessionCwdConflict(sessionId, cwd, agent.session.header.cwd)
- }
- return agent
- }
- /**
- * Install or return the Session-local model selection used by prompt assembly.
- * @param agent - live Agent that owns the selection.
- * @returns the installed mutable selection reference.
- */
- selectionFor(agent: Agent): InstalledSelection {
- const installed = this.selections.get(agent)
- if (installed !== undefined) return installed
- const projectionState = this.ctx.sessionProjections.stateOf(agent.session, 'modelSelection')
- if (projectionState === undefined) {
- throw new Error('api-session: required modelSelection projection is not registered')
- }
- let picked = projectionState.pending === null
- ? undefined
- : agentModelSelection(projectionState.pending)
- const defaultModel = this.ctx.agentDefaultModel
- const selection: InstalledSelection = {
- get current(): AgentModelSelection {
- if (picked !== undefined) return picked
- const logged = agent.session.requestHeader()?.config
- if (logged === undefined) return defaultModel.currentSelection()
- return {
- provider: logged.provider,
- model: logged.model,
- ...(logged.reasoningEffort === undefined ? {} : { reasoningEffort: logged.reasoningEffort }),
- }
- },
- set current(next: AgentModelSelection) {
- picked = next
- },
- consume(provider: string, model: string, reasoningEffort: string | undefined): boolean {
- if (picked?.provider !== provider
- || picked.model !== model
- || picked.reasoningEffort !== reasoningEffort) return false
- picked = undefined
- return true
- },
- assembled: undefined,
- }
- installModelSelection(agent.ctx, selection)
- this.selections.set(agent, selection)
- return selection
- }
- /**
- * Commit and cache one validated selection for the next prompt assembly.
- * @param agent - live Agent that owns the selection.
- * @param selection - validated selection to record and apply.
- */
- selectForNextRequest(agent: Agent, selection: AgentModelSelection): void {
- agent.session.append('model/selection', selection)
- this.selectionFor(agent).current = selection
- }
- /**
- * Let a matching durable request header retire the execution cache.
- * @param agent - live Agent whose request was recorded.
- * @param provider - provider route used by the request.
- * @param model - provider-owned model used by the request.
- * @param reasoningEffort - adapter-owned effort used by the request.
- * @returns whether the pending selection was consumed.
- */
- consumeSelection(
- agent: Agent,
- provider: string,
- model: string,
- reasoningEffort: string | undefined,
- ): boolean {
- return this.selections.get(agent)?.consume(provider, model, reasoningEffort) ?? false
- }
- /**
- * Read the current Agent preset from the Session projection.
- * @param session - live Session whose projection state is available.
- * @returns the current preset, or undefined when the capability is absent.
- */
- presetForSession(session: Session): string | undefined {
- return this.ctx.sessionProjections.stateOf(session, 'agentPreset') ?? undefined
- }
- /**
- * Serialize image admission and model selection for one Agent.
- * @param agent - live Agent that owns the serialization chain.
- * @param operation - asynchronous operation admitted after prior work settles.
- * @returns the operation result or rejection.
- */
- serializeImageAdmission<Value>(agent: Agent, operation: () => Promise<Value>): Promise<Value> {
- const result = (this.imageAdmissionChains.get(agent) ?? Promise.resolve()).then(operation)
- this.imageAdmissionChains.set(agent, result.then(() => undefined, () => undefined))
- return result
- }
- /**
- * Resolve the preset id and pre-publication Agent setup for a create or resume.
- * @param presetId - requested preset or the configured default when omitted.
- * @returns the resolved preset identity and Agent setup callback.
- */
- async composeAgent(presetId: string | undefined): Promise<{
- readonly agentPreset?: string
- readonly setup: AgentSetup
- }> {
- const presets = this.ctx.get('agentPresets')
- if (presets === undefined) return { setup: (agentCtx) => { this.installSelection(agentCtx) } }
- const resolvedId = (await presets.resolve(presetId)).id
- return {
- agentPreset: resolvedId,
- setup: async (agentCtx) => {
- this.installSelection(agentCtx)
- await presets.mount(agentCtx, resolvedId)
- },
- }
- }
- private liveAgent(sessionId: SessionId): ApiSessionAgentResult | undefined {
- const agent = this.ctx.agents.get(sessionId)
- if (agent === undefined) return undefined
- return hasApiSessionSubagentOwner(this.ctx, agent.session, agent)
- ? { error: apiSessionSubagentOwnershipError(sessionId) }
- : { agent }
- }
- private async resume(sessionId: SessionId, supplied?: SessionObservation): Promise<Agent> {
- if (supplied !== undefined) return this.resumeObserved(sessionId, supplied)
- try {
- using observation = await this.ctx.sessionQuery.observeSession(sessionId)
- return await this.resumeObserved(sessionId, observation)
- } catch (error: unknown) {
- if (error instanceof SessionQueryError
- && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
- throw new ApiSessionNotFound(`session "${sessionId}" not found`)
- }
- throw error
- }
- }
- private async resumeObserved(
- sessionId: SessionId,
- observation: SessionObservation,
- ): Promise<Agent> {
- if (observation.header.id !== sessionId || observation.header.cwd === undefined) {
- throw new ApiSessionNotFound(`session "${sessionId}" not found`)
- }
- if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, undefined)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- const composition = await this.composeAgent(this.presetForObservation(observation))
- const published = this.ctx.sessions.get(sessionId)
- const live = this.ctx.agents.get(sessionId)
- if (published !== undefined && hasApiSessionSubagentOwner(this.ctx, published, live)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- return (await this.ctx.agents.resume({
- resumeSessionId: sessionId,
- agentOptions: this.agentOptions(),
- setup: composition.setup,
- })).agent
- }
- private async createOrAdopt(
- sessionId: SessionId,
- cwd: string,
- checkPersistedIdentity: boolean,
- presetId: string | undefined,
- ): Promise<Agent> {
- const attached = this.ctx.sessions.get(sessionId)
- const live = this.ctx.agents.get(sessionId)
- if (attached !== undefined && hasApiSessionSubagentOwner(this.ctx, attached, live)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- if (live !== undefined) return live
- if (checkPersistedIdentity) {
- try {
- using observation = await this.ctx.sessionQuery.observeSession(sessionId)
- if (hasApiSessionSubagentOwner(this.ctx, { header: observation.header }, undefined)) {
- throw new ApiSessionSubagentOwnership(sessionId)
- }
- if (observation.header.cwd !== cwd) {
- throw new ApiSessionCwdConflict(sessionId, cwd, observation.header.cwd)
- }
- const storedPreset = this.presetForObservation(observation)
- this.assertPresetUnchanged(sessionId, presetId, storedPreset)
- const composition = await this.composeAgent(storedPreset)
- return (await this.ctx.agents.resume({
- resumeSessionId: sessionId,
- agentOptions: this.agentOptions(),
- setup: composition.setup,
- })).agent
- } catch (error: unknown) {
- if (!(error instanceof SessionQueryError)
- || error.code !== 'SESSION_QUERY_SESSION_NOT_FOUND') throw error
- }
- }
- try {
- await mkdir(cwd, { recursive: true })
- } catch (error: unknown) {
- throw new Error(`failed to ensure project directory "${cwd}": ${String(error)}`, { cause: error })
- }
- const composition = await this.composeAgent(presetId)
- return (await this.ctx.agents.create({
- sessionId,
- agentOptions: this.agentOptions(),
- meta: {
- cwd,
- ...(composition.agentPreset === undefined ? {} : { agentPreset: composition.agentPreset }),
- },
- setup: composition.setup,
- })).agent
- }
- private agentOptions(): AgentOptions {
- const { provider, model } = this.ctx.agentDefaultModel.currentSelection()
- return { provider, model }
- }
- private installSelection(agentCtx: Context): void {
- const agent = agentCtx.agent
- if (agent === undefined) throw new Error('api-session: Agent setup has no scoped Agent')
- this.selectionFor(agent)
- }
- /**
- * Read the current Agent preset from an all-projections observation.
- * @param observation - exact Session observation carrying its projection snapshot.
- * @returns the current preset, or undefined when the capability is absent.
- */
- presetForObservation(observation: SessionObservation): string | undefined {
- if (observation.projections === undefined) {
- throw new Error('api-session: Agent activation requires a projected Session observation')
- }
- return observation.projections.values.agentPreset ?? undefined
- }
- private assertPresetUnchanged(
- sessionId: SessionId,
- requested: string | undefined,
- existing: string | undefined,
- ): void {
- if (requested === undefined || requested === existing) return
- throw new ApiSessionPresetConflict(sessionId, requested, existing)
- }
- }
- function agentModelSelection(selection: ModelSelection): AgentModelSelection {
- return {
- provider: selection.provider,
- model: selection.model,
- ...(selection.reasoningEffort === undefined
- ? {}
- : { reasoningEffort: ReasoningEffortId(selection.reasoningEffort) }),
- }
- }
|