mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-14 04:01:35 +00:00
refactor(api): expose remaining domain remotes
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
/** Session Controller adapter for Agent-scoped file-reference discovery. */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type { Agent } from '@deepseek-ai/dsh-agent'
|
||||
import type {} from '@deepseek-ai/dsh-file-reference'
|
||||
import type { FileReferenceCandidate } from '@deepseek-ai/dsh-file-reference/types'
|
||||
import { Remote, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Host owner of the `fileReferences` Remote namespace. */
|
||||
sessionFileReferences: SessionFileReferences
|
||||
}
|
||||
}
|
||||
|
||||
/** Host Remote adapter over the composed file-reference provider. */
|
||||
export class SessionFileReferences extends TypertRemoteService {
|
||||
static inject = ['fileReferences', 'typert']
|
||||
|
||||
/** @param ctx - Host context carrying the selected file-reference provider. */
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'sessionFileReferences', { namespace: 'fileReferences' })
|
||||
}
|
||||
|
||||
/**
|
||||
* List file and directory candidates for one Agent's working directory.
|
||||
* @param agent - target Agent resolved from the Session identity on the wire.
|
||||
* @param query - path text following `@` or `@"`.
|
||||
* @param signal - caller cancellation.
|
||||
* @returns deterministic path-only candidates from the composed provider.
|
||||
*/
|
||||
@Remote
|
||||
list(
|
||||
agent: Agent,
|
||||
query: string,
|
||||
signal: AbortSignal,
|
||||
): Promise<FileReferenceCandidate[]> {
|
||||
return this.ctx.fileReferences.list(agent, query, signal)
|
||||
}
|
||||
}
|
||||
|
||||
export default SessionFileReferences
|
||||
@@ -3,10 +3,13 @@
|
||||
import { Context } from '@deepseek-ai/cordis'
|
||||
import z from '@deepseek-ai/schemastery'
|
||||
import { errorChain } from '@deepseek-ai/dsh-llm'
|
||||
import { openNativePath } from '@deepseek-ai/dsh-native-command'
|
||||
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 { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import { resolveWorkspacePath } from '@deepseek-ai/dsh-util-workspace-path'
|
||||
import {
|
||||
ApiSessionNotFound,
|
||||
ApiSessionAgentController,
|
||||
inspectApiSession,
|
||||
type ApiSessionAgentResult,
|
||||
@@ -14,9 +17,13 @@ import {
|
||||
import { SessionCommandController } from './commands.ts'
|
||||
import { SessionControlController } from './control.ts'
|
||||
import { SessionHistoryController } from './history.ts'
|
||||
import { SessionFileReferences } from './file-references.ts'
|
||||
import { ApiSessionList, DEFAULT_COLD_BLANK_PROBE_MAX_BYTES } from './list.ts'
|
||||
import { buildModelCatalog } from './catalog.ts'
|
||||
import { installModelSelectionProjection } from './model-selection-projection.ts'
|
||||
import { SessionSkillCatalog } from './skill-catalog.ts'
|
||||
import type {
|
||||
ModelCatalog,
|
||||
SessionAttachmentRequest,
|
||||
SessionAttachmentValue,
|
||||
SessionCancelRequest,
|
||||
@@ -30,6 +37,8 @@ import type {
|
||||
SessionForkValue,
|
||||
SessionListRequest,
|
||||
SessionListValue,
|
||||
SessionOpenWorkspacePathRequest,
|
||||
SessionOpenWorkspacePathValue,
|
||||
SessionPage,
|
||||
SessionPageRequest,
|
||||
SessionPromptRequest,
|
||||
@@ -46,6 +55,8 @@ import type {
|
||||
|
||||
export type * from './types.ts'
|
||||
export { ApiSessionNotFound } from './agent.ts'
|
||||
export { SessionFileReferences } from './file-references.ts'
|
||||
export { SessionSkillCatalog } from './skill-catalog.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
@@ -60,6 +71,12 @@ export interface Config {
|
||||
readonly coldBlankProbeMaxBytes?: number
|
||||
}
|
||||
|
||||
/** Host integrations replaceable by direct unit tests. */
|
||||
export interface SessionControllerInternals {
|
||||
/** Native default-application handoff. */
|
||||
readonly openPath?: (path: string, signal: AbortSignal) => Promise<void>
|
||||
}
|
||||
|
||||
/** Host service backing the generated `ctx.remote.session` namespace. */
|
||||
export class SessionController extends TypertRemoteService {
|
||||
static inject = [
|
||||
@@ -83,13 +100,14 @@ export class SessionController extends TypertRemoteService {
|
||||
private readonly controlState: SessionControlController
|
||||
private readonly history: SessionHistoryController
|
||||
private readonly listState: ApiSessionList
|
||||
private readonly openPath: (path: string, signal: AbortSignal) => Promise<void>
|
||||
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) {
|
||||
constructor(ctx: Context, config: Config, internals: SessionControllerInternals = {}) {
|
||||
super(ctx, 'sessionController', { namespace: 'session' })
|
||||
installModelSelectionProjection(ctx)
|
||||
this.agents = new ApiSessionAgentController(ctx)
|
||||
@@ -105,6 +123,9 @@ export class SessionController extends TypertRemoteService {
|
||||
ctx,
|
||||
config.coldBlankProbeMaxBytes ?? DEFAULT_COLD_BLANK_PROBE_MAX_BYTES,
|
||||
)
|
||||
this.openPath = internals.openPath ?? openNativePath
|
||||
ctx.plugin(SessionFileReferences)
|
||||
ctx.plugin(SessionSkillCatalog)
|
||||
|
||||
ctx.on('session/created', (session) => {
|
||||
ctx.emit('api-session/added', this.listState.summaryFor(session))
|
||||
@@ -214,6 +235,74 @@ export class SessionController extends TypertRemoteService {
|
||||
return this.commands.selectModel(request)
|
||||
}
|
||||
|
||||
/**
|
||||
* Describe every currently routable model for Host-generation selectors.
|
||||
* @returns provider-grouped models, the deployment default, and isolated provider failures.
|
||||
*/
|
||||
@Remote('modelCatalog')
|
||||
modelCatalog(): Promise<ModelCatalog> {
|
||||
return buildModelCatalog(this.ctx)
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a path resolved against one Session's workspace on the Host desktop.
|
||||
* @param request - Session identity and absolute or workspace-relative path.
|
||||
* @param signal - caller lifetime; abort terminates inspection or the native command.
|
||||
* @returns confirmation after the native opener accepts the path.
|
||||
* @throws TypertRemoteFailure when the request is invalid, the Session is missing, or the opener fails.
|
||||
*/
|
||||
@Remote('openWorkspacePath')
|
||||
async openWorkspacePath(
|
||||
request: SessionOpenWorkspacePathRequest,
|
||||
signal: AbortSignal,
|
||||
): Promise<SessionOpenWorkspacePathValue> {
|
||||
if (request.path.length === 0) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'bad-request',
|
||||
message: 'session.openWorkspacePath requires a non-empty path',
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
signal.throwIfAborted()
|
||||
let cwd: string | undefined
|
||||
try {
|
||||
cwd = (await this.inspect(request.sessionId, signal)).meta.cwd
|
||||
} catch (error: unknown) {
|
||||
if (signal.aborted) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'cancelled', message: 'path open was aborted', details: {},
|
||||
})
|
||||
}
|
||||
if (error instanceof ApiSessionNotFound) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'session-not-found',
|
||||
message: error.message,
|
||||
details: { sessionId: request.sessionId },
|
||||
})
|
||||
}
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'internal',
|
||||
message: `session "${request.sessionId}" could not be inspected: ${String(error)}`,
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
try {
|
||||
await this.openPath(resolveWorkspacePath(cwd, request.path), signal)
|
||||
return { opened: true }
|
||||
} catch (error: unknown) {
|
||||
if (signal.aborted) {
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'cancelled', message: 'path open was aborted', details: {},
|
||||
})
|
||||
}
|
||||
throw new TypertRemoteFailure({
|
||||
code: 'internal',
|
||||
message: `path open failed: ${error instanceof Error ? error.message : String(error)}`,
|
||||
details: {},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename one Session after explicitly resuming it.
|
||||
* @param request - Session identity and proposed title.
|
||||
@@ -310,5 +399,5 @@ export class SessionController extends TypertRemoteService {
|
||||
|
||||
}
|
||||
|
||||
export { buildModelCatalog } from './catalog.ts'
|
||||
export { buildModelCatalog }
|
||||
export default SessionController
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
/** Session-addressed, cold-readable skill catalog Remote. */
|
||||
|
||||
import type { Context } from '@deepseek-ai/cordis'
|
||||
import type {} from '@deepseek-ai/dsh-agent-presets/types'
|
||||
import type { SessionId } from '@deepseek-ai/dsh-session'
|
||||
import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
|
||||
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
||||
import type { ScopeKey } from '@deepseek-ai/dsh-scope'
|
||||
import { Remote, TypertRemoteFailure, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
|
||||
import type { SkillListRequest, SkillListValue } from './types.ts'
|
||||
|
||||
declare module '@deepseek-ai/cordis' {
|
||||
interface Context {
|
||||
/** Host owner of the Session-addressed `skills` Remote namespace. */
|
||||
sessionSkillCatalog: SessionSkillCatalog
|
||||
}
|
||||
}
|
||||
|
||||
/** Host service backing `ctx.remote.skills` without activating a cold Agent. */
|
||||
export class SessionSkillCatalog extends TypertRemoteService {
|
||||
static inject = ['agents', 'sessionQuery', 'typert']
|
||||
|
||||
/** @param ctx - Host context carrying Session reads and optional skill/preset services. */
|
||||
constructor(ctx: Context) {
|
||||
super(ctx, 'sessionSkillCatalog', { namespace: 'skills' })
|
||||
}
|
||||
|
||||
/**
|
||||
* List the user-invocable skills visible to one Session composition.
|
||||
* @param request - Session identity whose cwd and preset select the catalog view.
|
||||
* @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.
|
||||
* @returns user-invocable skill metadata without loading skill bodies.
|
||||
* @throws TypertRemoteFailure when the Session cannot be inspected or no registry can serve it.
|
||||
*/
|
||||
@Remote
|
||||
async list(request: SkillListRequest, signal: AbortSignal): Promise<SkillListValue> {
|
||||
void signal
|
||||
const { sessionId } = request
|
||||
let cwd: string | undefined
|
||||
let agentPreset: string | undefined
|
||||
try {
|
||||
using observation = await this.ctx.sessionQuery.observeSession(sessionId)
|
||||
if (observation.projections === undefined) {
|
||||
throw new Error('skill catalog requires a projected Session observation')
|
||||
}
|
||||
cwd = observation.header.cwd
|
||||
agentPreset = observation.projections.values.agentPreset ?? undefined
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof SessionQueryError
|
||||
&& error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
|
||||
throw failure(
|
||||
'session-not-found',
|
||||
`session "${sessionId}" not found`,
|
||||
{ sessionId },
|
||||
)
|
||||
}
|
||||
throw failure(
|
||||
'internal',
|
||||
`session "${sessionId}" could not be inspected: ${String(error)}`,
|
||||
)
|
||||
}
|
||||
if (cwd === undefined) {
|
||||
throw failure('internal', `session "${sessionId}" has no project cwd`)
|
||||
}
|
||||
|
||||
const live = this.ctx.agents.get(sessionId)
|
||||
const presets = this.ctx.get('agentPresets')
|
||||
const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills')
|
||||
const skillRegistry = scoped ?? this.ctx.get('skills')
|
||||
if (skillRegistry === undefined) {
|
||||
throw failure(
|
||||
'internal',
|
||||
'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill',
|
||||
)
|
||||
}
|
||||
|
||||
const scope = await this.scopeFor(sessionId, agentPreset)
|
||||
try {
|
||||
const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable)
|
||||
return {
|
||||
skills: skills.map(skill => ({
|
||||
name: skill.name,
|
||||
description: skill.description,
|
||||
...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse },
|
||||
modelInvocable: skill.invocation.modelInvocable,
|
||||
})),
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
throw failure('internal', `skill listing failed: ${String(error)}`)
|
||||
}
|
||||
}
|
||||
|
||||
/** Resolve a live or standing preset scope without creating an Agent. */
|
||||
private async scopeFor(
|
||||
sessionId: SessionId,
|
||||
agentPreset: string | undefined,
|
||||
): Promise<ScopeKey | undefined> {
|
||||
const live = this.ctx.agents.get(sessionId)
|
||||
if (live !== undefined) return live
|
||||
const presets = this.ctx.get('agentPresets')
|
||||
if (presets === undefined) return undefined
|
||||
try {
|
||||
return await presets.standingKeyFor(agentPreset)
|
||||
} catch {
|
||||
// An unknown or unusable recorded preset falls back to the global registry.
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Build one stable Remote failure with optional typed details. */
|
||||
function failure(
|
||||
code: 'session-not-found' | 'internal',
|
||||
message: string,
|
||||
details: { readonly sessionId: SessionId } | Record<never, never> = {},
|
||||
): TypertRemoteFailure {
|
||||
return new TypertRemoteFailure({ code, message, details })
|
||||
}
|
||||
|
||||
export default SessionSkillCatalog
|
||||
@@ -223,6 +223,28 @@ export type SessionError = {
|
||||
}
|
||||
}[keyof SessionErrorDetailsMap]
|
||||
|
||||
/** Session-addressed request for the human-invocable skill catalog. */
|
||||
export interface SkillListRequest {
|
||||
readonly sessionId: SessionId
|
||||
}
|
||||
|
||||
/** One skill available to the Session's human-facing composer. */
|
||||
export interface SkillEntry {
|
||||
/** Kebab-case identifier referenced as `/name`. */
|
||||
readonly name: string
|
||||
/** Short routing description. */
|
||||
readonly description: string
|
||||
/** Optional extra routing guidance. */
|
||||
readonly whenToUse?: string
|
||||
/** Whether the same skill is also advertised to the model. */
|
||||
readonly modelInvocable: boolean
|
||||
}
|
||||
|
||||
/** Human-invocable skills visible through one Session's composition. */
|
||||
export interface SkillListValue {
|
||||
readonly skills: readonly SkillEntry[]
|
||||
}
|
||||
|
||||
/** Session list request. */
|
||||
export interface SessionListRequest {
|
||||
readonly cursor?: string
|
||||
@@ -340,6 +362,18 @@ export interface SessionCancelValue {
|
||||
readonly accepted: true
|
||||
}
|
||||
|
||||
/** Session-addressed request to open one workspace path on the Host desktop. */
|
||||
export interface SessionOpenWorkspacePathRequest {
|
||||
readonly sessionId: SessionId
|
||||
/** Absolute or Session-workspace-relative path. */
|
||||
readonly path: string
|
||||
}
|
||||
|
||||
/** Confirmation that the Host handed a workspace path to its native opener. */
|
||||
export interface SessionOpenWorkspacePathValue {
|
||||
readonly opened: true
|
||||
}
|
||||
|
||||
/** Client-minted prompt identity used to reconcile optimistic and durable messages. */
|
||||
export type SessionRequestId = Branded<'session-request-id'>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user