Files
deepseek-harness/packages/host/apiproxy/src/api/sessions.ts
imccyu 19336686a6 feat(apiproxy): session.rename RPC delegating to the session-title service
New unary method in RpcMethodMap with the title-invalid error code; the impl
resolves the agent (cold sessions resume first) and delegates to
ctx.sessionTitle.rename, returning the normalized title plus its event seq so
clients settle the title projection cell ahead of the push frame. session.fork
stays on the reserved-seam list.
2026-07-29 18:59:23 +08:00

236 lines
10 KiB
TypeScript

/**
* sessions domain contract. Method signatures are the source of truth:
* unary methods take the RpcRequest<P> narrow form and the impl echoes rpcId; everything
* else references RequestPayload<'session.*'> / ResponseValue<'session.*'>.
*/
import type { ContentBlock } from '@deepseek-ai/dsh-llm/types'
import type { SessionEvent, SessionId } from '@deepseek-ai/dsh-session/types'
// The pure-type outlet: api/ is browser-importable, and the package root's
// cordis Context merge (via dsh-agent) must not enter client aggregates.
import type { SessionProjectionMap } from '@deepseek-ai/dsh-session-projection/types'
import type { RpcId, RpcRequest, RpcResponse } from './rpc.ts'
import type { ToolEventView } from './events.ts'
import type { WorkspaceId } from './workspace.ts'
declare module '@deepseek-ai/dsh-llm' {
interface MessageSourceMap {
/**
* The prompt's rpcId is passed through MessageSource into the `user/message` event
* (the client uses it to reconcile the optimistically
* echoed provisional message with the event stream). kind stays `'user'` — the model face
* carries no transport vocabulary; rpcId is an extra durable-JSON field passed back to the client with the event.
*/
'user-rpc': { kind: 'user'; rpcId: RpcId }
}
}
/**
* One history page entry: the raw event plus the optional host-computed render
* intent (same semantics as the mux frame's `view` slot — a pagination-time
* derivation, never persisted).
*/
export interface HistoryEntry {
event: SessionEvent
view?: ToolEventView
}
/**
* The projection baseline riding the history tail page: one synchronous cut
* over every registered projection unit, read from the registry's watermark
* cache. `asOfSeq` is the seq of the last committed event every value
* reflects — the window tail event seq (`-1` for an empty log, mirroring
* `session/subscribed.lastSeq`), directly comparable with
* `session/projection` frame seqs under the client's higher-seq-wins rule. A
* key absent from `values` means the capability is absent (its domain plugin
* is unmounted).
*/
export interface SessionProjectionsBlock {
/** Seq of the last event the values reflect; -1 for an empty log. */
asOfSeq: number
/** Whole current value per registered projection key. */
values: Partial<SessionProjectionMap>
}
/** Complete model target selected for one session. */
export interface ModelTarget {
/** Registered provider route. */
provider: string
/** Provider-owned model id. */
model: string
/** Adapter-owned reasoning effort; absence preserves adapter/provider default behavior. */
reasoningEffort?: string
}
/** One adapter-owned reasoning effort displayed for an exact model route. */
export interface ModelReasoningEffort {
/** Opaque value submitted back to the owning adapter. */
id: string
/** Adapter-supplied display name. */
name: string
/** Optional adapter-supplied description. */
description?: string
}
/** Selectable reasoning metadata for one exact model route. */
export interface ModelReasoning {
/** Efforts in adapter-preferred display order. */
efforts: ModelReasoningEffort[]
/** Adapter-configured default; absence preserves the provider default. */
defaultEffort?: string
}
/** One model displayed inside its provider group. */
export interface ModelCatalogModel {
/** Provider-owned model id. */
id: string
/** Provider-supplied display name. */
name: string
/** Optional provider-supplied description. */
description?: string
/** The current model was inserted because the advisory catalog omitted it. */
unlisted?: true
/** Exact-route reasoning metadata when the adapter exposes it. */
reasoning?: ModelReasoning
}
/** One provider and the models it advertised successfully. */
export interface ModelProviderGroup {
/** Provider route id used for requests. */
id: string
/** Provider display name. */
name: string
/** Models in provider-preferred order. */
models: ModelCatalogModel[]
}
/** A provider whose asynchronous catalog lookup failed. */
export interface ModelCatalogFailure {
/** Provider route id. */
id: string
/** Provider display name. */
name: string
/** Lookup failure diagnostic. */
message: string
}
/** Detached model-directory snapshot for one session. */
export interface SessionModels {
/** Target selected for the session's next assembled step. */
current: ModelTarget
/** Successfully loaded provider groups. */
groups: ModelProviderGroup[]
/** Provider-local failures; successful groups remain usable. */
failures: ModelCatalogFailure[]
}
/** Session list entry (v1 builds no index: list does readdir+stat). */
export interface SessionSummary {
sessionId: SessionId
/** Persisted file mtime. */
updatedAt: number
/** Status of the attached agent; always false for cold (unattached) sessions. */
running: boolean
/**
* Derived conversation-not-started bit: true while no turn has run (no
* prompt was accepted yet). Standalone plugin events — command lifecycle
* records, plan/mode, titles, goals — do not open a turn and therefore do
* not clear it. Clients hide blank sessions from lists and reuse them for
* New Session on the same workspace. Always false for cold sessions —
* lazy persistence keeps a never-appended session out of the store, and a
* listed cold session's log holds its turns.
*/
blank: boolean
/** fork/spawn lineage (session.header.parentSession passthrough); absent for root sessions. */
parentSessionId?: SessionId
/** Session working directory (header.cwd passthrough); absent when unrecorded. */
cwd?: string
/**
* Projection baseline for this row, with zero log loads: attached sessions
* read the registry's live watermark cut; cold sessions read the persisted
* projection cache's stored rows — as stale as that session's last durable
* checkpoint (`asOfSeq` says exactly how stale), never wrong, and directly
* seedable into the client's per-session value store under its
* higher-seq-wins rule (a list baseline can never overwrite a newer push
* frame). Absent when no value is available (no registry, no cache row for
* a cold session, or a fail-soft cache read miss); a listing client treats
* absence as "no title yet", exactly like a blank session.
*/
projections?: SessionProjectionsBlock
}
/** Session-domain unary methods (the map keys session.* of RpcMethodMap). */
export interface SessionsApi {
/** Lists persisted sessions (updatedAt descending). v1 returns everything; cursor is a reserved seat, unimplemented. */
list(request: RpcRequest<{ cursor?: string }>): Promise<RpcResponse<{ items: SessionSummary[] }>>
/**
* Creates a real session and its idle agent. At most one of `workspaceId` /
* `cwd` is accepted; an omitted project uses the Host cwd. A caller may
* preallocate `sessionId`: retries with the same id and cwd return the same
* session, while a different cwd fails with `session-conflict`. Workspace
* creation attaches the session after publication; an attach failure
* returns `workspace-attach-failed` with the published session id.
*/
create(request: RpcRequest<{ workspaceId?: WorkspaceId; cwd?: string; sessionId?: SessionId }>):
Promise<RpcResponse<{ sessionId: SessionId }>>
/**
* Reads a window of history events; page boundaries align to message boundaries: one page =
* all raw events owned by a whole number of messages (including their chunk / tool events),
* never cut mid-message. The tail page (beforeSeq absent) additionally carries the in-flight
* partial — chunk events already emitted for the last unfinalized message.
* Each entry pairs the raw SessionEvent with the host-computed view (tool events whose
* presenter produced one, evaluated against the registry at pagination time); the client
* rebuilds the surface from the events with the shared fold.
* The tail page — and only the tail page — additionally carries `projections`
* when the deployment mounts the session-projection registry: every moment
* the client needs a fresh baseline already pulls the tail page, and
* loadOlder (the only beforeSeq path) is the only path that never needs one.
* A deployment without the registry serves histories without the block.
*/
history(request: RpcRequest<{ sessionId: SessionId; beforeSeq?: number; maxMessages?: number }>):
Promise<RpcResponse<{ events: HistoryEntry[]; hasMore: boolean; projections?: SessionProjectionsBlock }>>
/** Reads a fresh advisory model directory for this session. Provider lookups run independently. */
models(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<SessionModels>>
/**
* Selects the complete target for this session. Exact model metadata
* validates an optional reasoning effort, while catalog membership remains
* advisory.
*/
selectModel(request: RpcRequest<{
sessionId: SessionId
provider: string
model: string
reasoningEffort?: string
}>):
Promise<RpcResponse<{ selected: ModelTarget }>>
/**
* Renames a session: appends a `session/title` event with the `user`
* source, which pins the title against automatic regeneration. The
* normalized accepted title and the title event's seq return so the caller
* can settle its projection cell without waiting for the push frame. A
* title that normalizes to empty fails with `title-invalid`.
*/
rename(request: RpcRequest<{ sessionId: SessionId; title: string }>):
Promise<RpcResponse<{ title: string; seq: number }>>
/**
* Sends a message. content is core's ContentBlock[] verbatim; mode maps 1:1 — queue→send, steer→steer.
* A prompt whose content is exactly one text block starting with '/' is a slash command: the host
* executes it through the command registry (mode-agnostic) and it is never sent to the model. A
* successful command returns ok with the command slot (its success text, when the command produced
* one — carried for future rendering; the state change is the feedback). A usage/state error is an
* RPC error with code command-error; an unrecognized name is an RPC error with code unknown-command.
*/
prompt(request: RpcRequest<{ sessionId: SessionId; mode: 'queue' | 'steer'; content: ContentBlock[] }>):
Promise<RpcResponse<{ accepted: true; command?: { kind: 'success'; text?: string } }>>
/** Stops: clears both FIFOs + aborts the current step (1:1 with agent.cancel). */
cancel(request: RpcRequest<{ sessionId: SessionId }>): Promise<RpcResponse<{ accepted: true }>>
}