diff --git a/packages/core/scope/src/scoped-events.generated.ts b/packages/core/scope/src/scoped-events.generated.ts index 672914c5c3..da93075512 100644 --- a/packages/core/scope/src/scoped-events.generated.ts +++ b/packages/core/scope/src/scoped-events.generated.ts @@ -34,6 +34,7 @@ const scopedSubjectResolvers: Readonly (args[0] as Record)['agent'], 'tools/pre-execute': args => (args[0] as Record)['agent'], 'tools/result': args => (args[0] as Record)['agent'], + 'user-questions/request': args => (args[0] as Record)['agent'], }) /** diff --git a/packages/interaction/user-questions/src/index.ts b/packages/interaction/user-questions/src/index.ts index 803f831826..241ea6cef3 100644 --- a/packages/interaction/user-questions/src/index.ts +++ b/packages/interaction/user-questions/src/index.ts @@ -10,6 +10,7 @@ import { Context, Service } from '@deepseek-ai/cordis' import type { Agent } from '@deepseek-ai/dsh-agent' import { HarnessError } from '@deepseek-ai/dsh-llm' +import { scopeTarget } from '@deepseek-ai/dsh-scope' declare module '@deepseek-ai/cordis' { interface Context { @@ -144,11 +145,18 @@ export class UserQuestionService extends Service { 'BAD_INTENT') } } - if (this.provider === undefined) { - throw new UserQuestionError('no user-questions provider is registered', 'NO_PROVIDER') - } + const askProvider = () => this.provider === undefined + ? Promise.reject(new UserQuestionError('no user-questions provider is registered', 'NO_PROVIDER')) + : this.provider.ask(request) try { - return await this.provider.ask(request) + return await (agent === undefined + ? askProvider() + : this.ctx.waterfall( + scopeTarget(agent, agent), + 'user-questions/request', + { ...request, agent }, + askProvider, + )) } catch (error) { if (request.signal?.aborted && !(error instanceof UserQuestionError)) { throw abortedQuestion(error) diff --git a/packages/interaction/user-questions/tests/user-questions.spec.ts b/packages/interaction/user-questions/tests/user-questions.spec.ts index 3ba062d23c..596aeb06ee 100644 --- a/packages/interaction/user-questions/tests/user-questions.spec.ts +++ b/packages/interaction/user-questions/tests/user-questions.spec.ts @@ -171,6 +171,25 @@ describe('UserQuestionService', () => { expect(result).toEqual({ answers: [{ id: 'confirm', selected: ['yes'] }] }) }) + it('offers an Agent-scoped waterfall before the provider fallback', async () => { + const ctx = new Context() + await ctx.plugin(AgentRegistry) + await ctx.plugin(UserQuestionService) + const p = provider('fallback') + ctx.userQuestions.registerProvider(p) + const agent = stubAgent('root') + ctx.agents.enter(agent, undefined) + ctx.on('user-questions/request', request => Promise.resolve({ + answers: request.questions.map(question => ({ id: question.id, selected: ['remote'] })), + })) + + await expect(ctx.userQuestions.ask({ + questions: [{ id: 'confirm', question: 'Proceed?' }], + agent, + })).resolves.toEqual({ answers: [{ id: 'confirm', selected: ['remote'] }] }) + expect(p.seen).toEqual([]) + }) + it('rejects a supplied agent when no live registry can attest it', async () => { const ctx = new Context() await ctx.plugin(UserQuestionService)