@oh-my-pi/pi-coding-agent 17.1.5 → 17.1.7

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 (244) hide show
  1. package/CHANGELOG.md +54 -0
  2. package/dist/{CHANGELOG-qs3vd6xf.md → CHANGELOG-5k4dq4g6.md} +54 -0
  3. package/dist/cli.js +3342 -3274
  4. package/dist/types/capability/index.d.ts +1 -1
  5. package/dist/types/capability/types.d.ts +23 -1
  6. package/dist/types/config/settings-schema.d.ts +39 -2
  7. package/dist/types/cursor.d.ts +2 -1
  8. package/dist/types/extensibility/extensions/runner.d.ts +4 -0
  9. package/dist/types/extensibility/shared-events.d.ts +18 -1
  10. package/dist/types/internal-urls/mcp-protocol.d.ts +3 -2
  11. package/dist/types/internal-urls/parse.d.ts +12 -0
  12. package/dist/types/internal-urls/router.d.ts +6 -0
  13. package/dist/types/internal-urls/types.d.ts +6 -0
  14. package/dist/types/lsp/config.d.ts +1 -0
  15. package/dist/types/lsp/types.d.ts +2 -0
  16. package/dist/types/mcp/manager.d.ts +5 -0
  17. package/dist/types/mcp/tool-bridge.d.ts +13 -0
  18. package/dist/types/modes/components/custom-editor.d.ts +5 -0
  19. package/dist/types/modes/controllers/extension-ui-controller.d.ts +1 -1
  20. package/dist/types/modes/rpc/rpc-mode.d.ts +2 -0
  21. package/dist/types/sdk.d.ts +3 -1
  22. package/dist/types/session/agent-session-types.d.ts +8 -6
  23. package/dist/types/session/agent-session.d.ts +28 -1
  24. package/dist/types/session/model-controls.d.ts +4 -1
  25. package/dist/types/session/session-advisors.d.ts +7 -1
  26. package/dist/types/session/session-tools.d.ts +44 -6
  27. package/dist/types/session/streaming-output.d.ts +7 -2
  28. package/dist/types/session/tool-choice-queue.d.ts +6 -4
  29. package/dist/types/session/turn-recovery.d.ts +5 -3
  30. package/dist/types/task/index.d.ts +1 -1
  31. package/dist/types/task/types.d.ts +3 -11
  32. package/dist/types/thinking.d.ts +21 -2
  33. package/dist/types/tools/index.d.ts +8 -3
  34. package/dist/types/tools/output-meta.d.ts +5 -0
  35. package/dist/types/tools/read.d.ts +9 -1
  36. package/dist/types/tools/xdev.d.ts +53 -67
  37. package/dist/types/utils/cpuprofile.d.ts +51 -0
  38. package/dist/types/utils/inspect-image-mode.d.ts +29 -0
  39. package/dist/types/utils/profile-tree.d.ts +47 -0
  40. package/dist/types/utils/sample-profile.d.ts +67 -0
  41. package/dist/types/utils/title-generator.d.ts +17 -16
  42. package/package.json +12 -12
  43. package/src/capability/index.ts +43 -12
  44. package/src/capability/mcp.ts +21 -0
  45. package/src/capability/types.ts +20 -1
  46. package/src/cli/read-cli.ts +44 -2
  47. package/src/config/settings-schema.ts +42 -2
  48. package/src/config/settings.ts +35 -0
  49. package/src/cursor.ts +4 -3
  50. package/src/discovery/builtin-rules/index.ts +2 -0
  51. package/src/discovery/builtin-rules/ts-no-local-is-record.md +48 -0
  52. package/src/eval/py/runner.py +16 -2
  53. package/src/extensibility/extensions/runner.ts +117 -5
  54. package/src/extensibility/extensions/types.ts +0 -1
  55. package/src/extensibility/extensions/wrapper.ts +74 -42
  56. package/src/extensibility/hooks/tool-wrapper.ts +11 -4
  57. package/src/extensibility/hooks/types.ts +0 -1
  58. package/src/extensibility/shared-events.ts +18 -1
  59. package/src/internal-urls/mcp-protocol.ts +17 -3
  60. package/src/internal-urls/parse.ts +31 -0
  61. package/src/internal-urls/router.ts +24 -4
  62. package/src/internal-urls/types.ts +6 -0
  63. package/src/live/transport.ts +2 -2
  64. package/src/lsp/client.ts +2 -2
  65. package/src/lsp/config.ts +4 -0
  66. package/src/lsp/types.ts +2 -0
  67. package/src/mcp/config.ts +26 -14
  68. package/src/mcp/manager.ts +26 -9
  69. package/src/mcp/tool-bridge.ts +52 -1
  70. package/src/memories/index.ts +25 -6
  71. package/src/modes/components/custom-editor.ts +39 -16
  72. package/src/modes/components/status-line/segments.ts +3 -1
  73. package/src/modes/components/tips.txt +2 -1
  74. package/src/modes/components/tool-execution.ts +8 -7
  75. package/src/modes/controllers/command-controller.ts +10 -10
  76. package/src/modes/controllers/extension-ui-controller.ts +7 -7
  77. package/src/modes/controllers/selector-controller.ts +7 -2
  78. package/src/modes/rpc/rpc-mode.ts +60 -47
  79. package/src/prompts/steering/user-interjection.md +2 -5
  80. package/src/prompts/tools/task.md +4 -2
  81. package/src/sdk.ts +64 -58
  82. package/src/session/agent-session-types.ts +8 -5
  83. package/src/session/agent-session.ts +144 -11
  84. package/src/session/model-controls.ts +48 -12
  85. package/src/session/session-advisors.ts +30 -14
  86. package/src/session/session-listing.ts +66 -4
  87. package/src/session/session-tools.ts +162 -54
  88. package/src/session/streaming-output.ts +18 -6
  89. package/src/session/tool-choice-queue.ts +19 -4
  90. package/src/session/turn-recovery.ts +25 -6
  91. package/src/slash-commands/builtin-registry.ts +69 -0
  92. package/src/task/executor.ts +11 -3
  93. package/src/task/index.ts +43 -32
  94. package/src/task/types.ts +12 -17
  95. package/src/thinking.ts +68 -5
  96. package/src/tools/bash.ts +16 -9
  97. package/src/tools/index.ts +36 -16
  98. package/src/tools/output-meta.ts +20 -0
  99. package/src/tools/read.ts +89 -15
  100. package/src/tools/write.ts +16 -7
  101. package/src/tools/xdev.ts +198 -210
  102. package/src/utils/cpuprofile.ts +235 -0
  103. package/src/utils/inspect-image-mode.ts +39 -0
  104. package/src/utils/profile-tree.ts +111 -0
  105. package/src/utils/sample-profile.ts +437 -0
  106. package/src/utils/title-generator.ts +88 -34
  107. package/dist/types/advisor/__tests__/advisor.test.d.ts +0 -1
  108. package/dist/types/advisor/__tests__/config.test.d.ts +0 -1
  109. package/dist/types/advisor/__tests__/emission-guard.test.d.ts +0 -1
  110. package/dist/types/cli/__tests__/auth-gateway-catalog.test.d.ts +0 -1
  111. package/dist/types/cli/update-cli.test.d.ts +0 -1
  112. package/dist/types/config/__tests__/model-registry.test.d.ts +0 -1
  113. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +0 -1
  114. package/dist/types/eval/__tests__/bridge-timeout.test.d.ts +0 -1
  115. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +0 -1
  116. package/dist/types/eval/__tests__/completion-bridge.test.d.ts +0 -1
  117. package/dist/types/eval/__tests__/helpers-local-roots.test.d.ts +0 -1
  118. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +0 -1
  119. package/dist/types/eval/__tests__/js-context-manager.test.d.ts +0 -1
  120. package/dist/types/eval/__tests__/julia-prelude.test.d.ts +0 -1
  121. package/dist/types/eval/__tests__/kernel-spawn.test.d.ts +0 -1
  122. package/dist/types/eval/__tests__/prelude-agent.test.d.ts +0 -1
  123. package/dist/types/eval/__tests__/process-entry-import.test.d.ts +0 -1
  124. package/dist/types/eval/py/__tests__/prelude.test.d.ts +0 -1
  125. package/dist/types/eval/py/__tests__/runner-shell-output.test.d.ts +0 -1
  126. package/dist/types/hindsight/client.test.d.ts +0 -1
  127. package/dist/types/internal-urls/__tests__/agent-protocol-nested.test.d.ts +0 -1
  128. package/dist/types/internal-urls/__tests__/ssh-protocol.test.d.ts +0 -1
  129. package/dist/types/launch/broker-list-order.test.d.ts +0 -1
  130. package/dist/types/launch/broker-output-snapshot.test.d.ts +0 -1
  131. package/dist/types/launch/protocol.test.d.ts +0 -1
  132. package/dist/types/launch/spawn-options.test.d.ts +0 -1
  133. package/dist/types/launch/terminal-output.test.d.ts +0 -1
  134. package/dist/types/live/protocol.test.d.ts +0 -1
  135. package/dist/types/mcp/config-writer.test.d.ts +0 -1
  136. package/dist/types/mcp/smithery-auth.test.d.ts +0 -1
  137. package/dist/types/mcp/smithery-registry.test.d.ts +0 -1
  138. package/dist/types/mcp/transports/stdio.test.d.ts +0 -1
  139. package/dist/types/modes/components/__tests__/dynamic-border.test.d.ts +0 -1
  140. package/dist/types/modes/components/__tests__/move-overlay.test.d.ts +0 -1
  141. package/dist/types/modes/components/__tests__/pause-screen.test.d.ts +0 -1
  142. package/dist/types/modes/components/__tests__/skill-message.test.d.ts +0 -1
  143. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +0 -1
  144. package/dist/types/modes/components/custom-editor.test.d.ts +0 -1
  145. package/dist/types/modes/components/login-dialog.test.d.ts +0 -1
  146. package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +0 -1
  147. package/dist/types/modes/components/status-line/component.test.d.ts +0 -1
  148. package/dist/types/modes/components/tool-execution.test.d.ts +0 -1
  149. package/dist/types/modes/controllers/extension-ui-controller.test.d.ts +0 -1
  150. package/dist/types/modes/noninteractive-dispose.test.d.ts +0 -1
  151. package/dist/types/modes/print-mode.test.d.ts +0 -1
  152. package/dist/types/modes/session-teardown.test.d.ts +0 -1
  153. package/dist/types/modes/theme/mermaid-rendering.test.d.ts +0 -1
  154. package/dist/types/modes/utils/transcript-render-helpers.test.d.ts +0 -1
  155. package/dist/types/modes/warp-events.test.d.ts +0 -1
  156. package/dist/types/plan-mode/approved-plan-prompt.test.d.ts +0 -1
  157. package/dist/types/plan-mode/model-transition.test.d.ts +0 -1
  158. package/dist/types/plan-mode/reentry-prompt.test.d.ts +0 -1
  159. package/dist/types/session/agent-session-error-log.test.d.ts +0 -1
  160. package/dist/types/session/blob-store.test.d.ts +0 -1
  161. package/dist/types/session/messages.test.d.ts +0 -1
  162. package/dist/types/session/session-context.test.d.ts +0 -1
  163. package/dist/types/ssh/__tests__/connection-manager-args.test.d.ts +0 -1
  164. package/dist/types/ssh/__tests__/connection-manager-timeout.test.d.ts +0 -1
  165. package/dist/types/ssh/__tests__/file-transfer-posix-guard.test.d.ts +0 -1
  166. package/dist/types/ssh/__tests__/sshfs-mount.test.d.ts +0 -1
  167. package/dist/types/system-prompt.test.d.ts +0 -1
  168. package/dist/types/task/render.test.d.ts +0 -1
  169. package/dist/types/task/spawn-policy.test.d.ts +0 -1
  170. package/dist/types/tools/__tests__/eval-description.test.d.ts +0 -1
  171. package/dist/types/tools/__tests__/glob.test.d.ts +0 -1
  172. package/dist/types/tools/__tests__/json-tree.test.d.ts +0 -1
  173. package/dist/types/tools/__tests__/vibe-render.test.d.ts +0 -1
  174. package/dist/types/tools/hub/launch-compat.test.d.ts +0 -1
  175. package/dist/types/vibe/__tests__/token-rate.test.d.ts +0 -1
  176. package/src/advisor/__tests__/advisor.test.ts +0 -4889
  177. package/src/advisor/__tests__/config.test.ts +0 -349
  178. package/src/advisor/__tests__/emission-guard.test.ts +0 -147
  179. package/src/cli/__tests__/auth-gateway-catalog.test.ts +0 -111
  180. package/src/cli/update-cli.test.ts +0 -28
  181. package/src/config/__tests__/model-registry.test.ts +0 -182
  182. package/src/eval/__tests__/agent-bridge.test.ts +0 -1509
  183. package/src/eval/__tests__/bridge-timeout.test.ts +0 -170
  184. package/src/eval/__tests__/budget-bridge.test.ts +0 -80
  185. package/src/eval/__tests__/completion-bridge.test.ts +0 -412
  186. package/src/eval/__tests__/helpers-local-roots.test.ts +0 -55
  187. package/src/eval/__tests__/idle-timeout.test.ts +0 -80
  188. package/src/eval/__tests__/js-context-manager.test.ts +0 -456
  189. package/src/eval/__tests__/julia-prelude.test.ts +0 -66
  190. package/src/eval/__tests__/kernel-spawn.test.ts +0 -115
  191. package/src/eval/__tests__/prelude-agent.test.ts +0 -156
  192. package/src/eval/__tests__/process-entry-import.test.ts +0 -137
  193. package/src/eval/py/__tests__/prelude.test.ts +0 -104
  194. package/src/eval/py/__tests__/runner-shell-output.test.ts +0 -157
  195. package/src/hindsight/client.test.ts +0 -75
  196. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +0 -141
  197. package/src/internal-urls/__tests__/ssh-protocol.test.ts +0 -331
  198. package/src/launch/broker-list-order.test.ts +0 -89
  199. package/src/launch/broker-output-snapshot.test.ts +0 -126
  200. package/src/launch/protocol.test.ts +0 -59
  201. package/src/launch/spawn-options.test.ts +0 -31
  202. package/src/launch/terminal-output.test.ts +0 -107
  203. package/src/live/protocol.test.ts +0 -140
  204. package/src/mcp/config-writer.test.ts +0 -43
  205. package/src/mcp/smithery-auth.test.ts +0 -29
  206. package/src/mcp/smithery-registry.test.ts +0 -51
  207. package/src/mcp/transports/stdio.test.ts +0 -427
  208. package/src/modes/components/__tests__/dynamic-border.test.ts +0 -55
  209. package/src/modes/components/__tests__/move-overlay.test.ts +0 -252
  210. package/src/modes/components/__tests__/pause-screen.test.ts +0 -143
  211. package/src/modes/components/__tests__/skill-message.test.ts +0 -94
  212. package/src/modes/components/custom-editor-plugin-ctor.test.ts +0 -36
  213. package/src/modes/components/custom-editor.test.ts +0 -510
  214. package/src/modes/components/login-dialog.test.ts +0 -56
  215. package/src/modes/components/status-line/component.jj-cache.test.ts +0 -229
  216. package/src/modes/components/status-line/component.test.ts +0 -84
  217. package/src/modes/components/tool-execution.test.ts +0 -162
  218. package/src/modes/controllers/extension-ui-controller.test.ts +0 -250
  219. package/src/modes/noninteractive-dispose.test.ts +0 -73
  220. package/src/modes/print-mode.test.ts +0 -71
  221. package/src/modes/session-teardown.test.ts +0 -219
  222. package/src/modes/theme/mermaid-rendering.test.ts +0 -53
  223. package/src/modes/utils/transcript-render-helpers.test.ts +0 -38
  224. package/src/modes/warp-events.test.ts +0 -794
  225. package/src/plan-mode/approved-plan-prompt.test.ts +0 -36
  226. package/src/plan-mode/model-transition.test.ts +0 -60
  227. package/src/plan-mode/reentry-prompt.test.ts +0 -41
  228. package/src/session/agent-session-error-log.test.ts +0 -59
  229. package/src/session/blob-store.test.ts +0 -56
  230. package/src/session/messages.test.ts +0 -282
  231. package/src/session/session-context.test.ts +0 -384
  232. package/src/ssh/__tests__/connection-manager-args.test.ts +0 -191
  233. package/src/ssh/__tests__/connection-manager-timeout.test.ts +0 -61
  234. package/src/ssh/__tests__/file-transfer-posix-guard.test.ts +0 -105
  235. package/src/ssh/__tests__/sshfs-mount.test.ts +0 -13
  236. package/src/system-prompt.test.ts +0 -236
  237. package/src/task/render.test.ts +0 -290
  238. package/src/task/spawn-policy.test.ts +0 -62
  239. package/src/tools/__tests__/eval-description.test.ts +0 -18
  240. package/src/tools/__tests__/glob.test.ts +0 -37
  241. package/src/tools/__tests__/json-tree.test.ts +0 -35
  242. package/src/tools/__tests__/vibe-render.test.ts +0 -210
  243. package/src/tools/hub/launch-compat.test.ts +0 -40
  244. package/src/vibe/__tests__/token-rate.test.ts +0 -96
