Regenerate config/tool/module/event catalogs, Cordis API and client slot catalogs; re-record reviewed bilingual hashes instead of merging generated bytes. Accept deleted filesystem invariant companions. Master has no replacement event-invariant registration; keep delete-intent in typed events, policy guards, and behavior tests. Adopt master workspace-changes intent waterfalls and tool-execution recorder association, add a prepended delete-intent listener that delegates before capture, remove argument parsing and the fs-patch dependency, and retain the real-tool Review matrix. Keep master filesystem mutation-result metadata and tool switches alongside opt-in apply_patch. Await current-directory resolution in plan and no-follow preflight; store patch diffs as processPath. Adapt native/PTC fixtures to the supported modes. Preserve client patch diffs and stopped states with master partial arguments and absolute navigation. Use master's rewritten ProviderEditor, including its remaining repeated model validation. Regenerate the lockfile with pnpm install and align the new fs-patch package version. Clean retired package build outputs. Update exact rescope exceptions for verified preset and event identifiers from master; old package names remain rejected.
110 lines
4.7 KiB
TypeScript
110 lines
4.7 KiB
TypeScript
/** Session-addressed, cold-readable skill catalog Remote. */
|
|
|
|
import type { Context } from '@deepseek-ai/cordis'
|
|
import type {} from '@deepseek-ai/dsh-agent-preset-registry/types'
|
|
import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
|
|
import { isUserInvocable } from '@deepseek-ai/dsh-skill'
|
|
import type {} from '@deepseek-ai/dsh-working-directory'
|
|
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', 'workingDirectory']
|
|
|
|
/** @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> {
|
|
const { sessionId } = request
|
|
let cwd: string
|
|
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.projections.values.workingDirectory
|
|
?? observation.header.cwd
|
|
?? this.ctx.workingDirectory.defaultDirectory
|
|
agentPreset = observation.projections.values.agentPreset ?? undefined
|
|
const live = this.ctx.agents.get(sessionId)
|
|
if (live !== undefined) cwd = await this.ctx.workingDirectory.ensure(live, signal)
|
|
} 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)}`,
|
|
{},
|
|
)
|
|
}
|
|
|
|
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',
|
|
{},
|
|
)
|
|
}
|
|
|
|
await using lease = live === undefined ? await this.scopeFor(agentPreset) : undefined
|
|
const scope = live ?? lease?.key
|
|
try {
|
|
const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable)
|
|
return {
|
|
skills: skills.map(skill => ({
|
|
name: skill.name,
|
|
...skill.path === undefined ? {} : { path: skill.path },
|
|
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(
|
|
agentPreset: string | undefined,
|
|
): Promise<({ key: ScopeKey } & AsyncDisposable) | undefined> {
|
|
const presets = this.ctx.get('agentPresets')
|
|
if (presets === undefined) return undefined
|
|
try {
|
|
return await presets.acquireScope(agentPreset)
|
|
} catch (error) {
|
|
if (error instanceof RemoteError && (error.code === 'agent-preset/not-found' || error.code === 'agent-preset/invalid')) return undefined
|
|
this.ctx.logger.warn(`skill catalog could not acquire preset ${String(agentPreset)}; using global skills: ${String(error)}`)
|
|
return undefined
|
|
}
|
|
}
|
|
}
|
|
|
|
export default SessionSkillCatalog
|