scripted-provider.ts 4.4 KB

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