/** * User-interaction seam (`ctx.userInteraction`): a UI-backed service for * pausing an agent tool call until the human answers a question. The model- * facing tool lives in `@deepseek-ai/dsh-tool-ask-user`; UI packages provide * the single active provider. * * @module @deepseek-ai/dsh-user-interaction */ import { Context, Service } from 'cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import { HarnessError } from '@deepseek-ai/dsh-llm' declare module 'cordis' { interface Context { userInteraction: UserInteractionService } } import type { AskUserQuestionAnswer, AskUserQuestionItem } from './types.ts' export type { AskUserQuestionAnswer, AskUserQuestionAnswerItem, AskUserQuestionItem, AskUserQuestionOption, } from './types.ts' /** Request for a human answer. */ export interface AskUserQuestionRequest { /** Questions to display. */ questions: AskUserQuestionItem[] /** Calling agent, when the request came from an agent tool call. */ agent?: Agent /** Abort signal for the owning tool/step. */ signal?: AbortSignal } /** UI-side provider for user questions. */ export interface UserInteractionProvider { ask(request: AskUserQuestionRequest): Promise } /** Stable error taxonomy for user-interaction failures. */ export class UserInteractionError extends HarnessError { constructor(message: string, code: string, options?: ErrorOptions) { super(message, code, options) this.name = 'UserInteractionError' } } /** `ctx.userInteraction`: one active UI provider plus an `ask()` surface. */ export class UserInteractionService extends Service { private provider: UserInteractionProvider | undefined constructor(ctx: Context) { super(ctx, 'userInteraction') } /** * Register the UI provider. Only one provider may be active in a context. * * @param provider UI-side implementation that collects answers. * @returns Disposer that unregisters this provider. */ registerProvider(provider: UserInteractionProvider): () => void { const dispose = this.ctx.effect(function* (this: UserInteractionService) { if (this.provider !== undefined) { throw new UserInteractionError('a user-interaction provider is already registered', 'DUPLICATE_PROVIDER') } this.provider = provider yield () => { this.provider = undefined } }.bind(this), 'userInteraction.registerProvider()') return () => void dispose() } /** * Ask the active UI provider and wait for the user's answer. * * @param request Questions, owner agent, and abort signal. * @returns The answer chosen or typed by the human. */ async ask(request: AskUserQuestionRequest): Promise { if (request.signal?.aborted) { throw new UserInteractionError('ask_user_question was aborted before the user answered', 'ASK_ABORTED') } if (request.questions.length === 0) { throw new UserInteractionError('ask_user_question requires at least one question', 'EMPTY_QUESTIONS') } if (this.provider === undefined) { throw new UserInteractionError('no user-interaction provider is registered', 'NO_PROVIDER') } return this.provider.ask(request) } } export default UserInteractionService