scripted-provider.ts 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104
  1. /** Package-local scripted child boundary for deterministic tool-subagent tests. */
  2. import type { Context } from 'cordis'
  3. import type { ContentBlock } from '@deepseek-ai/dsh-llm'
  4. import { SessionId } from '@deepseek-ai/dsh-session'
  5. import type {
  6. SubagentCapabilities,
  7. SubagentProvider,
  8. SubagentResult,
  9. SubagentRun,
  10. SubagentStartRequest,
  11. SubagentStopReason,
  12. } from '@deepseek-ai/dsh-subagent'
  13. const DEFAULT_CAPABILITIES: SubagentCapabilities = {
  14. outputSchema: true,
  15. depthLimit: true,
  16. toolFilter: true,
  17. persona: true,
  18. }
  19. /** Options for one scripted provider fixture. */
  20. export interface Config {
  21. /** Registry name to register under. */
  22. name: string
  23. /** Final text returned by the scripted child. */
  24. reply?: string
  25. /** Terminal result reason. */
  26. stopReason?: SubagentStopReason
  27. /** Start-time features advertised by the provider. */
  28. capabilities?: Partial<SubagentCapabilities>
  29. /** Whether tool descriptions say the child inherits completed turns. */
  30. inheritsParentContext?: boolean
  31. /** Structured value returned when the request asks for one. */
  32. structured?: unknown
  33. }
  34. /** Scripted provider whose result aborts if its signal or disposer wins first. */
  35. class ScriptedSubagentProvider implements SubagentProvider {
  36. readonly capabilities: SubagentCapabilities
  37. readonly inheritsParentContext: boolean
  38. constructor(
  39. readonly name: string,
  40. private readonly config: Config,
  41. ) {
  42. this.capabilities = { ...DEFAULT_CAPABILITIES, ...config.capabilities }
  43. this.inheritsParentContext = config.inheritsParentContext ?? false
  44. }
  45. async start(request: SubagentStartRequest): Promise<SubagentRun> {
  46. if (request.signal.aborted) throw new Error('scripted subagent start aborted before publication')
  47. const reply = this.config.reply ?? 'scripted subagent reply'
  48. const output: ContentBlock[] = [{ type: 'text', text: reply }]
  49. const wantsStructured = request.outputSchema !== undefined && this.capabilities.outputSchema
  50. const stopReason = this.config.stopReason ?? 'completed'
  51. const state = { cancelled: false }
  52. const onAbort = (): void => { state.cancelled = true }
  53. request.signal.addEventListener('abort', onAbort, { once: true })
  54. await Promise.resolve()
  55. if (state.cancelled) {
  56. request.signal.removeEventListener('abort', onAbort)
  57. throw new Error('scripted subagent start aborted before publication')
  58. }
  59. const resultFor = (): SubagentResult => ({
  60. output,
  61. ...wantsStructured ? { structured: this.config.structured ?? { reply } } : {},
  62. stopReason: state.cancelled ? 'aborted' : stopReason,
  63. })
  64. const result = new Promise<SubagentResult>((resolve) => {
  65. setTimeout(() => { resolve(resultFor()) }, 0)
  66. }).finally(() => {
  67. request.signal.removeEventListener('abort', onAbort)
  68. })
  69. return {
  70. id: SessionId(`scripted-subagent:${this.name}:${request.parent.id}`),
  71. localAgent: undefined,
  72. result,
  73. dispose(): Promise<void> {
  74. state.cancelled = true
  75. request.signal.removeEventListener('abort', onAbort)
  76. return Promise.resolve()
  77. },
  78. }
  79. }
  80. }
  81. /**
  82. * Mount one scripted provider through an effect-scoped local plugin.
  83. * @param ctx - context carrying the real subagent registry.
  84. * @param config - scripted provider identity and outcome.
  85. * @returns the fixture plugin's disposable fiber.
  86. */
  87. export function mountScriptedProvider(ctx: Context, config: Config) {
  88. return ctx.plugin({
  89. name: 'scripted-subagent-provider',
  90. inject: ['subagents'],
  91. apply(pluginCtx: Context): void {
  92. pluginCtx.subagents.registerProvider(new ScriptedSubagentProvider(config.name, config))
  93. },
  94. })
  95. }