index.ts 3.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  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. import type { AskUserQuestionAnswer, AskUserQuestionItem } from './types.ts'
  18. export type {
  19. AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionItem, AskUserQuestionOption,
  20. } from './types.ts'
  21. /** Request for a human answer. */
  22. export interface AskUserQuestionRequest {
  23. /** Questions to display. */
  24. questions: AskUserQuestionItem[]
  25. /** Calling agent, when the request came from an agent tool call. */
  26. agent?: Agent
  27. /** Abort signal for the owning tool/step. */
  28. signal?: AbortSignal
  29. }
  30. /** UI-side provider for user questions. */
  31. export interface UserInteractionProvider {
  32. ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer>
  33. }
  34. /** Stable error taxonomy for user-interaction failures. */
  35. export class UserInteractionError extends HarnessError {
  36. constructor(message: string, code: string, options?: ErrorOptions) {
  37. super(message, code, options)
  38. this.name = 'UserInteractionError'
  39. }
  40. }
  41. /** `ctx.userInteraction`: one active UI provider plus an `ask()` surface. */
  42. export class UserInteractionService extends Service {
  43. private provider: UserInteractionProvider | undefined
  44. constructor(ctx: Context) {
  45. super(ctx, 'userInteraction')
  46. }
  47. /**
  48. * Register the UI provider. Only one provider may be active in a context.
  49. *
  50. * @param provider UI-side implementation that collects answers.
  51. * @returns Disposer that unregisters this provider.
  52. */
  53. registerProvider(provider: UserInteractionProvider): () => void {
  54. const dispose = this.ctx.effect(function* (this: UserInteractionService) {
  55. if (this.provider !== undefined) {
  56. throw new UserInteractionError('a user-interaction provider is already registered', 'DUPLICATE_PROVIDER')
  57. }
  58. this.provider = provider
  59. yield () => {
  60. this.provider = undefined
  61. }
  62. }.bind(this), 'userInteraction.registerProvider()')
  63. return () => void dispose()
  64. }
  65. /**
  66. * Ask the active UI provider and wait for the user's answer.
  67. *
  68. * @param request Questions, owner agent, and abort signal.
  69. * @returns The answer chosen or typed by the human.
  70. */
  71. async ask(request: AskUserQuestionRequest): Promise<AskUserQuestionAnswer> {
  72. if (request.signal?.aborted) {
  73. throw new UserInteractionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED')
  74. }
  75. if (request.questions.length === 0) {
  76. throw new UserInteractionError('ask_user_question requires at least one question', 'EMPTY_QUESTIONS')
  77. }
  78. if (this.provider === undefined) {
  79. throw new UserInteractionError('no user-interaction provider is registered', 'NO_PROVIDER')
  80. }
  81. return this.provider.ask(request)
  82. }
  83. }
  84. export default UserInteractionService