index.ts 7.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143
  1. /**
  2. * The ACP server app: the default agent spine ({@link @deepseek-ai/dsh-agent-spine-demo}),
  3. * human-command registry, JSONL session persistence, and the
  4. * {@link @deepseek-ai/dsh-acp} bridge. The app owns those plugins through one
  5. * ordered lifecycle so ACP sessions quiesce before persistence detaches. It
  6. * writes nothing to stdout.
  7. * It pre-creates no agents and leaves adapters, executors, and optional tools to
  8. * the leaf, which must likewise avoid stdout loggers. Named exports are
  9. * required so Loader retains this plugin's `Config` schema (see
  10. * docs/postmortem/0001).
  11. * @module @deepseek-ai/dsh-acp-demo
  12. */
  13. import type { Context } from 'cordis'
  14. import { join } from 'node:path'
  15. import z from 'schemastery'
  16. import * as acp from '@deepseek-ai/dsh-acp'
  17. import CommandService from '@deepseek-ai/dsh-commands'
  18. import * as commandGoal from '@deepseek-ai/dsh-command-goal'
  19. import * as agentCore from '@deepseek-ai/dsh-agent-spine-demo'
  20. import * as workspaceContext from '@deepseek-ai/dsh-workspace-context'
  21. import ToolRegistry, { type Config as ToolsConfig } from '@deepseek-ai/dsh-tools'
  22. import SessionPersistenceJsonl, {
  23. JsonlCompressionSchema,
  24. type JsonlCompression,
  25. } from '@deepseek-ai/dsh-session-persistence-jsonl'
  26. import * as sessionCheckpointPolicy from '@deepseek-ai/dsh-session-checkpoint-policy'
  27. import UserInteractionService from '@deepseek-ai/dsh-user-interaction'
  28. import SessionQuerySqlite from '@deepseek-ai/dsh-session-query-sqlite'
  29. import SessionReferenceService, { type Config as SessionReferenceConfig } from '@deepseek-ai/dsh-session-reference'
  30. export const name = 'acp-demo'
  31. const DEFAULT_PERSISTENCE_ROOT = './.sessions'
  32. /**
  33. * App config: the swappable per-deployment values. `provider` and `model` configure the
  34. * agent template the ACP bridge creates each session's agent from (NOT a
  35. * pre-created agent — ACP creates agents at `session/new`); `persona` is the
  36. * deployment persona (forwarded to the system-prompt plugin); `toolOrder` is
  37. * the explicit model-facing tool order (forwarded to the system-prompt plugin);
  38. * `tools` is the tool registry's config (its presentation `mode`, forwarded
  39. * through agent-spine-demo); `persistenceRoot` is the JSONL backend's directory.
  40. */
  41. export interface Config {
  42. /** Provider route for ACP-created agents. */
  43. provider: string
  44. /** Model name for ACP-created agents (must have a registered adapter). */
  45. model: string
  46. /** Bundled agent-loop concurrency cap; `1` is serial and omission uses its default. */
  47. maxParallelToolCalls?: number
  48. /** Deployment persona (the system-prompt plugin's `persona` config). */
  49. persona?: string
  50. /** Explicit model-facing tool order (the system-prompt plugin's `toolOrder` config; see dsh-system-prompt). */
  51. toolOrder?: string[]
  52. /** Tool-registry config — its presentation `mode` (forwarded through agent-spine-demo; see dsh-tools). */
  53. tools?: ToolsConfig
  54. /** DeepSeek Harness home directory exposed to bash and used for local skill discovery. */
  55. dshHome?: string
  56. /** Fallback session-title limits forwarded through agent-spine-demo. */
  57. sessionTitle?: NonNullable<agentCore.Config['sessionTitle']>
  58. /** Directory for JSONL sessions and the derived query index. Defaults to `./.sessions`. */
  59. persistenceRoot?: string
  60. /** Write delta-chunk runs as packed storage rows (the JSONL backend's `packChunks`). Defaults to `false`. */
  61. packChunks?: boolean
  62. /** JSONL artifact encoding; defaults to checksummed Zstandard frames. */
  63. persistenceCompression?: JsonlCompression
  64. /** Cross-session reference discovery and snapshot byte budgets. */
  65. sessionReferences?: SessionReferenceConfig
  66. /** Controls automatic AGENTS.md/CLAUDE.md loading; configure a byte budget or set `false`. */
  67. workspaceContext: agentCore.Config['workspaceContext']
  68. /** Skill registry, local-provider, and model-facing consumer config forwarded to agent-spine-demo. */
  69. skills?: agentCore.SkillConfig
  70. /** Model-facing bash tool config forwarded through agent-core. */
  71. toolBash?: NonNullable<agentCore.Config['toolBash']>
  72. /** Generic background-task controls forwarded through agent-core; set false to omit their tool surface. */
  73. toolTasks?: NonNullable<agentCore.Config['toolTasks']>
  74. /** Persisted same-session goals; owner defaults enable them, or false disables the stack and command. */
  75. goals?: agentCore.GoalConfig | false
  76. /** Bounded transient model-request retry policy forwarded through agent-core. */
  77. llmRetry?: NonNullable<agentCore.Config['llmRetry']>
  78. }
  79. // Each front door owns a complete, directly readable config schema; extracting
  80. // the common fields would make two small app contracts depend on a new facade.
  81. /* jscpd:ignore-start */
  82. export const Config: z<Config> = z.object({
  83. provider: z.string().required(),
  84. model: z.string().required(),
  85. maxParallelToolCalls: z.number().step(1).min(1),
  86. persona: z.string(),
  87. // The array default is forced to undefined: ABSENT means "lexicographic
  88. // order" (the owning dsh-system-prompt schema does the same), while
  89. // schemastery's native [] default would read as an invalid configured list.
  90. toolOrder: z.array(z.string()).default(undefined as unknown as string[]),
  91. tools: ToolRegistry.Config,
  92. dshHome: z.string(),
  93. sessionTitle: agentCore.SessionTitleConfigSchema,
  94. persistenceRoot: z.string().default(DEFAULT_PERSISTENCE_ROOT),
  95. packChunks: z.boolean().default(false),
  96. persistenceCompression: JsonlCompressionSchema,
  97. sessionReferences: SessionReferenceService.Config,
  98. workspaceContext: z.union([z.const(false), workspaceContext.Config]).required(),
  99. skills: agentCore.SkillConfigSchema,
  100. toolBash: agentCore.ToolBashConfigSchema,
  101. toolTasks: z.union([z.const(false), agentCore.ToolTasksConfigSchema]),
  102. goals: z.union([z.const(false), agentCore.GoalConfigSchema]),
  103. llmRetry: agentCore.LlmRetryConfigSchema,
  104. })
  105. /* jscpd:ignore-end */
  106. /**
  107. * Compose the spine with the ACP front door. The agent-spine-demo bundle pre-creates
  108. * NO agents (its `agents` list defaults to `[]`) and carries the deployment
  109. * `persona`; the JSONL backend and derived query index persist under
  110. * `persistenceRoot`; the ACP bridge owns stdout for JSON-RPC and creates one
  111. * agent per `session/new` from the provider/model pair. The composite effect
  112. * unloads in reverse order, keeping checkpoint and persistence listeners
  113. * attached until ACP agents have flushed their closing events. No logger, no
  114. * `hmr` — stdout stays pure.
  115. */
  116. export function apply(ctx: Context, config: Config): void {
  117. const goals = config.goals ?? {}
  118. const persistenceRoot = config.persistenceRoot ?? DEFAULT_PERSISTENCE_ROOT
  119. ctx.effect(function* () {
  120. yield ctx.plugin(CommandService).dispose
  121. if (goals !== false) yield ctx.plugin(commandGoal).dispose
  122. yield ctx.plugin(agentCore, { ...agentCore.pickSpineConfig(config), goals }).dispose
  123. yield ctx.plugin(UserInteractionService).dispose
  124. // Same rationale as the Config schema above: each front door forwards its own
  125. // persistence passthroughs rather than sharing a facade with stdio-demo.
  126. /* jscpd:ignore-start */
  127. yield ctx.plugin(SessionPersistenceJsonl, {
  128. root: persistenceRoot,
  129. ...config.packChunks !== undefined ? { packChunks: config.packChunks } : {},
  130. ...(config.persistenceCompression === undefined ? {} : { compression: config.persistenceCompression }),
  131. }).dispose
  132. /* jscpd:ignore-end */
  133. yield ctx.plugin(sessionCheckpointPolicy).dispose
  134. yield ctx.plugin(SessionQuerySqlite, { path: join(persistenceRoot, 'session-query.db') }).dispose
  135. yield ctx.plugin(SessionReferenceService, config.sessionReferences ?? {}).dispose
  136. yield ctx.plugin(acp, { provider: config.provider, model: config.model }).dispose
  137. }, 'acp-demo.composition')
  138. }