@vellumai/assistant 0.8.7-dev.202606052232.2ddc989 → 0.8.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/bun.lock +2 -2
  2. package/docs/plugins.md +832 -0
  3. package/examples/plugins/echo/README.md +60 -61
  4. package/examples/plugins/echo/package.json +2 -1
  5. package/examples/plugins/echo/register.ts +143 -0
  6. package/node_modules/@vellumai/skill-host-contracts/src/skill-host.ts +6 -7
  7. package/openapi.yaml +5 -15
  8. package/package.json +2 -2
  9. package/src/__tests__/agent-loop-exit-reason.test.ts +56 -3
  10. package/src/__tests__/anthropic-provider.test.ts +1 -1
  11. package/src/__tests__/app-control-flow.test.ts +1 -1
  12. package/src/__tests__/app-dir-path-guard.test.ts +0 -1
  13. package/src/__tests__/approval-routes-http.test.ts +1 -4
  14. package/src/__tests__/channel-approval-routes.test.ts +1 -1
  15. package/src/__tests__/channel-approvals.test.ts +1 -1
  16. package/src/__tests__/circuit-breaker-pipeline.test.ts +405 -0
  17. package/src/__tests__/compaction-pipeline.test.ts +210 -0
  18. package/src/__tests__/compaction-timeout-recovery.test.ts +251 -0
  19. package/src/__tests__/conversation-agent-loop-disk-pressure.test.ts +3 -0
  20. package/src/__tests__/conversation-agent-loop-inference-profile.test.ts +3 -0
  21. package/src/__tests__/conversation-agent-loop-overflow.test.ts +7 -3
  22. package/src/__tests__/conversation-agent-loop.test.ts +39 -42
  23. package/src/__tests__/conversation-clean-command.test.ts +2 -5
  24. package/src/__tests__/conversation-provider-retry-repair.test.ts +5 -4
  25. package/src/__tests__/conversation-runtime-assembly.test.ts +71 -140
  26. package/src/__tests__/conversation-runtime-workspace.test.ts +27 -108
  27. package/src/__tests__/conversation-starter-routes.test.ts +6 -14
  28. package/src/__tests__/conversation-workspace-cache-state.test.ts +16 -17
  29. package/src/__tests__/conversation-workspace-injection.test.ts +1 -61
  30. package/src/__tests__/conversation-workspace-tool-tracking.test.ts +6 -7
  31. package/src/__tests__/db-acp-history.test.ts +0 -101
  32. package/src/__tests__/dynamic-page-surface.test.ts +0 -31
  33. package/src/__tests__/file-write-tool.test.ts +0 -63
  34. package/src/__tests__/gateway-only-guard.test.ts +2 -12
  35. package/src/__tests__/guardian-grant-minting.test.ts +1 -1
  36. package/src/__tests__/guardian-routing-invariants.test.ts +4 -2
  37. package/src/__tests__/handlers-user-message-approval-consumption.test.ts +1 -1
  38. package/src/__tests__/heartbeat-disk-pressure.test.ts +0 -1
  39. package/src/__tests__/heartbeat-service.test.ts +0 -1
  40. package/src/__tests__/host-app-control-routes.test.ts +1 -1
  41. package/src/__tests__/host-cu-routes-targeted.test.ts +3 -3
  42. package/src/__tests__/injector-background-turn.test.ts +1 -1
  43. package/src/__tests__/injector-chain.test.ts +6 -34
  44. package/src/__tests__/injector-disk-pressure.test.ts +34 -77
  45. package/src/__tests__/injector-document-comments.test.ts +1 -1
  46. package/src/__tests__/list-messages-hidden-metadata.test.ts +0 -38
  47. package/src/__tests__/memory-v2-static-injector.test.ts +1 -1
  48. package/src/__tests__/{overflow-reduction-loop.test.ts → overflow-reduce-pipeline.test.ts} +284 -64
  49. package/src/__tests__/pipeline-runner.test.ts +554 -0
  50. package/src/__tests__/plugin-api-shim.test.ts +6 -3
  51. package/src/__tests__/plugin-bootstrap.test.ts +23 -12
  52. package/src/__tests__/plugin-registry.test.ts +49 -3
  53. package/src/__tests__/plugin-types.test.ts +70 -0
  54. package/src/__tests__/reaction-persistence.test.ts +1 -1
  55. package/src/__tests__/send-endpoint-busy.test.ts +1 -4
  56. package/src/__tests__/skill-feature-flags-integration.test.ts +0 -33
  57. package/src/__tests__/subagent-call-site-routing.test.ts +1 -1
  58. package/src/__tests__/subagent-fork-notifications.test.ts +3 -1
  59. package/src/__tests__/subagent-fork-spawn.test.ts +1 -1
  60. package/src/__tests__/subagent-manager-notify.test.ts +3 -1
  61. package/src/__tests__/subagent-notify-parent.test.ts +3 -1
  62. package/src/__tests__/subagent-spawn-tool-fork.test.ts +1 -1
  63. package/src/__tests__/user-plugin-loader.test.ts +286 -54
  64. package/src/acp/__tests__/client-handler.test.ts +0 -40
  65. package/src/acp/__tests__/prepare-agent-env.test.ts +0 -137
  66. package/src/acp/__tests__/session-manager-persistence.test.ts +28 -95
  67. package/src/acp/agent-process.ts +1 -61
  68. package/src/acp/client-handler.ts +0 -31
  69. package/src/acp/prepare-agent-env.ts +29 -83
  70. package/src/acp/resolve-agent.test.ts +7 -320
  71. package/src/acp/resolve-agent.ts +18 -182
  72. package/src/acp/session-manager.ts +73 -495
  73. package/src/acp/types.ts +0 -8
  74. package/src/agent/compaction-circuit.ts +102 -60
  75. package/src/agent/loop.ts +59 -32
  76. package/src/api/responses/conversation-message.ts +1 -7
  77. package/src/approvals/guardian-request-resolvers.ts +1 -1
  78. package/src/background-wake/next-wake.ts +0 -1
  79. package/src/config/__tests__/feature-flag-registry-guard.test.ts +2 -2
  80. package/src/config/acp-defaults.test.ts +0 -10
  81. package/src/config/acp-defaults.ts +0 -6
  82. package/src/config/bundled-skills/acp/SKILL.md +31 -83
  83. package/src/config/bundled-skills/acp/TOOLS.json +4 -4
  84. package/src/config/bundled-skills/app-builder/SKILL.md +381 -224
  85. package/src/config/bundled-skills/app-builder/TOOLS.json +0 -29
  86. package/src/config/bundled-skills/document-editor/SKILL.md +23 -28
  87. package/src/config/bundled-skills/document-editor/TOOLS.json +1 -1
  88. package/src/config/bundled-tool-registry.ts +0 -2
  89. package/src/config/feature-flag-registry.json +5 -14
  90. package/src/config/schemas/heartbeat.ts +0 -9
  91. package/src/context/strip-injections.ts +2 -8
  92. package/src/context/window-manager.ts +1 -2
  93. package/src/daemon/conversation-agent-loop-handlers.ts +11 -0
  94. package/src/daemon/conversation-agent-loop.ts +279 -62
  95. package/src/daemon/conversation-runtime-assembly.ts +69 -106
  96. package/src/daemon/conversation-store.ts +90 -9
  97. package/src/daemon/conversation-workspace.ts +0 -17
  98. package/src/daemon/conversation.ts +6 -0
  99. package/src/daemon/external-plugins-bootstrap.ts +11 -11
  100. package/src/daemon/handlers/conversations.ts +1 -3
  101. package/src/daemon/handlers/skills.ts +1 -4
  102. package/src/daemon/lifecycle.ts +0 -21
  103. package/src/daemon/server.ts +0 -2
  104. package/src/heartbeat/__tests__/heartbeat-service.test.ts +0 -3
  105. package/src/heartbeat/heartbeat-run-store.ts +1 -23
  106. package/src/heartbeat/heartbeat-service.ts +0 -26
  107. package/src/ipc/__tests__/browser-ipc.test.ts +1 -1
  108. package/src/ipc/__tests__/ui-request-route.test.ts +3 -3
  109. package/src/ipc/skill-routes/__tests__/memory.test.ts +0 -15
  110. package/src/ipc/skill-routes/memory.ts +2 -4
  111. package/src/memory/conversation-starter-checkpoints.ts +0 -1
  112. package/src/memory/db-init.ts +0 -2
  113. package/src/memory/job-handlers/conversation-starters.ts +2 -13
  114. package/src/memory/jobs-worker.ts +1 -1
  115. package/src/memory/migrations/index.ts +0 -1
  116. package/src/memory/schema/acp.ts +0 -4
  117. package/src/memory/v2/__tests__/consolidation-job.test.ts +3 -3
  118. package/src/memory/v2/consolidation-job.ts +4 -13
  119. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/assign.test.ts +4 -4
  120. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/live-integration.test.ts +4 -4
  121. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/maintain-job.test.ts +5 -5
  122. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/orchestrate.test.ts +3 -3
  123. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/reconcile.test.ts +2 -2
  124. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/render-injection.test.ts +1 -1
  125. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/router.test.ts +3 -3
  126. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selection-log-store.test.ts +8 -8
  127. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/selector.test.ts +3 -3
  128. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/shadow-plugin.test.ts +12 -12
  129. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/assign.ts +5 -5
  130. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/capabilities.ts +2 -2
  131. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/maintain-job.ts +8 -8
  132. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/page-content.ts +2 -2
  133. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/provider-blocks.ts +1 -1
  134. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/reconcile.ts +3 -3
  135. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/render-injection.ts +1 -1
  136. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/router.ts +3 -3
  137. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selection-log-store.ts +4 -4
  138. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/selector.ts +4 -4
  139. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/shadow-plugin.ts +90 -28
  140. package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/tree.ts +1 -1
  141. package/src/plugin-api/index.ts +5 -0
  142. package/src/plugins/defaults/circuit-breaker/middlewares/circuitBreaker.ts +93 -0
  143. package/src/plugins/defaults/{memory-v3-shadow → circuit-breaker}/package.json +2 -2
  144. package/src/plugins/defaults/circuit-breaker/register.ts +39 -0
  145. package/src/plugins/defaults/compaction/middlewares/compaction.ts +25 -0
  146. package/src/plugins/defaults/compaction/package.json +1 -1
  147. package/src/plugins/defaults/compaction/register.ts +19 -8
  148. package/src/plugins/defaults/compaction/terminal.ts +73 -0
  149. package/src/plugins/defaults/index.ts +5 -3
  150. package/src/plugins/defaults/{memory-retrieval/injectors.ts → injectors/register.ts} +7 -45
  151. package/src/plugins/defaults/memory-retrieval/hooks/post-compact.ts +7 -11
  152. package/src/plugins/defaults/memory-retrieval/injector-chain.ts +2 -2
  153. package/src/plugins/defaults/overflow-reduce/middlewares/overflowReduce.ts +126 -0
  154. package/src/plugins/defaults/overflow-reduce/package.json +15 -0
  155. package/src/plugins/defaults/overflow-reduce/register.ts +42 -0
  156. package/src/plugins/external-api.ts +2 -2
  157. package/src/plugins/pipeline.ts +293 -6
  158. package/src/plugins/registry.ts +37 -9
  159. package/src/plugins/types.ts +336 -32
  160. package/src/plugins/user-loader.ts +127 -30
  161. package/src/proactive-artifact/aux-message-injector.ts +1 -1
  162. package/src/proactive-artifact/job.test.ts +1 -1
  163. package/src/prompts/__tests__/system-prompt.test.ts +0 -6
  164. package/src/prompts/templates/BOOTSTRAP-ACTIVATION-RAIL.md +2 -4
  165. package/src/runtime/__tests__/agent-wake.test.ts +5 -5
  166. package/src/runtime/__tests__/interactive-ui.test.ts +1 -1
  167. package/src/runtime/agent-wake.ts +3 -0
  168. package/src/runtime/assistant-event-hub.ts +1 -1
  169. package/src/runtime/channel-approvals.ts +1 -1
  170. package/src/runtime/interactive-ui.ts +1 -1
  171. package/src/runtime/routes/__tests__/acp-routes.test.ts +55 -283
  172. package/src/runtime/routes/__tests__/conversation-list-routes.test.ts +1 -1
  173. package/src/runtime/routes/__tests__/surface-action-routes.test.ts +4 -5
  174. package/src/runtime/routes/__tests__/surface-content-routes.test.ts +1 -4
  175. package/src/runtime/routes/acp-routes.test.ts +25 -89
  176. package/src/runtime/routes/acp-routes.ts +29 -81
  177. package/src/runtime/routes/approval-routes.ts +1 -1
  178. package/src/runtime/routes/browser-routes.ts +1 -1
  179. package/src/runtime/routes/browser-tabs-routes.ts +10 -6
  180. package/src/runtime/routes/conversation-cli-routes.ts +1 -1
  181. package/src/runtime/routes/conversation-list-routes.ts +1 -1
  182. package/src/runtime/routes/conversation-query-routes.ts +1 -1
  183. package/src/runtime/routes/conversation-routes.ts +2 -15
  184. package/src/runtime/routes/conversation-starter-routes.ts +7 -13
  185. package/src/runtime/routes/conversations-import-routes.ts +7 -24
  186. package/src/runtime/routes/host-app-control-routes.ts +1 -1
  187. package/src/runtime/routes/host-cu-routes.ts +1 -1
  188. package/src/runtime/routes/identity-routes.ts +3 -18
  189. package/src/runtime/routes/inbound-message-handler.ts +1 -1
  190. package/src/runtime/routes/memory-v3-routes.ts +6 -16
  191. package/src/runtime/routes/playground/helpers.ts +1 -1
  192. package/src/runtime/routes/surface-conversation-resolver.ts +3 -4
  193. package/src/runtime/routes/work-items-routes.ts +4 -2
  194. package/src/runtime/services/conversation-serializer.ts +1 -1
  195. package/src/signals/cancel.ts +4 -2
  196. package/src/subagent/manager.ts +5 -17
  197. package/src/tools/acp/list-agents.test.ts +1 -7
  198. package/src/tools/acp/spawn.test.ts +55 -158
  199. package/src/tools/acp/spawn.ts +72 -47
  200. package/src/tools/acp/steer.test.ts +8 -105
  201. package/src/tools/acp/steer.ts +17 -48
  202. package/src/tools/apps/executors.ts +8 -13
  203. package/src/tools/filesystem/write.ts +0 -34
  204. package/src/tools/subagent/spawn.ts +4 -2
  205. package/src/tools/ui-surface/definitions.ts +4 -25
  206. package/src/workspace/migrations/051-seed-conversation-summarization-callsite.ts +5 -4
  207. package/src/workspace/migrations/097-enable-adaptive-thinking-managed-profiles.ts +45 -69
  208. package/examples/plugins/echo/hooks/post-tool-use.ts +0 -18
  209. package/examples/plugins/echo/hooks/stop.ts +0 -16
  210. package/examples/plugins/echo/hooks/user-prompt-submit.ts +0 -18
  211. package/examples/plugins/echo/src/emit.ts +0 -19
  212. package/src/__tests__/compaction-circuit.test.ts +0 -258
  213. package/src/__tests__/compaction-direct.test.ts +0 -132
  214. package/src/__tests__/conversations-import-system-filter.test.ts +0 -101
  215. package/src/acp/__tests__/agent-process.test.ts +0 -161
  216. package/src/acp/__tests__/helpers/acp-history-db.ts +0 -82
  217. package/src/acp/__tests__/helpers/exec-file-stub.ts +0 -101
  218. package/src/acp/__tests__/session-manager-resume.test.ts +0 -736
  219. package/src/acp/auto-install.test.ts +0 -196
  220. package/src/acp/auto-install.ts +0 -177
  221. package/src/acp/feature-gate.test.ts +0 -48
  222. package/src/acp/feature-gate.ts +0 -34
  223. package/src/acp/resume-hint.ts +0 -25
  224. package/src/config/bundled-skills/app-builder/references/DESIGN_SYSTEM.md +0 -48
  225. package/src/config/bundled-skills/app-builder/references/RESPONSIVE.md +0 -57
  226. package/src/config/bundled-skills/app-builder/references/SLIDES.md +0 -38
  227. package/src/config/bundled-skills/app-builder/tools/app-list.ts +0 -62
  228. package/src/daemon/conversation-registry.ts +0 -159
  229. package/src/daemon/overflow-reduction-loop.ts +0 -230
  230. package/src/memory/migrations/272-acp-session-history-cwd.ts +0 -36
  231. package/src/plugins/defaults/compaction/compact.ts +0 -59
  232. package/src/plugins/defaults/memory-v3-shadow/hooks/post-compact.ts +0 -14
  233. package/src/plugins/defaults/memory-v3-shadow/hooks/user-prompt-submit.ts +0 -19
  234. package/src/plugins/defaults/memory-v3-shadow/injector.ts +0 -75
  235. package/src/plugins/defaults/memory-v3-shadow/register.ts +0 -26
  236. package/src/tools/acp/context.ts +0 -20
  237. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/capabilities.test.ts +0 -0
  238. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/core.test.ts +0 -0
  239. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/eval-turns.json +0 -0
  240. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/fixtures/live-turns.json +0 -0
  241. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/health.test.ts +0 -0
  242. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/needle.test.ts +0 -0
  243. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/provider-blocks.test.ts +0 -0
  244. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/snapshot.test.ts +0 -0
  245. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/tree.test.ts +0 -0
  246. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/types.test.ts +0 -0
  247. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-eviction.test.ts +0 -0
  248. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/__tests__/working-set-skeleton.test.ts +0 -0
  249. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/core.ts +0 -0
  250. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/README.md +0 -0
  251. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/assignments.json +0 -0
  252. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/core.json +0 -0
  253. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-x.md +0 -0
  254. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-a/topic-y.md +0 -0
  255. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/data/leaves/domain-b/topic-z.md +0 -0
  256. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/health.ts +0 -0
  257. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/llm-retry.ts +0 -0
  258. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/needle.ts +0 -0
  259. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/orchestrate.ts +0 -0
  260. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/snapshot.ts +0 -0
  261. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/types.ts +0 -0
  262. /package/src/{plugins/defaults/memory-v3-shadow → memory/v3}/working-set.ts +0 -0
