index.ts 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122
  1. /**
  2. * A scripted {@link SubagentProvider} for testing the subagent seam WITHOUT a
  3. * model or a real child agent. Mirrors `@deepseek-ai/dsh-llm-replay`: it lets a
  4. * test drive the service and the model-facing tool through the REAL cordis
  5. * Loader / export path, exercising registration, capability validation, the
  6. * run lifecycle, and the structured-output branch deterministically.
  7. *
  8. * Plugin export shape: named `name`/`inject`/`Config`/`apply`, NO default —
  9. * a functional plugin (it only registers a provider; it is never injected).
  10. *
  11. * @module @deepseek-ai/dsh-subagent-mock
  12. */
  13. import type { Context } from 'cordis'
  14. import z from 'schemastery'
  15. import { AgentId } from '@deepseek-ai/dsh-agent'
  16. import type { ContentBlock } from '@deepseek-ai/dsh-llm'
  17. import type {
  18. SubagentCapabilities,
  19. SubagentProvider,
  20. SubagentResult,
  21. SubagentRun,
  22. SubagentStartRequest,
  23. SubagentStopReason,
  24. } from '@deepseek-ai/dsh-subagent'
  25. const STOP_REASONS = ['completed', 'aborted', 'error', 'max-tokens', 'refusal'] as const
  26. const DEFAULT_CAPS: SubagentCapabilities = { outputSchema: true, depthLimit: true, toolFilter: true, persona: true }
  27. /**
  28. * A scripted provider: every {@link start} returns a run whose `result`
  29. * resolves on a microtask with the configured reply (and a structured value
  30. * when the request asked for one and the capability is on). `dispose` is a
  31. * no-op; a `cancel()` before the result settles flips the stop reason to
  32. * `aborted`, so the cancellation path is observable in a test.
  33. */
  34. class MockSubagentProvider implements SubagentProvider {
  35. readonly capabilities: SubagentCapabilities
  36. readonly inheritsParentContext: boolean
  37. constructor(
  38. readonly name: string,
  39. private readonly config: Config,
  40. ) {
  41. this.capabilities = { ...DEFAULT_CAPS, ...config.capabilities }
  42. this.inheritsParentContext = config.inheritsParentContext ?? false
  43. }
  44. start(request: SubagentStartRequest): SubagentRun {
  45. const reply = this.config.reply ?? 'mock subagent reply'
  46. const output: ContentBlock[] = [{ type: 'text', text: reply }]
  47. const wantsStructured = request.outputSchema !== undefined && this.capabilities.outputSchema
  48. const baseStop: SubagentStopReason = this.config.stopReason ?? 'completed'
  49. let cancelled = false
  50. // A deterministic child id derived from the parent — no clock/random (both
  51. // banned in deterministic paths here, and unnecessary for a scripted run).
  52. const id = AgentId(`mock-subagent:${this.name}:${request.parent.id}`)
  53. const resultFor = (): SubagentResult => ({
  54. output,
  55. structured: wantsStructured ? (this.config.structured ?? { reply }) : undefined,
  56. stopReason: cancelled ? 'aborted' : baseStop,
  57. })
  58. return {
  59. id,
  60. result: Promise.resolve().then(resultFor),
  61. cancel() {
  62. cancelled = true
  63. },
  64. async dispose() {
  65. // Scripted run holds no resources — nothing to await.
  66. },
  67. }
  68. }
  69. }
  70. export const name = 'subagent-mock'
  71. export const inject = ['subagents']
  72. /** Config for the mock provider; all optional with test-friendly defaults. */
  73. export interface Config {
  74. /** Registry name to register under. */
  75. name: string
  76. /** The text the scripted child "returns" as its final answer. */
  77. reply?: string
  78. /** The stop reason the run settles with. */
  79. stopReason?: SubagentStopReason
  80. /** Which start-time capabilities to advertise (default: all `true`). */
  81. capabilities?: Partial<SubagentCapabilities>
  82. /**
  83. * The context contract to declare ({@link SubagentProvider.inheritsParentContext});
  84. * default `false` (spawn-like). Set `true` to exercise the fork-shaped tool
  85. * wording in consumer tests.
  86. */
  87. inheritsParentContext?: boolean
  88. /**
  89. * Structured value surfaced when a request carries an `outputSchema` and the
  90. * `outputSchema` capability is on (default: `{ reply }`).
  91. */
  92. structured?: unknown
  93. }
  94. export const Config: z<Config> = z.object({
  95. name: z.string().default('mock'),
  96. reply: z.string(),
  97. stopReason: z.union(STOP_REASONS),
  98. capabilities: z.object({
  99. outputSchema: z.boolean(),
  100. depthLimit: z.boolean(),
  101. toolFilter: z.boolean(),
  102. persona: z.boolean(),
  103. }),
  104. inheritsParentContext: z.boolean(),
  105. structured: z.any(),
  106. })
  107. export function apply(ctx: Context, config: Config): void {
  108. ctx.subagents.registerProvider(new MockSubagentProvider(config.name, config))
  109. }