scripted-provider.ts 4.2 KB

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