scripted-provider.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107
  1. /** Package-local scripted child boundary for deterministic tool-subagent tests. */
  2. import type { Context } from '@deepseek-ai/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. /** Observes each start; the child's result additionally waits for the returned promise. */
  34. onStart?: (request: SubagentStartRequest) => Promise<void> | void
  35. }
  36. /** Scripted provider whose result aborts if its signal or disposer wins first. */
  37. class ScriptedSubagentProvider implements SubagentProvider {
  38. readonly capabilities: SubagentCapabilities
  39. readonly inheritsParentContext: boolean
  40. constructor(
  41. readonly name: string,
  42. private readonly config: Config,
  43. ) {
  44. this.capabilities = { ...DEFAULT_CAPABILITIES, ...config.capabilities }
  45. this.inheritsParentContext = config.inheritsParentContext ?? false
  46. }
  47. async start(request: SubagentStartRequest): Promise<SubagentRun> {
  48. if (request.signal.aborted) throw new Error('scripted subagent start aborted before publication')
  49. const reply = this.config.reply ?? 'scripted subagent reply'
  50. const output: ContentBlock[] = [{ type: 'text', text: reply }]
  51. const wantsStructured = request.outputSchema !== undefined && this.capabilities.outputSchema
  52. const stopReason = this.config.stopReason ?? 'completed'
  53. const state = { cancelled: false }
  54. const onAbort = (): void => { state.cancelled = true }
  55. request.signal.addEventListener('abort', onAbort, { once: true })
  56. await Promise.resolve()
  57. if (state.cancelled) {
  58. request.signal.removeEventListener('abort', onAbort)
  59. throw new Error('scripted subagent start aborted before publication')
  60. }
  61. const resultFor = (): SubagentResult => ({
  62. output,
  63. ...wantsStructured ? { structured: this.config.structured ?? { reply } } : {},
  64. stopReason: state.cancelled ? 'aborted' : stopReason,
  65. })
  66. const gate = Promise.resolve(this.config.onStart?.(request))
  67. const result = gate.then(() => new Promise<SubagentResult>((resolve) => {
  68. setTimeout(() => { resolve(resultFor()) }, 0)
  69. })).finally(() => {
  70. request.signal.removeEventListener('abort', onAbort)
  71. })
  72. return {
  73. id: SessionId(`scripted-subagent:${this.name}:${request.parent.id}`),
  74. localAgent: undefined,
  75. result,
  76. dispose(): Promise<void> {
  77. state.cancelled = true
  78. request.signal.removeEventListener('abort', onAbort)
  79. return Promise.resolve()
  80. },
  81. }
  82. }
  83. }
  84. /**
  85. * Mount one scripted provider through an effect-scoped local plugin.
  86. * @param ctx - context carrying the real subagent registry.
  87. * @param config - scripted provider identity and outcome.
  88. * @returns the fixture plugin's disposable fiber.
  89. */
  90. export function mountScriptedProvider(ctx: Context, config: Config) {
  91. return ctx.plugin({
  92. name: 'scripted-subagent-provider',
  93. inject: ['subagents'],
  94. apply(pluginCtx: Context): void {
  95. pluginCtx.subagents.registerProvider(new ScriptedSubagentProvider(config.name, config))
  96. },
  97. })
  98. }