@@ -0,0 +1,832 @@
1
+ # Plugins
2
+
3
+ > **Note:** This guide documents the **legacy** plugin architecture — the
4
+ > in-tree `register.ts` + middleware-pipeline system. We are converging on
5
+ > the schema at
6
+ > [`experimental/plugins/README.md`](../../experimental/plugins/README.md)
7
+ > (file-based discovery, a `package.json` manifest, the
8
+ > `@vellumai/plugin-api` public contract). The plan is to reshape this guide
9
+ > section by section until it matches that one; wherever the two still
10
+ > differ is the next piece of consolidation work. New plugins should target
11
+ > the modern schema.
12
+
13
+ Plugins extend the assistant's default capabilities using hooks, tools,
14
+ skills, and more.
15
+
16
+ If you're authoring a plugin against the current convention, this file is
17
+ your map. Read [`examples/plugins/echo/`](../examples/plugins/echo/README.md)
18
+ alongside, it's the canonical reference implementation and exercises every
19
+ wired surface.
20
+
21
+ ## Table of contents
22
+
23
+ - [TL;DR](#tldr)
24
+ - [What a plugin can contribute today](#what-a-plugin-can-contribute-today)
25
+ - [Anatomy of a plugin](#anatomy-of-a-plugin)
26
+ - [Where plugins live](#where-plugins-live)
27
+ - [Manifest](#manifest)
28
+ - [Registration](#registration)
29
+ - [Middleware patterns](#middleware-patterns)
30
+ - [Hooks](#hooks)
31
+ - [Pipeline reference](#pipeline-reference)
32
+ - [Timeouts](#timeouts)
33
+ - [Strict-fail semantics](#strict-fail-semantics)
34
+ - [Credentials and config](#credentials-and-config)
35
+ - [Tool, route, and skill contributions](#tool-route-and-skill-contributions)
36
+ - [Cross-plugin communication](#cross-plugin-communication)
37
+ - [Hot reload](#hot-reload)
38
+ - [Troubleshooting](#troubleshooting)
39
+
40
+ ---
41
+
42
+ ## TL;DR
43
+
44
+ 1. Create a directory `<workspaceDir>/plugins/my-plugin/`.
45
+ 2. Drop a `package.json` with a `name` and a `peerDependencies["@vellumai/plugin-api"]` semver range.
46
+ 3. Add `middlewares/<name>.ts` files (default export = middleware function).
47
+ 4. Add `tools/<name>.ts` files (default export = tool definition).
48
+ 5. Restart the assistant — the loader scans `<workspaceDir>/plugins/*` and
49
+ registers the plugin on startup.
50
+
51
+ ## What a plugin can contribute today
52
+
53
+ | Surface | Where | Discovery |
54
+ | -------------------------- | ------------------- | ------------------------------------------------- |
55
+ | Pipeline middleware | `plugin.middleware` | keyed by pipeline name in `PipelineMiddlewareMap` |
56
+ | Model-visible tools | `plugin.tools` | each `PluginToolRegistration` |
57
+ | HTTP routes | `plugin.routes` | each `PluginRouteRegistration` |
58
+ | Skills | `plugin.skills` | each `PluginSkillRegistration` |
59
+ | System-prompt injectors | `plugin.injectors` | each `Injector` |
60
+ | Lifecycle & per-turn hooks | `plugin.hooks` | keyed by hook name (`init`, `shutdown`, …) |
61
+
62
+ The modern schema wires only **hooks** and **tools**; the middleware
63
+ pipelines, routes, skills, and injectors above are the surfaces that still
64
+ have no modern equivalent.
65
+
66
+ ---
67
+
68
+ ## Anatomy of a plugin
69
+
70
+ A plugin is a directory that exports a single `register.ts` (or compiled
71
+ `register.js`) entry point. That file builds a `Plugin` object and passes
72
+ it to `registerPlugin()` as an import-time side effect. Everything else —
73
+ pipeline middleware, lifecycle hooks, model-visible capabilities — hangs
74
+ off that one `Plugin` object.
75
+
76
+ ```
77
+ my-plugin/
78
+ ├── package.json # Node/Bun package metadata
79
+ ├── README.md # optional — human docs
80
+ └── register.ts # the entry point the assistant imports
81
+ ```
82
+
83
+ The `Plugin` shape is declared in
84
+ [`assistant/src/plugins/types.ts`](../src/plugins/types.ts):
85
+
86
+ ```typescript
87
+ export interface Plugin {
88
+ manifest: PluginManifest;
89
+ hooks?: PluginHooks;
90
+ tools?: Tool[];
91
+ routes?: PluginRouteRegistration[];
92
+ skills?: PluginSkillRegistration[];
93
+ injectors?: Injector[];
94
+ middleware?: Partial<PipelineMiddlewareMap>;
95
+ }
96
+ ```
97
+
98
+ Every field except `manifest` is optional. A plugin that only contributes
99
+ a hook doesn't need tools or routes; a plugin that only contributes a
100
+ skill can omit everything else. Lifecycle and per-turn behavior live under
101
+ `hooks` (see [Hooks](#hooks)).
102
+
103
+ ## Where plugins live
104
+
105
+ The assistant scans `<workspaceDir>/plugins/*` at startup. Any subdirectory
106
+ containing `register.js` or `register.ts` is dynamic-imported once. The
107
+ loader lives in
108
+ [`assistant/src/plugins/user-loader.ts`](../src/plugins/user-loader.ts) and
109
+ has three key properties:
110
+
111
+ - **Compiled wins.** If both `register.js` and `register.ts` are present,
112
+ the compiled `.js` file is loaded. This matches how the compiled
113
+ assistant binary resolves modules in production.
114
+ - **Per-plugin isolation.** If one plugin throws at import time, the error
115
+ is logged with the plugin directory and the loader moves on. Other
116
+ plugins still load. One broken plugin cannot brick the assistant.
117
+ - **Per-instance.** The scan runs under `vellumRoot()`. Each assistant
118
+ instance loads its own plugin set.
119
+
120
+ The loader runs after first-party plugin registrations and before
121
+ `bootstrapPlugins()` invokes every plugin's `init()`.
122
+
123
+ ## Manifest
124
+
125
+ The manifest is static metadata validated by the registry at registration
126
+ time. Its shape (see
127
+ [`types.ts`](../src/plugins/types.ts)):
128
+
129
+ ```typescript
130
+ export interface PluginManifest {
131
+ name: string; // kebab-case, unique
132
+ version: string; // semver, informational
133
+ requiresCredential?: string[]; // credential keys resolved before init()
134
+ requiresFlag?: string[]; // feature flag keys that must all be enabled
135
+ config?: unknown; // Zod-like parser for plugins.<name>
136
+ }
137
+ ```
138
+
139
+ | Field | Required | Purpose |
140
+ | -------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
141
+ | `name` | yes | Unique plugin identifier. Duplicate names fail registration. Used as the directory under `<workspaceDir>/plugins-data/<name>/` and the attribution tag in logs. |
142
+ | `version` | yes | Plugin's own semver. Informational — the registry does not compare it. |
143
+ | `requiresCredential` | no | Credential keys the plugin needs. The bootstrap resolves them via the credential store before `init()` runs and hands the values to the plugin in `ctx.credentials`. A missing credential fails startup with a clear error. |
144
+ | `requiresFlag` | no | Assistant feature-flag keys that must all be ON for the plugin to activate. If any listed flag is disabled at bootstrap, the plugin is skipped entirely: `init()` is not invoked and no tools, routes, skills, or shutdown hooks are registered for it. See [Feature-flag gating](#feature-flag-gating) below. |
145
+ | `config` | no | A parser-like validator (Zod schema, or any object with a `.parse(input)` method). If supplied, the bootstrap validates `config.plugins.<name>` through it before passing the result into `init()`. |
146
+
147
+ ### Host-compat: `peerDependencies["@vellumai/plugin-api"]`
148
+
149
+ Plugins declare which assistant versions they support via standard
150
+ `peerDependencies` in their `package.json`:
151
+
152
+ ```json
153
+ {
154
+ "name": "@me/my-logger",
155
+ "version": "1.2.3",
156
+ "peerDependencies": {
157
+ "@vellumai/plugin-api": "^0.8.0"
158
+ }
159
+ }
160
+ ```
161
+
162
+ At load time, the external-plugin loader resolves the assistant's running
163
+ version and runs `semver.satisfies(assistantVersion, range)` against the
164
+ declared range. The contract is currently soft while the plugin-installation
165
+ flow is in flux:
166
+
167
+ - **Range satisfied** — plugin loads.
168
+ - **Range not satisfied** — loader logs an error (`log.error`) and loads
169
+ the plugin anyway.
170
+ - **Range unparseable** — loader logs an error and loads the plugin anyway.
171
+ - **`@vellumai/plugin-api` peerDep absent** — loader logs a warning and
172
+ loads the plugin without a host-compat claim.
173
+
174
+ Once the install flow settles, the two error-logging branches above will
175
+ harden into hard rejections (with per-plugin isolation catching the
176
+ throw so one bad plugin can't brick the rest of the registry).
177
+
178
+ In-tree default plugins do not declare a peerDep — they ship with the
179
+ assistant binary and are version-locked by construction.
180
+
181
+ ### Example manifest
182
+
183
+ ```typescript
184
+ const manifest: PluginManifest = {
185
+ name: "my-logger",
186
+ version: "1.2.3",
187
+ requiresCredential: ["LOGGER_API_KEY"],
188
+ requiresFlag: ["my-logger-enabled"],
189
+ config: z.object({
190
+ endpoint: z.string().url(),
191
+ sampleRate: z.number().min(0).max(1).default(0.1),
192
+ }),
193
+ };
194
+ ```
195
+
196
+ ### Feature-flag gating
197
+
198
+ `manifest.requiresFlag` lists one or more **assistant-scope** feature-flag
199
+ keys (the same keys declared in
200
+ `meta/feature-flags/feature-flag-registry.json`). The bootstrap checks each
201
+ key against `isAssistantFeatureFlagEnabled` before touching the plugin. If
202
+ **any** listed flag is disabled, the plugin is skipped entirely for the
203
+ duration of this assistant boot:
204
+
205
+ - `init()` is **not** invoked.
206
+ - `tools`, `routes`, and `skills` are **not** registered.
207
+ - No shutdown hook entry is installed, so a plugin skipped at boot has
208
+ nothing to tear down on shutdown.
209
+
210
+ Flag state is resolved once at bootstrap time. Flipping a `requiresFlag`
211
+ key at runtime does not hot-reload the plugin — restart the assistant
212
+ after changing the flag to pick up the new state. An empty `requiresFlag` (or
213
+ the field being absent) means the plugin activates unconditionally.
214
+
215
+ The skip path emits a single `info`-level log line naming both the plugin
216
+ and the disabled flag, so operators can diagnose "why isn't my plugin
217
+ loading?" at a glance:
218
+
219
+ ```
220
+ plugins-bootstrap skipping plugin my-logger: feature flag my-logger-enabled is disabled
221
+ ```
222
+
223
+ **Cross-repo note:** new flag keys used here must be declared in the
224
+ assistant-scope section of
225
+ `meta/feature-flags/feature-flag-registry.json` (and provisioned in the
226
+ platform's Terraform configuration). See the root `CLAUDE.md`'s "Assistant
227
+ Feature Flags" section for the full procedure.
228
+
229
+ ## Registration
230
+
231
+ A plugin's `register.ts` calls `registerPlugin()` at module load time. The
232
+ function is exposed via the `globalThis.__vellumPluginRuntime` bridge so the
233
+ plugin file does not need to import from the daemon's source tree:
234
+
235
+ ```typescript
236
+ import type { Plugin } from "<path-to-assistant>/src/plugins/types.js";
237
+
238
+ interface VellumPluginRuntime {
239
+ readonly version: 1;
240
+ readonly registerPlugin: (plugin: Plugin) => void;
241
+ readonly assistantEventHub: import("<path-to-assistant>/src/runtime/assistant-event-hub.js").AssistantEventHub;
242
+ readonly getSecureKeyAsync: (account: string) => Promise<string | undefined>;
243
+ }
244
+
245
+ const runtime = (globalThis as { __vellumPluginRuntime?: VellumPluginRuntime })
246
+ .__vellumPluginRuntime;
247
+ if (!runtime || runtime.version !== 1) {
248
+ throw new Error(
249
+ "vellum plugin runtime not available — install a recent assistant build",
250
+ );
251
+ }
252
+ const { registerPlugin } = runtime;
253
+
254
+ const myPlugin: Plugin = {
255
+ manifest: {
256
+ name: "my-plugin",
257
+ version: "0.1.0",
258
+ },
259
+ middleware: {
260
+ /* ... */
261
+ },
262
+ };
263
+
264
+ registerPlugin(myPlugin);
265
+ ```
266
+
267
+ **Why the bridge?** When the daemon is a `bun --compile` binary, its modules
268
+ are bundled into the executable. Plugins that import the daemon's modules by
269
+ absolute path (`/abs/path/to/assistant/src/plugins/registry.js`) reload fresh
270
+ disk copies into a separate module graph, and any `registerPlugin()` call in
271
+ the plugin lands in a registry the daemon never reads. The
272
+ `globalThis.__vellumPluginRuntime` handle is the same instance the daemon's
273
+ bundled code holds onto, so plugin registrations always reach the right
274
+ place — whether the daemon was built with `bun --compile` or is running from
275
+ source.
276
+
277
+ Type-only imports (`import type { Plugin } from "..."`) remain free to use
278
+ absolute paths to the assistant source — the TypeScript compiler erases them
279
+ and they have no module-identity effect at runtime.
280
+
281
+ **Rules:**
282
+
283
+ - Exactly one `registerPlugin()` call per plugin. The registry rejects
284
+ duplicate names.
285
+ - `register.ts` must not export named symbols consumed from outside. The
286
+ loader treats the import as side-effect-only.
287
+ - Throwing inside `register.ts` is caught by the loader and logged, then
288
+ the loader moves on. Do not rely on throws to signal "please don't load
289
+ this plugin" — use `requiresFlag` or a guard inside `init()` instead.
290
+ - The file runs before any lifecycle hooks. Keep it fast — heavy work
291
+ belongs in `init()`.
292
+ - The bridge is installed by the daemon before `loadUserPlugins()` runs, so
293
+ the global is always present when a plugin's module body executes.
294
+
295
+ ## Middleware patterns
296
+
297
+ Middleware is the heart of the plugin system. Every pipeline slot uses the
298
+ same onion-style signature:
299
+
300
+ ```typescript
301
+ export type Middleware<A, R> = (
302
+ args: A,
303
+ next: (args: A) => Promise<R>,
304
+ ctx: TurnContext,
305
+ ) => Promise<R>;
306
+ ```
307
+
308
+ The runner composes an array of middleware around a terminal handler. The
309
+ first middleware sees the request first and the response last; the
310
+ terminal runs at the innermost layer. See
311
+ [`assistant/src/plugins/pipeline.ts`](../src/plugins/pipeline.ts) for the
312
+ composition algorithm.
313
+
314
+ Five common patterns emerge from that signature:
315
+
316
+ ### Observe-only
317
+
318
+ Record something without changing the call. Call `next(args)` unchanged,
319
+ return the result unchanged. Wrap the call in `try`/`finally` so your
320
+ observer runs on both success and failure paths.
321
+
322
+ ```typescript
323
+ const observer: Middleware<MemoryArgs, MemoryResult> =
324
+ async function observeRetrieval(args, next, ctx) {
325
+ const start = performance.now();
326
+ let outcome: "success" | "error" = "success";
327
+ try {
328
+ return await next(args);
329
+ } catch (err) {
330
+ outcome = "error";
331
+ throw err;
332
+ } finally {
333
+ const ms = Math.round(performance.now() - start);
334
+ console.error(
335
+ JSON.stringify({ conversationId: args.conversationId, ms, outcome }),
336
+ );
337
+ }
338
+ };
339
+ ```
340
+
341
+ ### Transform input
342
+
343
+ Rewrite `args` before calling downstream. Useful for reshaping the inputs
344
+ (forcing an untrusted read, narrowing the turn the retriever sees).
345
+
346
+ ```typescript
347
+ const untrustedRead: Middleware<MemoryArgs, MemoryResult> =
348
+ async function untrustedRead(args, next, ctx) {
349
+ return next({ ...args, trustContext: undefined });
350
+ };
351
+ ```
352
+
353
+ ### Transform output
354
+
355
+ Call `next(args)` first, then modify the result before returning.
356
+
357
+ ```typescript
358
+ const dropNow: Middleware<MemoryArgs, MemoryResult> = async function dropNow(
359
+ args,
360
+ next,
361
+ ctx,
362
+ ) {
363
+ const result = await next(args);
364
+ return { ...result, nowContent: null };
365
+ };
366
+ ```
367
+
368
+ ### Short-circuit
369
+
370
+ Do not call `next(args)` — return a synthetic result directly. The
371
+ terminal and any inner middleware are skipped. Use this to stub, cache,
372
+ or mock a pipeline.
373
+
374
+ ```typescript
375
+ const skipUntrusted: Middleware<MemoryArgs, MemoryResult> =
376
+ async function skipUntrusted(args, next, ctx) {
377
+ if (!isTrusted(ctx.trust)) {
378
+ return { pkbContent: null, nowContent: null, graphResult: null };
379
+ }
380
+ return next(args);
381
+ };
382
+ ```
383
+
384
+ ### Veto (throw)
385
+
386
+ Throwing from middleware aborts the pipeline. The error propagates out
387
+ through any outer middleware unchanged — there is no internal
388
+ `try`/`catch` around user middleware.
389
+
390
+ ```typescript
391
+ const denyIfUntrusted: Middleware<CompactionArgs, CompactionResult> =
392
+ async function denyIfUntrusted(args, next, ctx) {
393
+ if (!isTrusted(ctx.trust)) {
394
+ throw new Error(`compaction denied by policy`);
395
+ }
396
+ return next(args);
397
+ };
398
+ ```
399
+
400
+ ### Naming middleware
401
+
402
+ Give middleware a stable `name` (via `async function <name>(…)`). The
403
+ pipeline runner pulls `Function.name` into its `chain` log field so
404
+ operators can see the registered chain at a glance:
405
+
406
+ ```
407
+ plugin.pipeline pipeline=compaction chain=["observeCompaction","defaultCompaction"] durationMs=1840 outcome=success
408
+ ```
409
+
410
+ ## Hooks
411
+
412
+ Hooks are the **modern** lifecycle surface: a plugin contributes one
413
+ function per phase under `plugin.hooks`, keyed by hook name. The wired
414
+ hook names live in [`HOOKS`](../src/plugin-api/constants.ts); the context
415
+ shapes and a full authoring walkthrough live in the
416
+ [experimental plugin guide](../../experimental/plugins/README.md#hooks).
417
+
418
+ Every hook implements `PluginHookFn` — it receives a context and either
419
+ mutates it in place (returning `void`) or returns a replacement. Hooks
420
+ from multiple plugins chain in registration order, and defaults register
421
+ first.
422
+
423
+ | Hook | Fires | Context |
424
+ | -------------------- | ----------------------------------------------------------------- | ------------------------- |
425
+ | `init` | Once when the plugin is registered. | `PluginInitContext` |
426
+ | `shutdown` | Once when the plugin is torn down. | `PluginShutdownContext` |
427
+ | `user-prompt-submit` | Once per user turn, before the agent loop receives the messages. | `UserPromptSubmitContext` |
428
+ | `post-tool-use` | Once per tool result, before it joins the provider-bound history. | `PostToolUseContext` |
429
+ | `stop` | Once per run when the model yields a turn with no tool calls. | `StopContext` |
430
+
431
+ ## Pipeline reference
432
+
433
+ Every pipeline slot and its purpose. Type details live in
434
+ [`types.ts`](../src/plugins/types.ts).
435
+
436
+ | Pipeline | Purpose |
437
+ | ---------------- | ------------------------------------------------------------------------------------------------------ |
438
+ | `compaction` | The conversation-compaction step. Wraps `ContextWindowManager.maybeCompact`. |
439
+ | `overflowReduce` | The reducer tier loop invoked when a turn blows the context budget. |
440
+ | `circuitBreaker` | The compaction circuit breaker. Tracks consecutive-failure state, decides whether to open the circuit. |
441
+
442
+ ## Timeouts
443
+
444
+ Each pipeline has a default timeout budget in milliseconds. When the
445
+ budget is exceeded the runner throws `PluginTimeoutError` carrying the
446
+ pipeline name, the offending plugin's name (if known), and the elapsed
447
+ duration. See
448
+ [`assistant/src/plugins/pipeline.ts`](../src/plugins/pipeline.ts) for the
449
+ current values.
450
+
451
+ | Pipeline | Timeout | Rationale |
452
+ | ---------------- | -------- | ---------------------------------------------------------------------------------------------------- |
453
+ | `compaction` | 30000 ms | Summarization involves a provider call; mirrors the pipeline-level budget for LLM-backed operations. |
454
+ | `overflowReduce` | 30000 ms | Iterative compaction; matches the `compaction` budget since each tier step may invoke it. |
455
+ | `circuitBreaker` | 500 ms | Numeric state update — must be near-instant. |
456
+
457
+ `null` timeouts skip the timer entirely. Finite timeouts arm a
458
+ `setTimeout` that races the pipeline via `Promise.race`.
459
+
460
+ ## Strict-fail semantics
461
+
462
+ **Plugin errors and timeouts fail the turn loudly. There is no silent
463
+ fallback to the default behavior.**
464
+
465
+ This is a deliberate design decision. The old inline behavior silently
466
+ absorbed many edge cases (a memory retrieval failure became an empty
467
+ memory block, a compaction error became no compaction, etc.). That made
468
+ debugging production issues miserable because failures disappeared into
469
+ logs nobody checked.
470
+
471
+ With strict-fail:
472
+
473
+ - Any error thrown from middleware propagates up to the caller. The
474
+ pipeline runner does not catch it.
475
+ - Any `PluginTimeoutError` from a budget breach propagates identically.
476
+ - The caller (agent loop, memory subsystem, whoever) decides how to
477
+ degrade. The pipeline itself does not paper over the failure.
478
+ - Exactly one structured log line is emitted per pipeline invocation, in
479
+ a `finally` block, regardless of outcome. It carries `outcome`
480
+ (`"success" | "error" | "timeout"`), `durationMs`, `chain`, plugin
481
+ attribution, and error details when applicable.
482
+
483
+ If you're writing middleware that wants to "try, fall back to default on
484
+ failure," express that at the call site instead — wrap the pipeline
485
+ invocation in your own try/catch. Do not swallow the error inside your
486
+ middleware's `try`/`catch` and silently return a degraded result.
487
+
488
+ ## Credentials and config
489
+
490
+ ### Credentials
491
+
492
+ Declare required credential keys in `manifest.requiresCredential`:
493
+
494
+ ```typescript
495
+ const manifest: PluginManifest = {
496
+ name: "my-plugin",
497
+ version: "1.0.0",
498
+ requiresCredential: ["MY_PLUGIN_API_KEY"],
499
+ };
500
+ ```
501
+
502
+ During bootstrap, the assistant resolves each key through the credential
503
+ store (via `getSecureKeyAsync`). In Docker mode that call goes through
504
+ the CES HTTP API; in local mode it hits the encrypted file store / CES
505
+ RPC backend. The resolved values are handed to your `init()`:
506
+
507
+ ```typescript
508
+ async init(ctx: PluginInitContext) {
509
+ const apiKey = ctx.credentials["MY_PLUGIN_API_KEY"];
510
+ // use it
511
+ }
512
+ ```
513
+
514
+ **Rules:**
515
+
516
+ - Never import the credential store directly. Always go through the
517
+ manifest.
518
+ - Missing credentials fail startup with a clear error naming the plugin
519
+ and the key. There is no silent fallback.
520
+ - Credentials are resolved once at bootstrap. Long-running plugins that
521
+ need rotation must re-resolve through their own mechanism.
522
+
523
+ ### Config
524
+
525
+ Declare a parser-like validator in `manifest.config`:
526
+
527
+ ```typescript
528
+ const configSchema = z.object({
529
+ endpoint: z.string().url(),
530
+ sampleRate: z.number().min(0).max(1).default(0.1),
531
+ });
532
+
533
+ const manifest: PluginManifest = {
534
+ name: "my-plugin",
535
+ version: "1.0.0",
536
+ config: configSchema,
537
+ };
538
+ ```
539
+
540
+ The bootstrap reads `config.plugins.<name>` from the assistant's config
541
+ and calls `manifest.config.parse(raw)`. The parsed result is handed to
542
+ your `init()`:
543
+
544
+ ```typescript
545
+ async init(ctx: PluginInitContext) {
546
+ const cfg = ctx.config as z.infer<typeof configSchema>;
547
+ // use cfg
548
+ }
549
+ ```
550
+
551
+ If you don't supply a validator, the raw config is passed through
552
+ untouched as `unknown` and your plugin must narrow it itself.
553
+
554
+ ### Other init context fields
555
+
556
+ The full `PluginInitContext`:
557
+
558
+ ```typescript
559
+ export interface PluginInitContext {
560
+ config: unknown; // parsed config (or raw if no validator)
561
+ credentials: Record<string, string>; // resolved credentials from requiresCredential
562
+ logger: unknown; // pino child logger, tagged { plugin: <name> }
563
+ pluginStorageDir: string; // <workspaceDir>/plugins-data/<name>/ (created by bootstrap)
564
+ assistantVersion: string; // assistant semver — same value used by the loader
565
+ // against your peerDependencies range
566
+ }
567
+ ```
568
+
569
+ `pluginStorageDir` is a per-plugin writable directory. Use it for
570
+ persistent state — cache files, counters, anything that must survive an
571
+ assistant restart. The bootstrap creates it on demand.
572
+
573
+ ## Tool, route, and skill contributions
574
+
575
+ Plugins can contribute model-visible capabilities alongside their
576
+ middleware. Each is optional.
577
+
578
+ ### Tools (`plugin.tools`)
579
+
580
+ An array of `Tool` objects. The bootstrap registers them with the global
581
+ tool registry after `init()` succeeds, stamping `origin: "plugin"` and
582
+ `owner: { kind: "plugin", id: <plugin.name> }` so they live in a ref-count
583
+ namespace disjoint from real skills (a plugin whose `manifest.name`
584
+ happens to match a skill id cannot collide with that skill's
585
+ registrations).
586
+
587
+ ```typescript
588
+ const myPlugin: Plugin = {
589
+ manifest: {
590
+ /* ... */
591
+ },
592
+ tools: [
593
+ {
594
+ name: "my_tool",
595
+ description: "Does the thing.",
596
+ category: "plugin",
597
+ defaultRiskLevel: "low",
598
+ getDefinition: () => ({
599
+ name: "my_tool",
600
+ description: "Does the thing.",
601
+ input_schema: { type: "object", properties: {}, required: [] },
602
+ }),
603
+ execute: async (input, ctx) => ({ content: "result", isError: false }),
604
+ },
605
+ ],
606
+ };
607
+ ```
608
+
609
+ Tools are unregistered automatically on shutdown. See
610
+ [`assistant/src/tools/types.ts`](../src/tools/types.ts) for the full
611
+ `Tool` interface including optional fields like `executionMode` and
612
+ `executionTarget`.
613
+
614
+ ### Routes (`plugin.routes`)
615
+
616
+ An array of `SkillRoute` objects — the same shape the skill-route
617
+ registry consumes. Registered via `registerSkillRoute` after `init()`
618
+ succeeds; the runtime retains the opaque handle returned by each call
619
+ and uses those handles to unregister the plugin's routes on shutdown.
620
+ Handle-keyed unregistration is deliberate: two owners (plugin vs.
621
+ skill, or plugin vs. plugin) can legitimately declare the same regex,
622
+ and identity matching ensures one owner's teardown cannot evict
623
+ another owner's live routes.
624
+
625
+ ```typescript
626
+ const myPlugin: Plugin = {
627
+ manifest: {
628
+ /* ... */
629
+ },
630
+ routes: [
631
+ {
632
+ pattern: /^\/_plugin\/my-plugin\/status$/,
633
+ methods: ["GET"],
634
+ handler: async (req, match) => new Response("ok"),
635
+ },
636
+ ],
637
+ };
638
+ ```
639
+
640
+ ### Skills (`plugin.skills`)
641
+
642
+ An array of `PluginSkillRegistration` objects. Each becomes a discoverable
643
+ skill under `source: "plugin"` in the model's `skill_load` /
644
+ `skill_execute` flow.
645
+
646
+ ```typescript
647
+ const myPlugin: Plugin = {
648
+ manifest: {
649
+ /* ... */
650
+ },
651
+ skills: [
652
+ {
653
+ id: "my-plugin/do-thing",
654
+ name: "do-thing",
655
+ description: "Does the thing via plugin-contributed skill.",
656
+ body: "# SKILL.md body returned when loaded\n...",
657
+ },
658
+ ],
659
+ };
660
+ ```
661
+
662
+ See
663
+ [`plugin-skill-contributions.ts`](../src/plugins/plugin-skill-contributions.ts)
664
+ for the in-memory registry details and ref-counted lifecycle.
665
+
666
+ ### Injectors (`plugin.injectors`)
667
+
668
+ An array of `Injector` objects that emit system-prompt-time content.
669
+ Each has a stable `name`, an ascending `order` used to position it in the
670
+ injection chain, and a `produce(ctx)` method that returns an
671
+ `InjectionBlock` or `null`.
672
+
673
+ The default injectors use `order` 10 through 70 with gaps of 10, so
674
+ plugin-contributed injectors can slot at `25`, `35`, etc. without
675
+ renumbering.
676
+
677
+ ```typescript
678
+ const myPlugin: Plugin = {
679
+ manifest: {
680
+ /* ... */
681
+ },
682
+ injectors: [
683
+ {
684
+ name: "my-plugin/status",
685
+ order: 25,
686
+ async produce(ctx) {
687
+ return {
688
+ id: "my-plugin/status",
689
+ text: `<my_plugin_status>ok</my_plugin_status>`,
690
+ };
691
+ },
692
+ },
693
+ ],
694
+ };
695
+ ```
696
+
697
+ ## Cross-plugin communication
698
+
699
+ Plugins should not call each other directly. There is no cross-plugin
700
+ import API — a plugin's export surface is intentionally limited to the
701
+ `Plugin` object it registers.
702
+
703
+ For cross-cutting concerns (broadcasting events, reacting to
704
+ system-level changes), use the `assistantEventHub` pub/sub in
705
+ [`runtime/assistant-event-hub.ts`](../src/runtime/assistant-event-hub.ts).
706
+ The hub is the canonical place to publish events from inside the
707
+ assistant process and to subscribe from anywhere that has access to the
708
+ assistant's module graph.
709
+
710
+ Do not add new HTTP endpoints to implement plugin-to-plugin messaging
711
+ inside a single assistant process.
712
+
713
+ ## Hot reload
714
+
715
+ **Not supported in v1.** Registering a plugin takes effect at assistant
716
+ startup only. To pick up a new or modified plugin:
717
+
718
+ ```bash
719
+ vellum restart
720
+ ```
721
+
722
+ The registry's internal state is not mutable at runtime. `init()` and
723
+ `onShutdown()` hooks are fired exactly once per assistant boot.
724
+
725
+ If you need hot reload for development, symlink your plugin directory
726
+ into `<workspaceDir>/plugins/` so edits propagate, and automate the restart
727
+ loop externally.
728
+
729
+ ## Troubleshooting
730
+
731
+ ### `external plugin X: peerDependencies["@vellumai/plugin-api"] requires "<range>" but assistant is <version> — loading anyway`
732
+
733
+ Logged at `error` level. Your plugin's declared
734
+ `peerDependencies["@vellumai/plugin-api"]` range does not include the
735
+ running assistant's version. The plugin still loads while the install
736
+ flow is being shaped, but a future release will turn this into a hard
737
+ rejection. Either widen the range in your `package.json` (typically by
738
+ bumping the major in `^X.Y.Z`) or upgrade the assistant.
739
+
740
+ ### `external plugin X: peerDependencies["@vellumai/plugin-api"] is not a valid semver range — loading anyway`
741
+
742
+ Logged at `error` level, same lenient policy as above. The value declared
743
+ under `peerDependencies["@vellumai/plugin-api"]` is not parseable as a
744
+ semver range. Use a standard range expression such as `^0.8.0`,
745
+ `>=0.8.0 <0.10`, or an exact version.
746
+
747
+ ### `external plugin X missing plugin-api peerDependency — loading without host-compat claim`
748
+
749
+ Warning, not an error. Your plugin's `package.json` does not declare a
750
+ `peerDependencies["@vellumai/plugin-api"]` entry, so the loader has no
751
+ host-compat range to check and loads the plugin without that guard. Add
752
+ the peerDep so future assistant upgrades surface incompatibility before
753
+ the plugin runs.
754
+
755
+ ### "plugin X is already registered"
756
+
757
+ Two plugins tried to register under the same `manifest.name`. Names must
758
+ be globally unique. Rename one, or if this is a dev-reload issue,
759
+ restart the assistant.
760
+
761
+ ### "plugin X requires credential Y but the credential store returned no value"
762
+
763
+ The credential named in `requiresCredential` is not set. Run:
764
+
765
+ ```bash
766
+ vellum credentials set Y
767
+ ```
768
+
769
+ …and restart the assistant.
770
+
771
+ ### "plugin X config validation failed: …"
772
+
773
+ The config block under `config.plugins.<name>` failed the manifest's
774
+ parser. Check your config against the plugin's schema — the error
775
+ message carries the validator's diagnostic.
776
+
777
+ ### `PluginTimeoutError: Plugin pipeline '<name>' timed out after N ms`
778
+
779
+ A plugin's middleware exceeded the pipeline's budget. The offending
780
+ plugin is named in `ctx.pluginName` when available. Tighten the
781
+ middleware (it's probably blocking on I/O it shouldn't) or, if the
782
+ work is genuinely heavy, move it out of the critical path into a
783
+ background job that publishes results through `assistantEventHub`.
784
+
785
+ ### Reading pipeline log records
786
+
787
+ Every pipeline invocation emits one structured line tagged
788
+ `event=plugin.pipeline`. The fields:
789
+
790
+ | Field | Meaning |
791
+ | ------------------------------------------ | ----------------------------------------------------------------------- |
792
+ | `pipeline` | Pipeline name (`compaction`, `overflowReduce`, …). |
793
+ | `chain` | Ordered list of middleware function names, outermost first. |
794
+ | `durationMs` | Total time spent in the composed chain. |
795
+ | `outcome` | `"success"`, `"error"`, or `"timeout"`. |
796
+ | `pluginName` | The specific plugin's name when the runner could attribute the frame. |
797
+ | `timeoutMs` | The configured budget (only when one was set). |
798
+ | `errorName`, `errorMessage`, `errorStack` | Present on failure outcomes. |
799
+ | `requestId`, `conversationId`, `turnIndex` | Per-turn context for correlating with the rest of the assistant's logs. |
800
+
801
+ Pipe the assistant's stderr through `jq` to filter and inspect:
802
+
803
+ ```bash
804
+ tail -f ~/.vellum/daemon.log | jq 'select(.event == "plugin.pipeline")'
805
+ ```
806
+
807
+ To isolate slow pipelines:
808
+
809
+ ```bash
810
+ tail -f ~/.vellum/daemon.log \
811
+ | jq 'select(.event == "plugin.pipeline" and .durationMs > 1000)'
812
+ ```
813
+
814
+ To isolate errors and timeouts:
815
+
816
+ ```bash
817
+ tail -f ~/.vellum/daemon.log \
818
+ | jq 'select(.event == "plugin.pipeline" and .outcome != "success")'
819
+ ```
820
+
821
+ ### Plugin not loading at all
822
+
823
+ - Confirm the directory is under `<workspaceDir>/plugins/`.
824
+ - Confirm it has a `register.ts` or `register.js` at the top level.
825
+ - Check the assistant's stderr for a line like
826
+ `loaded user plugin (side-effect import completed)` or
827
+ `Failed to load user plugin <dir>: <err>`. Import-time throws are
828
+ logged but do not crash the assistant — the plugin is silently skipped
829
+ otherwise.
830
+ - Verify `register.ts` calls `registerPlugin()` exactly once at module
831
+ level. If the call is inside an unrelated conditional or wrapped in
832
+ an async function that is never awaited, the registry won't see it.