mirror of
https://github.com/deepseek-ai/deepseek-harness.git
synced 2026-09-09 04:02:35 +00:00
Single RemoteError with a merge-extensible, domain-prefixed code map; owners throw at the failure point; streams surface marked failures; clients consume ctx.remote directly with isRemoteFailure as the only discrimination point and construct no failure instances.
110 lines
4.3 KiB
TypeScript
110 lines
4.3 KiB
TypeScript
/** 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, RemoteError, 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 RemoteError 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 new RemoteError('session/not-found', `session "${sessionId}" not found`, { sessionId })
|
|
}
|
|
throw new RemoteError(
|
|
'gateway/internal',
|
|
`session "${sessionId}" could not be inspected: ${String(error)}`,
|
|
{},
|
|
)
|
|
}
|
|
if (cwd === undefined) {
|
|
throw new RemoteError('gateway/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 new RemoteError(
|
|
'gateway/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 new RemoteError('gateway/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
|
|
}
|
|
}
|
|
}
|
|
|
|
export default SessionSkillCatalog
|