scripted-provider.ts 4.1 KB

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