skill-catalog.ts 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
  1. /** Session-addressed, cold-readable skill catalog Remote. */
  2. import type { Context } from '@deepseek-ai/cordis'
  3. import type {} from '@deepseek-ai/dsh-agent-presets/types'
  4. import type { SessionId } from '@deepseek-ai/dsh-session'
  5. import { SessionQueryError } from '@deepseek-ai/dsh-session-query'
  6. import { isUserInvocable } from '@deepseek-ai/dsh-skill'
  7. import type { ScopeKey } from '@deepseek-ai/dsh-scope'
  8. import { Remote, RemoteError, TypertRemoteService } from '@deepseek-ai/dsh-typert-protocol'
  9. import type { SkillListRequest, SkillListValue } from './types.ts'
  10. declare module '@deepseek-ai/cordis' {
  11. interface Context {
  12. /** Host owner of the Session-addressed `skills` Remote namespace. */
  13. sessionSkillCatalog: SessionSkillCatalog
  14. }
  15. }
  16. /** Host service backing `ctx.remote.skills` without activating a cold Agent. */
  17. export class SessionSkillCatalog extends TypertRemoteService {
  18. static inject = ['agents', 'sessionQuery', 'typert']
  19. /** @param ctx - Host context carrying Session reads and optional skill/preset services. */
  20. constructor(ctx: Context) {
  21. super(ctx, 'sessionSkillCatalog', { namespace: 'skills' })
  22. }
  23. /**
  24. * List the user-invocable skills visible to one Session composition.
  25. * @param request - Session identity whose cwd and preset select the catalog view.
  26. * @param signal - caller lifetime carried by the Remote transport; admitted catalog reads retain their existing completion semantics.
  27. * @returns user-invocable skill metadata without loading skill bodies.
  28. * @throws RemoteError when the Session cannot be inspected or no registry can serve it.
  29. */
  30. @Remote
  31. async list(request: SkillListRequest, signal: AbortSignal): Promise<SkillListValue> {
  32. void signal
  33. const { sessionId } = request
  34. let cwd: string | undefined
  35. let agentPreset: string | undefined
  36. try {
  37. using observation = await this.ctx.sessionQuery.observeSession(sessionId)
  38. if (observation.projections === undefined) {
  39. throw new Error('skill catalog requires a projected Session observation')
  40. }
  41. cwd = observation.header.cwd
  42. agentPreset = observation.projections.values.agentPreset ?? undefined
  43. } catch (error: unknown) {
  44. if (error instanceof SessionQueryError
  45. && error.code === 'SESSION_QUERY_SESSION_NOT_FOUND') {
  46. throw new RemoteError('session/not-found', `session "${sessionId}" not found`, { sessionId })
  47. }
  48. throw new RemoteError(
  49. 'gateway/internal',
  50. `session "${sessionId}" could not be inspected: ${String(error)}`,
  51. {},
  52. )
  53. }
  54. if (cwd === undefined) {
  55. throw new RemoteError('gateway/internal', `session "${sessionId}" has no project cwd`, {})
  56. }
  57. const live = this.ctx.agents.get(sessionId)
  58. const presets = this.ctx.get('agentPresets')
  59. const scoped = live === undefined ? undefined : presets?.serviceFor(live, 'skills')
  60. const skillRegistry = scoped ?? this.ctx.get('skills')
  61. if (skillRegistry === undefined) {
  62. throw new RemoteError(
  63. 'gateway/internal',
  64. 'skill registry is absent: neither this session\'s agent preset nor the host composition mounts @deepseek-ai/dsh-skill',
  65. {},
  66. )
  67. }
  68. const scope = await this.scopeFor(sessionId, agentPreset)
  69. try {
  70. const skills = (await skillRegistry.list({ cwd, scope })).filter(isUserInvocable)
  71. return {
  72. skills: skills.map(skill => ({
  73. name: skill.name,
  74. ...skill.path === undefined ? {} : { path: skill.path },
  75. description: skill.description,
  76. ...skill.whenToUse === undefined ? {} : { whenToUse: skill.whenToUse },
  77. modelInvocable: skill.invocation.modelInvocable,
  78. })),
  79. }
  80. } catch (error: unknown) {
  81. throw new RemoteError('gateway/internal', `skill listing failed: ${String(error)}`, {})
  82. }
  83. }
  84. /** Resolve a live or standing preset scope without creating an Agent. */
  85. private async scopeFor(
  86. sessionId: SessionId,
  87. agentPreset: string | undefined,
  88. ): Promise<ScopeKey | undefined> {
  89. const live = this.ctx.agents.get(sessionId)
  90. if (live !== undefined) return live
  91. const presets = this.ctx.get('agentPresets')
  92. if (presets === undefined) return undefined
  93. try {
  94. return await presets.standingKeyFor(agentPreset)
  95. } catch {
  96. // An unknown or unusable recorded preset falls back to the global registry.
  97. return undefined
  98. }
  99. }
  100. }
  101. export default SessionSkillCatalog