index.ts 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128
  1. /**
  2. * User-interaction seam (`ctx.userInteraction`): a UI-backed service for
  3. * pausing an agent tool call until the human answers a question. The model-
  4. * facing tool lives in `@deepseek-ai/dsh-tool-ask-user`; UI packages provide
  5. * the single active provider.
  6. *
  7. * @module @deepseek-ai/dsh-user-interaction
  8. */
  9. import { Context, Service } from 'cordis'
  10. import type { Agent } from '@deepseek-ai/dsh-agent'
  11. import { HarnessError } from '@deepseek-ai/dsh-llm'
  12. declare module 'cordis' {
  13. interface Context {
  14. userInteraction: UserInteractionService
  15. }
  16. }
  17. /** One selectable answer offered to the user. */
  18. export interface AskUserQuestionOption {
  19. /** User-facing label. */
  20. label: string
  21. /** Optional extra context rendered by capable UIs. */
  22. description?: string
  23. }
  24. /** One question in an ask_user_question request. */
  25. export interface AskUserQuestionItem {
  26. /** Stable model-provided question id, echoed in the answer. */
  27. id: string
  28. /** The question to display. */
  29. question: string
  30. /** Optional short heading/group label. */
  31. header?: string
  32. /** Optional choices the UI can render as a menu. */
  33. options?: AskUserQuestionOption[]
  34. /** Whether more than one option may be selected. Defaults to single-select. */
  35. multiSelect?: boolean
  36. }
  37. /** Request for a human answer. */
  38. export interface AskUserQuestionRequest {
  39. /** Questions to display. */
  40. questions: AskUserQuestionItem[]
  41. /** Calling agent, when the request came from an agent tool call. */
  42. agent?: Agent
  43. /** Abort signal for the owning tool/step. */
  44. signal?: AbortSignal
  45. }
  46. /** Answer to one question. */
  47. export interface AskUserQuestionAnswerItem {
  48. /** The answered question id. */
  49. id: string
  50. /** Selected option labels. Empty when the answer is purely custom text. */
  51. selected: string[]
  52. /** Optional free-text "Other" answer. */
  53. custom?: string
  54. }
  55. /** The human's answer. */
  56. export interface AskUserQuestionAnswer {
  57. /** Structured answers keyed by question id. */
  58. answers: AskUserQuestionAnswerItem[]
  59. }
  60. /** UI-side provider for user questions. */
  61. export interface UserInteractionProvider {
  62. ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
  63. }
  64. /** Stable error taxonomy for user-interaction failures. */
  65. export class UserInteractionError extends HarnessError {
  66. constructor(message: string, code: string, options?: ErrorOptions) {
  67. super(message, code, options)
  68. this.name = 'UserInteractionError'
  69. }
  70. }
  71. /** `ctx.userInteraction`: one active UI provider plus an `ask()` surface. */
  72. export class UserInteractionService extends Service {
  73. private provider: UserInteractionProvider | undefined
  74. constructor(ctx: Context) {
  75. super(ctx, 'userInteraction')
  76. }
  77. /**
  78. * Register the UI provider. Only one provider may be active in a context.
  79. *
  80. * @param provider UI-side implementation that collects answers.
  81. * @returns Disposer that unregisters this provider.
  82. */
  83. registerProvider(provider: UserInteractionProvider): () => void {
  84. const dispose = this.ctx.effect(function* (this: UserInteractionService) {
  85. if (this.provider !== undefined) {
  86. throw new UserInteractionError('a user-interaction provider is already registered', 'DUPLICATE_PROVIDER')
  87. }
  88. this.provider = provider
  89. yield () => {
  90. this.provider = undefined
  91. }
  92. }.bind(this), 'userInteraction.registerProvider()')
  93. return () => void dispose()
  94. }
  95. /**
  96. * Ask the active UI provider and wait for the user's answer.
  97. *
  98. * @param request Questions, owner agent, and abort signal.
  99. * @returns The answer chosen or typed by the human.
  100. */
  101. async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer> {
  102. if (request.signal?.aborted) {
  103. throw new UserInteractionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED')
  104. }
  105. if (request.questions.length === 0) {
  106. throw new UserInteractionError('ask_user_question requires at least one question', 'EMPTY_QUESTIONS')
  107. }
  108. if (this.provider === undefined) {
  109. throw new UserInteractionError('no user-interaction provider is registered', 'NO_PROVIDER')
  110. }
  111. return this.provider.ask(request)
  112. }
  113. }
  114. export default UserInteractionService