package/src/tools/xdev.ts CHANGED
@@ -9,22 +9,27 @@
9
9
  * read xd://<tool> → tool docs + JSON parameter schema
10
10
  * write xd://<tool> → execute: `content` is the JSON args object
11
11
  *
12
+ * Direct and device dispatch share one canonical tool map. The mounted-name
13
+ * set controls presentation only; dispatch accepts the enabled union of
14
+ * top-level active and mounted names. Listing and prompt docs stay
15
+ * mounted-only because top-level tools already ship their schemas.
16
+ *
12
17
  * Args go through the same machinery as native tool calls: validated with
13
18
  * pi-ai's `validateToolArguments` (the schema is returned on mismatch, so a
14
19
  * malformed call self-corrects without a round trip) and streamed through
15
20
  * the write tool's existing incremental `content` decoding for live render
16
21
  * previews. Compared to a dispatcher def this still costs zero *schema
17
22
  * duplication* — one wire schema per tool instead of one per dispatcher
18
- * branch — but full docs + schema for every mounted device are inlined into
19
- * the system prompt (`XdevRegistry.docsAll()`) so no discovery `read` is
20
- * needed before first use; `read xd://<tool>` remains for on-demand re-fetch.
23
+ * branch — but full docs + schema for every mounted device can be inlined
24
+ * into the system prompt, so no discovery read is needed before first use;
25
+ * `read xd://<tool>` remains for on-demand re-fetch.
21
26
  *
