index.ts 3.9 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495
  1. /**
  2. * The in-process FORK subagent backend: registers a {@link SubagentProvider} on
  3. * `ctx.subagents` that runs each child as a child {@link Agent} SEEDED with a prefix of the
  4. * parent's session log — so the child inherits the parent's conversation context instead of
  5. * starting fresh. The seed ends at the last `turn/end`: the current tool-call turn is
  6. * unbalanced and cannot be replayed as a valid child session.
  7. * @module @deepseek-ai/dsh-subagent-fork-in-process
  8. */
  9. import type { Context } from '@deepseek-ai/cordis'
  10. import z from '@deepseek-ai/schemastery'
  11. import type { SessionEvent } from '@deepseek-ai/dsh-session'
  12. import type { Agent } from '@deepseek-ai/dsh-agent'
  13. import type {
  14. ContinuableCreateRequest,
  15. ContinuableCreateSpec,
  16. ResolvedSubagentStartRequest,
  17. SubagentCapabilities,
  18. SubagentProvider,
  19. } from '@deepseek-ai/dsh-subagent'
  20. import { startInProcessRun } from '@deepseek-ai/dsh-subagent-in-process-driver'
  21. export const name = 'subagent-fork-in-process'
  22. // `tools` is deliberately NOT injected — same rationale as subagent-spawn-in-process: the
  23. // per-run structured runtime gates its capture-tool registration on `tools`
  24. // itself, so this backend's apply timing (and the delegation tool's position
  25. // in the model-visible tool list) is unchanged by structured output.
  26. export const inject = ['subagents']
  27. /** Config: the registry name to register the provider under. */
  28. export interface Config {
  29. /** Provider name on `ctx.subagents` (default `fork`). */
  30. providerName: string
  31. }
  32. export const Config: z<Config> = z.object({
  33. providerName: z.string().default('fork'),
  34. })
  35. /**
  36. * The balanced completed-turn prefix of `parent`'s log: every event up to and including the
  37. * last `turn/end`. The in-flight turn is excluded; before any completed turn the child starts
  38. * fresh. Because live sequence numbers equal array indexes, the result remains a valid seed
  39. * beginning at sequence zero.
  40. * @param parent - the agent whose session log to slice.
  41. * @returns the seed events, contiguous from seq 0; empty when no turn has completed.
  42. */
  43. function completedTurnPrefix(parent: Agent): SessionEvent[] {
  44. const events = parent.session.snapshotEvents()
  45. const lastEnd = events.findLast(e => e.type === 'turn/end')
  46. if (lastEnd === undefined) return []
  47. // seq === array index (the append contract), so slice up to and including it.
  48. return events.slice(0, lastEnd.seq + 1)
  49. }
  50. /**
  51. * The fork provider. Supports `depthLimit` and `outputSchema` (via the shared
  52. * in-process structured runtime), `agentOptions` (merged over the parent
  53. * route), and `toolFilter`/`persona` (scoped restrict() and a scoped shadowing
  54. * persona section).
  55. */
  56. class ForkInProcessProvider implements SubagentProvider {
  57. readonly capabilities: SubagentCapabilities = {
  58. agentOptions: true,
  59. outputSchema: true,
  60. depthLimit: true,
  61. toolFilter: true,
  62. persona: true,
  63. }
  64. // Context contract: a forked child IS seeded with the parent's completed-turn prefix.
  65. readonly inheritsParentContext = true
  66. constructor(readonly name: string) {}
  67. start(request: ResolvedSubagentStartRequest) {
  68. const seed = completedTurnPrefix(request.parent)
  69. return startInProcessRun(request, {
  70. // Only pass a seed when there's a completed turn to inherit; an empty seed
  71. // is equivalent to a fresh child, so omit it to keep the session unseeded.
  72. ...seed.length > 0 ? { seed } : {},
  73. })
  74. }
  75. prepareContinuable(request: ContinuableCreateRequest): Promise<ContinuableCreateSpec> {
  76. // The fork prefix is captured ONCE, at creation: it becomes part of the
  77. // child's own durable transcript, so a later cold resume replays that
  78. // prefix instead of re-forking the parent's newer history.
  79. const seed = completedTurnPrefix(request.parent)
  80. return Promise.resolve(seed.length > 0 ? { seed } : {})
  81. }
  82. }
  83. export function apply(ctx: Context, config: Config): void {
  84. ctx.subagents.registerProvider(new ForkInProcessProvider(config.providerName))
  85. }