mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-08-29 04:26:38 +00:00
315 lines
11 KiB
TypeScript
315 lines
11 KiB
TypeScript
/** Session Remote owner: cold reads, explicit Agent commands, and live control state. */
|
|
|
|
import { Context } from '@deepseek-ai/cordis'
|
|
import z from '@deepseek-ai/schemastery'
|
|
import { errorChain } from '@deepseek-ai/dsh-llm'
|
|
import type { SessionEvent, SessionHeader, SessionId } from '@deepseek-ai/dsh-session'
|
|
import type { SessionObservation } from '@deepseek-ai/dsh-session-query'
|
|
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
|
import {
|
|
ApiSessionAgentController,
|
|
inspectApiSession,
|
|
type ApiSessionAgentResult,
|
|
} from './agent.ts'
|
|
import { SessionCommandController } from './commands.ts'
|
|
import { SessionControlController } from './control.ts'
|
|
import { SessionHistoryController } from './history.ts'
|
|
import { ApiSessionList, DEFAULT_COLD_BLANK_PROBE_MAX_BYTES } from './list.ts'
|
|
import { installModelSelectionProjection } from './model-selection-projection.ts'
|
|
import type {
|
|
SessionAttachmentRequest,
|
|
SessionAttachmentValue,
|
|
SessionCancelRequest,
|
|
SessionCancelValue,
|
|
SessionControlFrame,
|
|
SessionCreateRequest,
|
|
SessionCreateValue,
|
|
SessionFollowFrame,
|
|
SessionFollowRequest,
|
|
SessionForkRequest,
|
|
SessionForkValue,
|
|
SessionListRequest,
|
|
SessionListValue,
|
|
SessionPage,
|
|
SessionPageRequest,
|
|
SessionPromptRequest,
|
|
SessionPromptValue,
|
|
SessionRenameRequest,
|
|
SessionRenameValue,
|
|
SessionSearchRequest,
|
|
SessionSearchValue,
|
|
SessionSelectModelRequest,
|
|
SessionSelectModelValue,
|
|
SessionUpdateQueueRequest,
|
|
SessionUpdateQueueValue,
|
|
} from './types.ts'
|
|
|
|
export type * from './types.ts'
|
|
export { ApiSessionNotFound } from './agent.ts'
|
|
|
|
declare module '@deepseek-ai/cordis' {
|
|
interface Context {
|
|
/** Host Session business API and Remote namespace owner. */
|
|
sessionController: SessionController
|
|
}
|
|
}
|
|
|
|
/** Session Controller deployment policy. */
|
|
export interface Config {
|
|
/** Maximum cold Session artifact size eligible for one full projection observation. */
|
|
readonly coldBlankProbeMaxBytes?: number
|
|
}
|
|
|
|
/** Host service backing the generated `ctx.remote.session` namespace. */
|
|
export class SessionController extends TypertRemoteService {
|
|
static inject = [
|
|
'agentDefaultModel',
|
|
'agents',
|
|
'attachments',
|
|
'llm',
|
|
'sessions',
|
|
'sessionProjections',
|
|
'sessionQuery',
|
|
'typert',
|
|
'workspaceRegistry',
|
|
]
|
|
|
|
static Config: z<Config> = z.object({
|
|
coldBlankProbeMaxBytes: z.natural().default(DEFAULT_COLD_BLANK_PROBE_MAX_BYTES),
|
|
})
|
|
|
|
private readonly agents: ApiSessionAgentController
|
|
private readonly commands: SessionCommandController
|
|
private readonly controlState: SessionControlController
|
|
private readonly history: SessionHistoryController
|
|
private readonly listState: ApiSessionList
|
|
private readonly promotions = new Set<Promise<void>>()
|
|
|
|
/**
|
|
* @param ctx - Host context containing the Session capability assembly.
|
|
* @param config - cold-list observation policy.
|
|
*/
|
|
constructor(ctx: Context, config: Config) {
|
|
super(ctx, 'sessionController', { namespace: 'session' })
|
|
installModelSelectionProjection(ctx)
|
|
this.agents = new ApiSessionAgentController(ctx)
|
|
this.commands = new SessionCommandController(ctx, this.agents, process.cwd())
|
|
this.controlState = new SessionControlController(ctx)
|
|
// Registered before history so reverse-order teardown closes every
|
|
// follower before waiting for already-admitted promotions.
|
|
ctx.effect(() => async () => {
|
|
await Promise.allSettled([...this.promotions])
|
|
}, 'session-controller.promotions')
|
|
this.history = new SessionHistoryController(ctx, (observation) => { this.promote(observation) })
|
|
this.listState = new ApiSessionList(
|
|
ctx,
|
|
config.coldBlankProbeMaxBytes ?? DEFAULT_COLD_BLANK_PROBE_MAX_BYTES,
|
|
)
|
|
|
|
ctx.on('session/created', (session) => {
|
|
ctx.emit('api-session/added', this.listState.summaryFor(session))
|
|
})
|
|
ctx.on('session/disposed', (session) => {
|
|
ctx.emit('api-session/removed', session.id)
|
|
})
|
|
ctx.on('agent/status', ({ agent, status }) => {
|
|
ctx.emit('api-session/status', agent.id, status === 'running')
|
|
})
|
|
ctx.on('agent/error', ({ agent, error }) => {
|
|
ctx.emit('api-session/error', agent.id, errorChain(error))
|
|
})
|
|
ctx.on('session/event', (session, event) => {
|
|
if (event.type === 'request/header') {
|
|
const agent = ctx.agents.get(session.id)
|
|
if (agent?.session === session) this.agents.consumeSelection(
|
|
agent,
|
|
event.data.header.config.provider,
|
|
event.data.header.config.model,
|
|
event.data.header.config.reasoningEffort,
|
|
)
|
|
}
|
|
if (event.type !== 'user/message' || event.data.source.kind !== 'user') return
|
|
ctx.emit('api-session/activity', session.id, event.time)
|
|
})
|
|
}
|
|
|
|
private promote(observation: SessionObservation): void {
|
|
const sessionId = observation.header.id
|
|
const task = (async () => {
|
|
using ownedObservation = observation
|
|
const result = await this.agents.resolveObservedAgent(ownedObservation)
|
|
if ('error' in result) this.ctx.emit('api-session/error', sessionId, result.error.message)
|
|
})().catch((error: unknown) => {
|
|
this.ctx.logger.error(`session-controller: background activation for "${sessionId}" failed: ${errorChain(error)}`)
|
|
})
|
|
this.promotions.add(task)
|
|
void task.finally(() => { this.promotions.delete(task) })
|
|
}
|
|
|
|
/**
|
|
* Resolve or resume one ordinary Session for another Host API domain.
|
|
* @param sessionId - Session identity whose Agent owns the operation.
|
|
* @returns the live Agent or the stable Session-domain failure.
|
|
*/
|
|
resolveAgent(sessionId: SessionId): Promise<ApiSessionAgentResult> {
|
|
return this.agents.resolveAgent(sessionId)
|
|
}
|
|
|
|
/**
|
|
* Inspect one attached or persisted Session without activating its Agent.
|
|
* @param sessionId - durable Session identity.
|
|
* @param signal - optional caller cancellation for persistence reads.
|
|
* @returns the current attached state or persisted header and event prefix.
|
|
*/
|
|
inspect(
|
|
sessionId: SessionId,
|
|
signal?: AbortSignal,
|
|
): Promise<{ meta: SessionHeader; events: SessionEvent[] }> {
|
|
const attached = this.ctx.sessions.get(sessionId)
|
|
if (attached !== undefined) {
|
|
return Promise.resolve({ meta: attached.header, events: [...attached.events] })
|
|
}
|
|
return inspectApiSession(this.ctx, sessionId, signal)
|
|
}
|
|
|
|
/**
|
|
* Read all visible Session rows without resuming an Agent.
|
|
* @param _request - reserved empty list request.
|
|
* @param signal - cancellation for persistence reads.
|
|
* @returns visible Session summaries ordered by activity.
|
|
*/
|
|
@Remote('list')
|
|
async list(_request: SessionListRequest, signal: AbortSignal): Promise<SessionListValue> {
|
|
return { items: await this.listState.list(signal) }
|
|
}
|
|
|
|
/**
|
|
* Search visible Session content without resuming an Agent.
|
|
* @param request - literal message-content query.
|
|
* @param signal - cancellation for list and search reads.
|
|
* @returns authorized bounded Session search results.
|
|
*/
|
|
@Remote('search')
|
|
search(request: SessionSearchRequest, signal: AbortSignal): Promise<SessionSearchValue> {
|
|
return this.listState.search(request.query, signal)
|
|
}
|
|
|
|
/**
|
|
* Create or idempotently adopt one ordinary Session.
|
|
* @param request - requested identity, location, and Agent preset.
|
|
* @returns the Session identity and resolved preset when configured.
|
|
*/
|
|
@Remote('create')
|
|
create(request: SessionCreateRequest): Promise<SessionCreateValue> {
|
|
return this.commands.create(request)
|
|
}
|
|
|
|
/**
|
|
* Select one Session-local model after explicitly resuming the Session.
|
|
* @param request - Session identity and requested model selection.
|
|
* @returns the normalized selection installed for the Session.
|
|
*/
|
|
@Remote('selectModel')
|
|
selectModel(request: SessionSelectModelRequest): Promise<SessionSelectModelValue> {
|
|
return this.commands.selectModel(request)
|
|
}
|
|
|
|
/**
|
|
* Rename one Session after explicitly resuming it.
|
|
* @param request - Session identity and proposed title.
|
|
* @returns the accepted title and durable event sequence.
|
|
*/
|
|
@Remote('rename')
|
|
rename(request: SessionRenameRequest): Promise<SessionRenameValue> {
|
|
return this.commands.rename(request)
|
|
}
|
|
|
|
/**
|
|
* Fork one cold-readable completed-turn prefix into a new Session.
|
|
* @param request - source Session and optional event anchor.
|
|
* @returns the new Session identity.
|
|
*/
|
|
@Remote('fork')
|
|
fork(request: SessionForkRequest): Promise<SessionForkValue> {
|
|
return this.commands.fork(request)
|
|
}
|
|
|
|
/**
|
|
* Admit one prompt after explicitly resuming its Session.
|
|
* @param request - Session identity, prompt content, source metadata, and delivery mode.
|
|
* @param signal - caller cancellation before prompt admission begins.
|
|
* @returns acknowledgement that the Agent accepted the prompt.
|
|
*/
|
|
@Remote('prompt')
|
|
prompt(request: SessionPromptRequest, signal: AbortSignal): Promise<SessionPromptValue> {
|
|
signal.throwIfAborted()
|
|
return this.commands.prompt(request)
|
|
}
|
|
|
|
/**
|
|
* Read one image proven reachable from the addressed Session log.
|
|
* @param request - Session and attachment identities used for authorization.
|
|
* @returns the durable attachment reference and base64-encoded bytes.
|
|
*/
|
|
@Remote('attachment')
|
|
attachment(request: SessionAttachmentRequest): Promise<SessionAttachmentValue> {
|
|
return this.commands.attachment(request)
|
|
}
|
|
|
|
/**
|
|
* Mutate one still-pending queue occurrence on a live Agent.
|
|
* @param request - Session, queue item, and requested mutation.
|
|
* @returns acknowledgement that the queue mutation was applied.
|
|
*/
|
|
@Remote('updateQueue')
|
|
updateQueue(request: SessionUpdateQueueRequest): SessionUpdateQueueValue {
|
|
return this.commands.updateQueue(request)
|
|
}
|
|
|
|
/**
|
|
* Cancel one active Agent turn without dropping its pending inbox.
|
|
* @param request - Session whose active Agent turn is cancelled.
|
|
* @returns acknowledgement that cancellation was requested.
|
|
*/
|
|
@Remote('cancel')
|
|
cancel(request: SessionCancelRequest): SessionCancelValue {
|
|
return this.commands.cancel(request)
|
|
}
|
|
|
|
/**
|
|
* Read one cold-safe, message-aligned Session history page.
|
|
* @param request - durable address, backward cursor, and page budget.
|
|
* @param signal - cancellation for persistence reads.
|
|
* @returns one chronological page.
|
|
*/
|
|
@Remote('page')
|
|
page(request: SessionPageRequest, signal: AbortSignal): Promise<SessionPage> {
|
|
return this.history.page(request, signal)
|
|
}
|
|
|
|
/**
|
|
* Follow one Session log from its opening or resume cursor.
|
|
* @param request - durable address and last committed sequence already held by the caller.
|
|
* @param signal - cancellation owned by the Remote stream carrier.
|
|
* @returns a complete opening snapshot followed by gap-free event frames.
|
|
*/
|
|
@Remote({ mode: 'stream' })
|
|
follow(request: SessionFollowRequest, signal: AbortSignal): AsyncIterable<SessionFollowFrame> {
|
|
return this.history.follow(request, signal)
|
|
}
|
|
|
|
/**
|
|
* Stream a complete live-control baseline followed by replacement frames.
|
|
* @param signal - cancellation owned by the Remote stream carrier.
|
|
* @returns one complete baseline followed by live replacement frames.
|
|
*/
|
|
@Remote({ mode: 'stream' })
|
|
control(signal: AbortSignal): AsyncIterable<SessionControlFrame> {
|
|
return this.controlState.control(signal)
|
|
}
|
|
|
|
}
|
|
|
|
export { buildModelCatalog } from './catalog.ts'
|
|
export default SessionController
|