22
27
  * Rendering: the write renderer draws NOTHING until the streamed `path` is
23
28
  * known and provably does not target `xd://`; device writes then delegate to
24
29
  * the wrapped tool's own renderer with the decoded inner args.
25
30
  */
26
31
  import type { AgentToolContext, AgentToolResult, AgentToolUpdateCallback, ToolLoadMode } from "@oh-my-pi/pi-agent-core";
27
- import { type Tool as AiTool, toolWireSchema, validateToolArguments } from "@oh-my-pi/pi-ai";
32
+ import { type Tool as AiTool, jsonSchemaToTypeScript, toolWireSchema, validateToolArguments } from "@oh-my-pi/pi-ai";
28
33
  import { type Component, Container, Text } from "@oh-my-pi/pi-tui";
29
34
  import { parseStreamingJson } from "@oh-my-pi/pi-utils";
30
35
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
@@ -69,8 +74,7 @@ export type XdevDocsMode = "inline" | "builtins" | "catalog";
69
74
  * while the `xd://` transport is active. Discoverable tools mount unless they
70
75
  * are pinned top-level by {@link XDEV_KEEP_TOP_LEVEL} or carry the transport
71
76
  * itself ({@link XDEV_TRANSPORT_TOOLS}); essential tools never do. The caller
72
- * gates this on the transport being active (a session-owned
73
- * {@link XdevRegistry} existing).
77
+ * gates this on the transport being active.
74
78
  */
75
79
  export function isMountableUnderXdev(tool: { name: string; loadMode?: ToolLoadMode }): boolean {
76
80
  if (tool.name in XDEV_TRANSPORT_TOOLS || tool.name in XDEV_KEEP_TOP_LEVEL) return false;
@@ -107,7 +111,7 @@ function schemaDeclaresIntentField(schema: unknown): boolean {
107
111
  }
108
112
 
109
113
  function renderDocs(inst: Tool, heading = "#", descriptionCap?: number): string {
110
- const schema = JSON.stringify(toolWireSchema(inst as AiTool), null, 1);
114
+ const schema = jsonSchemaToTypeScript(toolWireSchema(inst as AiTool));
111
115
  let description = inst.description ?? "";
112
116
  if (descriptionCap !== undefined && description.length > descriptionCap) {
113
117
  description = `${description.slice(0, descriptionCap).trimEnd()}… (full docs: read ${XD_URL_PREFIX}${inst.name})`;
@@ -118,8 +122,8 @@ function renderDocs(inst: Tool, heading = "#", descriptionCap?: number): string
118
122
  description,
119
123
  "",
120
124
  `${heading}# Schema`,
121
- "```json",
122
- schema,
125
+ "```ts",
126
+ `type Args = ${schema};`,
123
127
  "```",
124
128
  `Execute by writing JSON to ${XD_URL_PREFIX}${inst.name}.`,
125
129
  ].join("\n");
@@ -208,227 +212,211 @@ function decodeInnerArgs(raw: unknown): Record<string, unknown> {
208
212
  /** Device-write content that requests docs instead of executing: empty, `?`, or `help`. */
209
213
  const HELP_CONTENT_RE = /^\s*(\?|help)?\s*$/i;
210
214
 
211
- /**
212
- * Registry of tools mounted under `xd://` for one session. `createTools`
213
- * mounts discoverable built-ins first; SDK assembly adds custom tools that do
214
- * not opt out. `read`/`write` consult it at execute time.
215
- */
216
- export class XdevRegistry {
217
- /** Discoverable built-ins mounted at construction; never reconciled away. */
218
- #builtins = new Map<string, Tool>();
219
- /**
220
- * Dynamic mounts (custom, MCP, extension, autoresearch) replaced wholesale
221
- * by {@link reconcile} as the active tool set changes, so a deactivated or
222
- * disconnected tool is no longer callable through a stale device.
223
- */
224
- #dynamic = new Map<string, Tool>();
225
-
226
- constructor(builtins: Iterable<Tool>) {
227
- for (const tool of builtins) this.#builtins.set(tool.name, tool);
228
- }
229
-
230
- /**
231
- * Replace the dynamic mount set while preserving the built-in devices. Order
232
- * follows `tools`; names absent from it are dropped. A built-in device is
233
- * never shadowed by a same-named dynamic entry.
234
- */
235
- reconcile(tools: Iterable<Tool>): void {
236
- const next = new Map<string, Tool>();
237
- for (const tool of tools) {
238
- if (this.#builtins.has(tool.name)) continue;
239
- next.set(tool.name, tool);
240
- }
241
- this.#dynamic = next;
242
- }
215
+ /** Shared tool state consumed by the `xd://` presentation layer. */
216
+ export interface XdevState {
217
+ /** Canonical session tool map; direct and device dispatch read the same instances. */
218
+ readonly tools: Map<string, Tool>;
219
+ /** Ordered names currently presented as mounted devices. */
220
+ readonly mountedNames: Set<string>;
221
+ /** Names originating from built-in factories, used only for prompt presentation. */
222
+ readonly builtInNames: Set<string>;
223
+ /** Whether a name is active at the top level. */
224
+ readonly isActive: (name: string) => boolean;
225
+ /** Optional execution-only decorator, such as the ACP permission gate. */
226
+ decorateExecution?(tool: Tool): Tool;
227
+ }
243
228
 
244
- get size(): number {
245
- return this.#builtins.size + this.#dynamic.size;
246
- }
229
+ /** Full-doc character budget for system-prompt mounted-device sections. */
230
+ export const XDEV_DOCS_TOTAL_BUDGET = 48_000;
231
+ /** Per-device cap preventing one pathological description from starving later devices. */
232
+ export const XDEV_DOCS_PER_DEVICE_CAP = 10_000;
233
+ /** Description cap for external mounted tools; their full docs remain readable on demand. */
234
+ export const XDEV_EXTERNAL_DESCRIPTION_CAP = 200;
235
+
236
+ /** Resolve any enabled tool through the canonical session map. */
237
+ export function resolveXdevTool(state: XdevState, name: string): Tool | undefined {
238
+ if (!state.mountedNames.has(name) && !state.isActive(name)) return undefined;
239
+ return state.tools.get(name);
240
+ }
247
241
 
248
- /** Mounted tools in catalog order: built-ins first, then dynamic mounts. */
249
- list(): readonly Tool[] {
250
- return [...this.#builtins.values(), ...this.#dynamic.values()];
251
- }
242
+ /** Resolve a mounted tool for top-level fallback execution. */
243
+ export function resolveMountedXdevTool(state: XdevState, name: string): Tool | undefined {
244
+ return state.mountedNames.has(name) ? state.tools.get(name) : undefined;
245
+ }
252
246
 
253
- get(name: string): Tool | undefined {
254
- return this.#builtins.get(name) ?? this.#dynamic.get(name);
255
- }
247
+ /** Resolve a mounted tool with its execution-only permission decorator. */
248
+ export function resolveMountedXdevExecutable(state: XdevState, name: string): Tool | undefined {
249
+ const tool = resolveMountedXdevTool(state, name);
250
+ return tool && state.decorateExecution ? state.decorateExecution(tool) : tool;
251
+ }
252
+ /** Mounted tools in presentation order, resolved from the canonical map. */
253
+ export function listXdevTools(state: XdevState): Tool[] {
254
+ return [...state.mountedNames].flatMap(name => {
255
+ const tool = state.tools.get(name);
256
+ return tool ? [tool] : [];
257
+ });
258
+ }
256
259
 
257
- /** `{name, summary}` pairs for prompt templates and /tools display. */
258
- entries(): Array<{ name: string; summary: string }> {
259
- return this.list().map(tool => ({
260
- name: tool.name,
261
- summary: promptCatalogSummary(
262
- tool,
263
- this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined,
264
- ),
265
- }));
266
- }
260
+ /** `{name, summary}` pairs for prompt templates and `/tools` display. */
261
+ export function xdevEntries(state: XdevState): Array<{ name: string; summary: string }> {
262
+ return listXdevTools(state).map(tool => ({
263
+ name: tool.name,
264
+ summary: promptCatalogSummary(
265
+ tool,
266
+ state.builtInNames.has(tool.name) ? undefined : XDEV_EXTERNAL_DESCRIPTION_CAP,
267
+ ),
268
+ }));
269
+ }
267
270
 
268
- /** `read xd://` listing with one device per line. */
269
- listing(): string {
270
- const rows = this.entries().map(({ name, summary }) => `${XD_URL_PREFIX}${name.padEnd(14)} ${summary}`);
271
- return [
272
- `${XD_URL_PREFIX} ${this.size} mounted tool devices.`,
273
- ...rows,
274
- "",
275
- `Read ${XD_URL_PREFIX}<tool> for docs + JSON schema; write the JSON args object to ${XD_URL_PREFIX}<tool> to execute.`,
276
- ].join("\n");
277
- }
271
+ /** `read xd://` listing with one device per line. */
272
+ export function xdevListing(state: XdevState): string {
273
+ const rows = xdevEntries(state).map(({ name, summary }) => `${XD_URL_PREFIX}${name.padEnd(14)} ${summary}`);
274
+ return [
275
+ `${XD_URL_PREFIX} ${state.mountedNames.size} mounted tool devices.`,
276
+ ...rows,
277
+ "",
278
+ `Read ${XD_URL_PREFIX}<tool> for docs + JSON schema; write the JSON args object to ${XD_URL_PREFIX}<tool> to execute. Active top-level tools accept the same dispatch.`,
279
+ ].join("\n");
280
+ }
278
281
 
279
- /** Docs + schema for one device; throws with the listing when unknown. */
280
- docs(name: string): string {
281
- return renderDocs(this.#resolve(name));
282
- }
282
+ /** Docs + schema for any enabled tool. */
283
+ export function xdevDocs(state: XdevState, name: string): string {
284
+ return renderDocs(resolveRequiredXdevTool(state, name));
285
+ }
283
286
 
284
- /**
285
- * Char budget for the full docs inlined into the system prompt. Large MCP
286
- * catalogs previously shipped every schema top-level; without a cap they
287
- * would bloat every request. Devices past the budget fall back to a
288
- * one-line summary their docs stay one `read xd://<tool>` away.
289
- */
290
- static readonly DOCS_TOTAL_BUDGET = 48_000;
291
- /** A single device's docs above this size never inline: one pathological
292
- * MCP description must not starve every later device. */
293
- static readonly DOCS_PER_DEVICE_CAP = 10_000;
294
- /** Description cap for EXTERNAL devices (dynamic mounts: MCP, custom,
295
- * extension, …) in the system-prompt embedding. Built-in devices inline
296
- * their full curated docs; external descriptions are server-controlled
297
- * prose the model can re-fetch, so only the lede earns prompt space. */
298
- static readonly EXTERNAL_DESCRIPTION_CAP = 200;
299
-
300
- /**
301
- * Docs + schema for mounted devices, nested under `##` headings for
302
- * system-prompt embedding. Inlines full docs in catalog order (built-ins
303
- * first) until {@link DOCS_TOTAL_BUDGET} is spent; the rest are listed by
304
- * name + summary with a pointer to on-demand `read xd://<tool>` docs.
305
- * Dynamic mounts embed at most {@link EXTERNAL_DESCRIPTION_CAP} description
306
- * chars (schema always intact); `read xd://<tool>` returns the full text.
307
- */
308
- docsAll(mode: XdevDocsMode = "inline", inlinePatterns: readonly string[] = []): string {
309
- const sections: string[] = [];
310
- const overflow: Tool[] = [];
311
- const inlineGlobs = compileInlineGlobs(inlinePatterns);
312
- let used = 0;
313
- for (const tool of this.list()) {
314
- if (!this.#shouldInline(tool, mode, inlineGlobs)) {
315
- overflow.push(tool);
316
- continue;
317
- }
318
- const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
319
- const docs = renderDocs(tool, "##", descriptionCap);
320
- if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET) {
321
- overflow.push(tool);
322
- continue;
323
- }
324
- used += docs.length;
325
- sections.push(docs);
287
+ /** Docs + schema for mounted devices under the configured prompt-doc policy. */
288
+ export function xdevDocsAll(
289
+ state: XdevState,
290
+ mode: XdevDocsMode = "inline",
291
+ inlinePatterns: readonly string[] = [],
292
+ ): string {
293
+ const sections: string[] = [];
294
+ const overflow: Tool[] = [];
295
+ const inlineGlobs = compileInlineGlobs(inlinePatterns);
296
+ let used = 0;
297
+ for (const tool of listXdevTools(state)) {
298
+ if (!shouldInlineXdevTool(state, tool, mode, inlineGlobs)) {
299
+ overflow.push(tool);
300
+ continue;
326
301
  }
327
- if (overflow.length > 0) {
328
- sections.push(
329
- [
330
- "## Additional devices (docs on demand)",
331
- ...overflow.map(tool => {
332
- const maxLength = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
333
- return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool, maxLength)}`;
334
- }),
335
- "",
336
- `Read ${XD_URL_PREFIX}<tool> for full docs + JSON schema before first use.`,
337
- ].join("\n"),
338
- );
302
+ const descriptionCap = state.builtInNames.has(tool.name) ? undefined : XDEV_EXTERNAL_DESCRIPTION_CAP;
303
+ const docs = renderDocs(tool, "##", descriptionCap);
304
+ if (docs.length > XDEV_DOCS_PER_DEVICE_CAP || used + docs.length > XDEV_DOCS_TOTAL_BUDGET) {
305
+ overflow.push(tool);
306
+ continue;
339
307
  }
340
- return sections.join("\n\n");
308
+ used += docs.length;
309
+ sections.push(docs);
310
+ }
311
+ if (overflow.length > 0) {
312
+ sections.push(
313
+ [
314
+ "## Additional devices (docs on demand)",
315
+ ...overflow.map(tool => {
316
+ const maxLength = state.builtInNames.has(tool.name) ? undefined : XDEV_EXTERNAL_DESCRIPTION_CAP;
317
+ return `- ${XD_URL_PREFIX}${tool.name} — ${promptCatalogSummary(tool, maxLength)}`;
318
+ }),
319
+ "",
320
+ `Read ${XD_URL_PREFIX}<tool> for full docs + JSON schema before first use.`,
321
+ ].join("\n"),
322
+ );
341
323
  }
324
+ return sections.join("\n\n");
325
+ }
342
326
 
343
- /** Docs for selected mounted devices under the configured prompt-doc policy. */
344
- docsFor(names: Iterable<string>, mode: XdevDocsMode, inlinePatterns: readonly string[] = []): string {
345
- const sections: string[] = [];
346
- const inlineGlobs = compileInlineGlobs(inlinePatterns);
347
- let used = 0;
348
- for (const name of names) {
349
- const tool = this.get(name);
350
- if (!tool || !this.#shouldInline(tool, mode, inlineGlobs)) continue;
351
- const descriptionCap = this.#dynamic.has(tool.name) ? XdevRegistry.EXTERNAL_DESCRIPTION_CAP : undefined;
352
- const docs = renderDocs(tool, "##", descriptionCap);
353
- if (docs.length > XdevRegistry.DOCS_PER_DEVICE_CAP || used + docs.length > XdevRegistry.DOCS_TOTAL_BUDGET)
354
- continue;
355
- used += docs.length;
356
- sections.push(docs);
357
- }
358
- return sections.join("\n\n");
327
+ /** Docs for selected mounted devices under the configured prompt-doc policy. */
328
+ export function xdevDocsFor(
329
+ state: XdevState,
330
+ names: Iterable<string>,
331
+ mode: XdevDocsMode,
332
+ inlinePatterns: readonly string[] = [],
333
+ ): string {
334
+ const sections: string[] = [];
335
+ const inlineGlobs = compileInlineGlobs(inlinePatterns);
336
+ let used = 0;
337
+ for (const name of names) {
338
+ const tool = resolveMountedXdevTool(state, name);
339
+ if (!tool || !shouldInlineXdevTool(state, tool, mode, inlineGlobs)) continue;
340
+ const descriptionCap = state.builtInNames.has(tool.name) ? undefined : XDEV_EXTERNAL_DESCRIPTION_CAP;
341
+ const docs = renderDocs(tool, "##", descriptionCap);
342
+ if (docs.length > XDEV_DOCS_PER_DEVICE_CAP || used + docs.length > XDEV_DOCS_TOTAL_BUDGET) continue;
343
+ used += docs.length;
344
+ sections.push(docs);
359
345
  }
346
+ return sections.join("\n\n");
347
+ }
360
348
 
361
- #shouldInline(tool: Tool, mode: XdevDocsMode, inlineGlobs: readonly Bun.Glob[]): boolean {
362
- return (
363
- mode !== "catalog" &&
364
- (mode === "inline" || this.#builtins.has(tool.name) || inlineGlobs.some(glob => glob.match(tool.name)))
349
+ function shouldInlineXdevTool(
350
+ state: XdevState,
351
+ tool: Tool,
352
+ mode: XdevDocsMode,
353
+ inlineGlobs: readonly Bun.Glob[],
354
+ ): boolean {
355
+ return (
356
+ mode !== "catalog" &&
357
+ (mode === "inline" || state.builtInNames.has(tool.name) || inlineGlobs.some(glob => glob.match(tool.name)))
358
+ );
359
+ }
360
+
361
+ function resolveRequiredXdevTool(state: XdevState, name: string): Tool {
362
+ const inst = resolveXdevTool(state, name);
363
+ if (!inst) {
364
+ throw new ToolError(
365
+ `No such tool: ${XD_URL_PREFIX}${name}. Mounted devices: ${[...state.mountedNames].join(", ")}. Active top-level tools are also dispatchable via ${XD_URL_PREFIX}<tool>.`,
365
366
  );
366
367
  }
368
+ return inst;
369
+ }
367
370
 
368
- #resolve(name: string): Tool {
369
- const inst = this.get(name);
370
- if (!inst) {
371
- throw new ToolError(
372
- `No such tool device: ${XD_URL_PREFIX}${name}. Mounted: ${this.list()
373
- .map(tool => tool.name)
374
- .join(", ")}.`,
375
- );
376
- }
377
- return inst;
378
- }
371
+ /** Execute an enabled canonical tool through `write xd://<tool>`. */
372
+ export async function dispatchXdevTool(
373
+ state: XdevState,
374
+ name: string,
375
+ content: string,
376
+ toolCallId: string,
377
+ signal?: AbortSignal,
378
+ onUpdate?: AgentToolUpdateCallback,
379
+ context?: AgentToolContext,
380
+ ): Promise<{ result: AgentToolResult<unknown>; xdev: XdevDispatch }> {
381
+ let xdev: XdevDispatch = { tool: name, mode: "execute" };
382
+ try {
383
+ const canonical = resolveRequiredXdevTool(state, name);
379
384
 
380
- /**
381
- * Execute a device write: `content` is the JSON args object (empty, `?`, or
382
- * `help` returns docs). Args validate against the wrapped tool's schema —
383
- * the schema comes back in the error on mismatch.
384
- */
385
- async dispatch(
386
- name: string,
387
- content: string,
388
- toolCallId: string,
389
- signal?: AbortSignal,
390
- onUpdate?: AgentToolUpdateCallback,
391
- context?: AgentToolContext,
392
- ): Promise<{ result: AgentToolResult<unknown>; xdev: XdevDispatch }> {
393
- let xdev: XdevDispatch = { tool: name, mode: "execute" };
394
- try {
395
- const inst = this.#resolve(name);
396
-
397
- if (HELP_CONTENT_RE.test(content)) {
398
- return {
399
- result: { content: [{ type: "text", text: renderDocs(inst) }] },
400
- xdev: { tool: name, mode: "help" },
401
- };
402
- }
403
-
404
- const validated = parseDeviceArgs(inst as AiTool, content, toolCallId, () => renderDocs(inst));
405
- xdev = { ...xdev, args: validated };
406
- const innerOnUpdate: AgentToolUpdateCallback | undefined = onUpdate
407
- ? partial =>
408
- onUpdate({
409
- content: partial.content,
410
- details: { xdev: { ...xdev, inner: partial.details } },
411
- isError: partial.isError,
412
- })
413
- : undefined;
414
- const result = await inst.execute(toolCallId, validated as never, signal, innerOnUpdate, context);
415
- return { result, xdev: { ...xdev, inner: result.details } };
416
- } catch (error) {
417
- if (
418
- error instanceof ToolAbortError ||
419
- signal?.aborted ||
420
- (error instanceof Error && error.name === "AbortError")
421
- ) {
422
- throw error;
423
- }
385
+ if (HELP_CONTENT_RE.test(content)) {
424
386
  return {
425
- result: {
426
- content: [{ type: "text", text: renderError(error) }],
427
- isError: true,
428
- },
429
- xdev,
387
+ result: { content: [{ type: "text", text: renderDocs(canonical) }] },
388
+ xdev: { tool: name, mode: "help" },
430
389
  };
431
390
  }
391
+
392
+ const validated = parseDeviceArgs(canonical as AiTool, content, toolCallId, () => renderDocs(canonical));
393
+ xdev = { ...xdev, args: validated };
394
+ const innerOnUpdate: AgentToolUpdateCallback | undefined = onUpdate
395
+ ? partial =>
396
+ onUpdate({
397
+ content: partial.content,
398
+ details: { xdev: { ...xdev, inner: partial.details } },
399
+ isError: partial.isError,
400
+ })
401
+ : undefined;
402
+ const executable = state.decorateExecution?.(canonical) ?? canonical;
403
+ const result = await executable.execute(toolCallId, validated as never, signal, innerOnUpdate, context);
404
+ return { result, xdev: { ...xdev, inner: result.details } };
405
+ } catch (error) {
406
+ if (
407
+ error instanceof ToolAbortError ||
408
+ signal?.aborted ||
409
+ (error instanceof Error && error.name === "AbortError")
410
+ ) {
411
+ throw error;
412
+ }
413
+ return {
414
+ result: {
415
+ content: [{ type: "text", text: renderError(error) }],
416
+ isError: true,
417
+ },
418
+ xdev,
419
+ };
432
420
  }
433
421
  }
434
422