@animalabs/agent-framework 0.26.1 → 0.27.0

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 (276) hide show
  1. package/README.md +57 -7
  2. package/dist/src/agent.d.ts +263 -22
  3. package/dist/src/agent.d.ts.map +1 -1
  4. package/dist/src/agent.js +531 -54
  5. package/dist/src/agent.js.map +1 -1
  6. package/dist/src/api/server.d.ts.map +1 -1
  7. package/dist/src/api/server.js +1 -2
  8. package/dist/src/api/server.js.map +1 -1
  9. package/dist/src/auto-continue.d.ts +33 -0
  10. package/dist/src/auto-continue.d.ts.map +1 -0
  11. package/dist/src/auto-continue.js +35 -0
  12. package/dist/src/auto-continue.js.map +1 -0
  13. package/dist/src/code-execution/isolate/scripts.d.ts +11 -0
  14. package/dist/src/code-execution/isolate/scripts.d.ts.map +1 -1
  15. package/dist/src/code-execution/isolate/scripts.js +5 -1
  16. package/dist/src/code-execution/isolate/scripts.js.map +1 -1
  17. package/dist/src/framework.d.ts +829 -156
  18. package/dist/src/framework.d.ts.map +1 -1
  19. package/dist/src/framework.js +3906 -1008
  20. package/dist/src/framework.js.map +1 -1
  21. package/dist/src/gate/event-gate.d.ts +309 -46
  22. package/dist/src/gate/event-gate.d.ts.map +1 -1
  23. package/dist/src/gate/event-gate.js +2117 -395
  24. package/dist/src/gate/event-gate.js.map +1 -1
  25. package/dist/src/gate/index.d.ts +2 -2
  26. package/dist/src/gate/index.d.ts.map +1 -1
  27. package/dist/src/gate/index.js +1 -1
  28. package/dist/src/gate/index.js.map +1 -1
  29. package/dist/src/gate/recipe-reconcile.d.ts +103 -0
  30. package/dist/src/gate/recipe-reconcile.d.ts.map +1 -0
  31. package/dist/src/gate/recipe-reconcile.js +325 -0
  32. package/dist/src/gate/recipe-reconcile.js.map +1 -0
  33. package/dist/src/gate/types.d.ts +68 -8
  34. package/dist/src/gate/types.d.ts.map +1 -1
  35. package/dist/src/index.d.ts +6 -4
  36. package/dist/src/index.d.ts.map +1 -1
  37. package/dist/src/index.js +5 -2
  38. package/dist/src/index.js.map +1 -1
  39. package/dist/src/mcpl/channel-registry.d.ts +433 -113
  40. package/dist/src/mcpl/channel-registry.d.ts.map +1 -1
  41. package/dist/src/mcpl/channel-registry.js +1152 -434
  42. package/dist/src/mcpl/channel-registry.js.map +1 -1
  43. package/dist/src/mcpl/conversation-router.d.ts +16 -8
  44. package/dist/src/mcpl/conversation-router.d.ts.map +1 -1
  45. package/dist/src/mcpl/conversation-router.js +29 -15
  46. package/dist/src/mcpl/conversation-router.js.map +1 -1
  47. package/dist/src/mcpl/convert-block.d.ts.map +1 -1
  48. package/dist/src/mcpl/convert-block.js +14 -2
  49. package/dist/src/mcpl/convert-block.js.map +1 -1
  50. package/dist/src/mcpl/hook-orchestrator.d.ts +7 -1
  51. package/dist/src/mcpl/hook-orchestrator.d.ts.map +1 -1
  52. package/dist/src/mcpl/hook-orchestrator.js +32 -7
  53. package/dist/src/mcpl/hook-orchestrator.js.map +1 -1
  54. package/dist/src/mcpl/index.d.ts +5 -1
  55. package/dist/src/mcpl/index.d.ts.map +1 -1
  56. package/dist/src/mcpl/index.js +4 -0
  57. package/dist/src/mcpl/index.js.map +1 -1
  58. package/dist/src/mcpl/inference-router.d.ts +14 -2
  59. package/dist/src/mcpl/inference-router.d.ts.map +1 -1
  60. package/dist/src/mcpl/inference-router.js +73 -28
  61. package/dist/src/mcpl/inference-router.js.map +1 -1
  62. package/dist/src/mcpl/prose-grammar.d.ts +4 -0
  63. package/dist/src/mcpl/prose-grammar.d.ts.map +1 -1
  64. package/dist/src/mcpl/prose-grammar.js +6 -0
  65. package/dist/src/mcpl/prose-grammar.js.map +1 -1
  66. package/dist/src/mcpl/protocol-family.d.ts.map +1 -1
  67. package/dist/src/mcpl/protocol-family.js +3 -0
  68. package/dist/src/mcpl/protocol-family.js.map +1 -1
  69. package/dist/src/mcpl/push-coalescer.d.ts +20 -2
  70. package/dist/src/mcpl/push-coalescer.d.ts.map +1 -1
  71. package/dist/src/mcpl/push-coalescer.js +33 -11
  72. package/dist/src/mcpl/push-coalescer.js.map +1 -1
  73. package/dist/src/mcpl/reference-fetcher.d.ts +63 -42
  74. package/dist/src/mcpl/reference-fetcher.d.ts.map +1 -1
  75. package/dist/src/mcpl/reference-fetcher.js +177 -145
  76. package/dist/src/mcpl/reference-fetcher.js.map +1 -1
  77. package/dist/src/mcpl/references.d.ts +380 -46
  78. package/dist/src/mcpl/references.d.ts.map +1 -1
  79. package/dist/src/mcpl/references.js +737 -78
  80. package/dist/src/mcpl/references.js.map +1 -1
  81. package/dist/src/mcpl/server-connection.d.ts +82 -3
  82. package/dist/src/mcpl/server-connection.d.ts.map +1 -1
  83. package/dist/src/mcpl/server-connection.js +132 -7
  84. package/dist/src/mcpl/server-connection.js.map +1 -1
  85. package/dist/src/mcpl/tool-lifecycle.d.ts +11 -0
  86. package/dist/src/mcpl/tool-lifecycle.d.ts.map +1 -1
  87. package/dist/src/mcpl/tool-lifecycle.js +21 -0
  88. package/dist/src/mcpl/tool-lifecycle.js.map +1 -1
  89. package/dist/src/mcpl/tool-presence.d.ts +259 -0
  90. package/dist/src/mcpl/tool-presence.d.ts.map +1 -0
  91. package/dist/src/mcpl/tool-presence.js +628 -0
  92. package/dist/src/mcpl/tool-presence.js.map +1 -0
  93. package/dist/src/mcpl/tool-result-normalize.d.ts +9 -3
  94. package/dist/src/mcpl/tool-result-normalize.d.ts.map +1 -1
  95. package/dist/src/mcpl/tool-result-normalize.js +14 -6
  96. package/dist/src/mcpl/tool-result-normalize.js.map +1 -1
  97. package/dist/src/mcpl/tool-signature.d.ts +39 -0
  98. package/dist/src/mcpl/tool-signature.d.ts.map +1 -0
  99. package/dist/src/mcpl/tool-signature.js +292 -0
  100. package/dist/src/mcpl/tool-signature.js.map +1 -0
  101. package/dist/src/mcpl/toolset-host.d.ts +200 -0
  102. package/dist/src/mcpl/toolset-host.d.ts.map +1 -0
  103. package/dist/src/mcpl/toolset-host.js +621 -0
  104. package/dist/src/mcpl/toolset-host.js.map +1 -0
  105. package/dist/src/mcpl/toolsets-module.d.ts +80 -0
  106. package/dist/src/mcpl/toolsets-module.d.ts.map +1 -0
  107. package/dist/src/mcpl/toolsets-module.js +431 -0
  108. package/dist/src/mcpl/toolsets-module.js.map +1 -0
  109. package/dist/src/mcpl/transport.d.ts +2 -2
  110. package/dist/src/mcpl/transport.d.ts.map +1 -1
  111. package/dist/src/mcpl/transport.js +3 -2
  112. package/dist/src/mcpl/transport.js.map +1 -1
  113. package/dist/src/mcpl/types.d.ts +21 -1
  114. package/dist/src/mcpl/types.d.ts.map +1 -1
  115. package/dist/src/mcpl/types.js.map +1 -1
  116. package/dist/src/mcpl/visible-content.d.ts +7 -9
  117. package/dist/src/mcpl/visible-content.d.ts.map +1 -1
  118. package/dist/src/mcpl/visible-content.js +21 -9
  119. package/dist/src/mcpl/visible-content.js.map +1 -1
  120. package/dist/src/module-registry.d.ts +23 -8
  121. package/dist/src/module-registry.d.ts.map +1 -1
  122. package/dist/src/module-registry.js +68 -15
  123. package/dist/src/module-registry.js.map +1 -1
  124. package/dist/src/modules/discord/index.js +1 -1
  125. package/dist/src/modules/discord/index.js.map +1 -1
  126. package/dist/src/modules/history/index.d.ts +2 -1
  127. package/dist/src/modules/history/index.d.ts.map +1 -1
  128. package/dist/src/modules/history/index.js +51 -17
  129. package/dist/src/modules/history/index.js.map +1 -1
  130. package/dist/src/modules/history/semantic.d.ts +108 -6
  131. package/dist/src/modules/history/semantic.d.ts.map +1 -1
  132. package/dist/src/modules/history/semantic.js +273 -57
  133. package/dist/src/modules/history/semantic.js.map +1 -1
  134. package/dist/src/modules/workspace/direct-write.d.ts +181 -0
  135. package/dist/src/modules/workspace/direct-write.d.ts.map +1 -0
  136. package/dist/src/modules/workspace/direct-write.js +384 -0
  137. package/dist/src/modules/workspace/direct-write.js.map +1 -0
  138. package/dist/src/modules/workspace/edit-text.d.ts +26 -12
  139. package/dist/src/modules/workspace/edit-text.d.ts.map +1 -1
  140. package/dist/src/modules/workspace/edit-text.js +58 -28
  141. package/dist/src/modules/workspace/edit-text.js.map +1 -1
  142. package/dist/src/modules/workspace/effects.d.ts +10 -0
  143. package/dist/src/modules/workspace/effects.d.ts.map +1 -1
  144. package/dist/src/modules/workspace/effects.js +1 -1
  145. package/dist/src/modules/workspace/effects.js.map +1 -1
  146. package/dist/src/modules/workspace/index.d.ts +246 -27
  147. package/dist/src/modules/workspace/index.d.ts.map +1 -1
  148. package/dist/src/modules/workspace/index.js +1216 -236
  149. package/dist/src/modules/workspace/index.js.map +1 -1
  150. package/dist/src/modules/workspace/read-page.d.ts +41 -0
  151. package/dist/src/modules/workspace/read-page.d.ts.map +1 -0
  152. package/dist/src/modules/workspace/read-page.js +213 -0
  153. package/dist/src/modules/workspace/read-page.js.map +1 -0
  154. package/dist/src/modules/workspace/result-text.d.ts +67 -0
  155. package/dist/src/modules/workspace/result-text.d.ts.map +1 -0
  156. package/dist/src/modules/workspace/result-text.js +94 -0
  157. package/dist/src/modules/workspace/result-text.js.map +1 -0
  158. package/dist/src/modules/workspace/text.d.ts +29 -12
  159. package/dist/src/modules/workspace/text.d.ts.map +1 -1
  160. package/dist/src/modules/workspace/text.js +35 -33
  161. package/dist/src/modules/workspace/text.js.map +1 -1
  162. package/dist/src/modules/workspace/types.d.ts +41 -17
  163. package/dist/src/modules/workspace/types.d.ts.map +1 -1
  164. package/dist/src/modules/workspace/types.js.map +1 -1
  165. package/dist/src/sent-rows.d.ts +59 -0
  166. package/dist/src/sent-rows.d.ts.map +1 -0
  167. package/dist/src/sent-rows.js +145 -0
  168. package/dist/src/sent-rows.js.map +1 -0
  169. package/dist/src/silent-heartbeat.d.ts +17 -10
  170. package/dist/src/silent-heartbeat.d.ts.map +1 -1
  171. package/dist/src/silent-heartbeat.js.map +1 -1
  172. package/dist/src/stable-hash.d.ts +3 -0
  173. package/dist/src/stable-hash.d.ts.map +1 -0
  174. package/dist/src/stable-hash.js +15 -0
  175. package/dist/src/stable-hash.js.map +1 -0
  176. package/dist/src/stored-injections.d.ts +99 -0
  177. package/dist/src/stored-injections.d.ts.map +1 -0
  178. package/dist/src/stored-injections.js +106 -0
  179. package/dist/src/stored-injections.js.map +1 -0
  180. package/dist/src/text-as-stored.d.ts +4 -3
  181. package/dist/src/text-as-stored.d.ts.map +1 -1
  182. package/dist/src/text-as-stored.js +4 -3
  183. package/dist/src/text-as-stored.js.map +1 -1
  184. package/dist/src/thinking-drops.d.ts +81 -0
  185. package/dist/src/thinking-drops.d.ts.map +1 -0
  186. package/dist/src/thinking-drops.js +103 -0
  187. package/dist/src/thinking-drops.js.map +1 -0
  188. package/dist/src/tool-image-ledger.d.ts +9 -8
  189. package/dist/src/tool-image-ledger.d.ts.map +1 -1
  190. package/dist/src/tool-image-ledger.js +9 -8
  191. package/dist/src/tool-image-ledger.js.map +1 -1
  192. package/dist/src/tool-presentation.d.ts.map +1 -1
  193. package/dist/src/tool-presentation.js +3 -1
  194. package/dist/src/tool-presentation.js.map +1 -1
  195. package/dist/src/tool-result-guard.d.ts +4 -2
  196. package/dist/src/tool-result-guard.d.ts.map +1 -1
  197. package/dist/src/tool-result-guard.js +21 -6
  198. package/dist/src/tool-result-guard.js.map +1 -1
  199. package/dist/src/tool-result-history.d.ts +186 -21
  200. package/dist/src/tool-result-history.d.ts.map +1 -1
  201. package/dist/src/tool-result-history.js +428 -89
  202. package/dist/src/tool-result-history.js.map +1 -1
  203. package/dist/src/tune-out/coordinator.d.ts +17 -9
  204. package/dist/src/tune-out/coordinator.d.ts.map +1 -1
  205. package/dist/src/tune-out/coordinator.js +84 -37
  206. package/dist/src/tune-out/coordinator.js.map +1 -1
  207. package/dist/src/types/agent.d.ts +186 -7
  208. package/dist/src/types/agent.d.ts.map +1 -1
  209. package/dist/src/types/events.d.ts +19 -2
  210. package/dist/src/types/events.d.ts.map +1 -1
  211. package/dist/src/types/framework.d.ts +24 -3
  212. package/dist/src/types/framework.d.ts.map +1 -1
  213. package/dist/src/types/module.d.ts +90 -7
  214. package/dist/src/types/module.d.ts.map +1 -1
  215. package/dist/src/types/trace.d.ts +57 -2
  216. package/dist/src/types/trace.d.ts.map +1 -1
  217. package/package.json +4 -3
  218. package/src/agent.ts +593 -60
  219. package/src/api/server.ts +1 -2
  220. package/src/auto-continue.ts +42 -0
  221. package/src/code-execution/isolate/scripts.ts +11 -3
  222. package/src/framework.ts +4004 -1026
  223. package/src/gate/event-gate.ts +2315 -389
  224. package/src/gate/index.ts +2 -1
  225. package/src/gate/recipe-reconcile.ts +355 -0
  226. package/src/gate/types.ts +70 -8
  227. package/src/index.ts +7 -4
  228. package/src/mcpl/channel-registry.ts +1220 -467
  229. package/src/mcpl/conversation-router.ts +34 -16
  230. package/src/mcpl/convert-block.ts +14 -1
  231. package/src/mcpl/hook-orchestrator.ts +33 -6
  232. package/src/mcpl/index.ts +6 -1
  233. package/src/mcpl/inference-router.ts +79 -34
  234. package/src/mcpl/prose-grammar.ts +7 -0
  235. package/src/mcpl/protocol-family.ts +2 -0
  236. package/src/mcpl/push-coalescer.ts +51 -13
  237. package/src/mcpl/reference-fetcher.ts +201 -162
  238. package/src/mcpl/references.ts +852 -78
  239. package/src/mcpl/server-connection.ts +145 -7
  240. package/src/mcpl/tool-lifecycle.ts +21 -0
  241. package/src/mcpl/tool-presence.ts +650 -0
  242. package/src/mcpl/tool-result-normalize.ts +18 -6
  243. package/src/mcpl/tool-signature.ts +285 -0
  244. package/src/mcpl/toolset-host.ts +637 -0
  245. package/src/mcpl/toolsets-module.ts +451 -0
  246. package/src/mcpl/transport.ts +3 -2
  247. package/src/mcpl/types.ts +22 -1
  248. package/src/mcpl/visible-content.ts +22 -9
  249. package/src/module-registry.ts +77 -24
  250. package/src/modules/discord/index.ts +1 -1
  251. package/src/modules/history/index.ts +48 -18
  252. package/src/modules/history/semantic.ts +308 -54
  253. package/src/modules/workspace/direct-write.ts +425 -0
  254. package/src/modules/workspace/edit-text.ts +82 -35
  255. package/src/modules/workspace/effects.ts +1 -1
  256. package/src/modules/workspace/index.ts +1274 -241
  257. package/src/modules/workspace/read-page.ts +222 -0
  258. package/src/modules/workspace/result-text.ts +119 -0
  259. package/src/modules/workspace/text.ts +58 -32
  260. package/src/modules/workspace/types.ts +41 -17
  261. package/src/sent-rows.ts +162 -0
  262. package/src/silent-heartbeat.ts +17 -10
  263. package/src/stable-hash.ts +14 -0
  264. package/src/stored-injections.ts +131 -0
  265. package/src/text-as-stored.ts +4 -3
  266. package/src/thinking-drops.ts +141 -0
  267. package/src/tool-image-ledger.ts +9 -8
  268. package/src/tool-presentation.ts +3 -1
  269. package/src/tool-result-guard.ts +19 -6
  270. package/src/tool-result-history.ts +488 -88
  271. package/src/tune-out/coordinator.ts +80 -37
  272. package/src/types/agent.ts +189 -7
  273. package/src/types/events.ts +19 -2
  274. package/src/types/framework.ts +21 -3
  275. package/src/types/module.ts +86 -7
  276. package/src/types/trace.ts +58 -2
@@ -7,7 +7,10 @@
7
7
  * An MCP result has two views, and each consumer gets its own:
8
8
  * - **The content array is the server's model-facing view.** Text stays text,
9
9
  * and an inline image stays native when a provider can take it: one of the
10
- * types providers accept, within their per-image size. Anything else the
10
+ * types providers accept, within the framework's own per-image limit. That
11
+ * limit applies to every image this reading carries, whichever route it goes
12
+ * to: it's kept for providers that enforce one, though some routes take
13
+ * more. Anything else the
11
14
  * provider format can't carry (audio, binary blobs, other images) is saved
12
15
  * to the workspace, and a bounded stub says where it went. A stub tells the
13
16
  * reader where the payload is; nothing is dropped silently. If a payload
@@ -48,8 +51,13 @@ const STUB_FIELD_POINTS = 200;
48
51
  const URI_POINTS = 2048;
49
52
  /** The image types providers take inline. Any other image is saved. */
50
53
  const NATIVE_IMAGE_TYPES = new Set(['image/png', 'image/jpeg', 'image/gif', 'image/webp']);
51
- /** The decoded size providers take per inline image: the same 5 MB ceiling
52
- * the framework's read_image tool guards. A larger image is saved. */
54
+ /** This reading's per-image limit on decoded size, applied to every image it
55
+ * carries, whichever route it goes to: a larger image is shown as a stub, and
56
+ * saved to the workspace where it can be (payloadStub). It's kept for
57
+ * providers that enforce one, though some routes take more. The framework's
58
+ * read_image tool refuses an image past the same 5 MB. The RFC-005 lanes (an
59
+ * MCPL peer's or a module's content, and the push lanes) hold an image to no
60
+ * such limit. */
53
61
  const NATIVE_IMAGE_MAX_BYTES = 5 * 1024 * 1024;
54
62
 
55
63
  /** Why a payload isn't shown inline, unless said otherwise. */
@@ -136,13 +144,17 @@ export interface StandardToolResult {
136
144
  * Content that is present but isn't a list of blocks makes it a malformed
137
145
  * result, never an empty success. A tool's error result with no words, or
138
146
  * only whitespace, reads as `noWords`, which the framework words for its
139
- * server and tool.
147
+ * server and tool. A payload stub with no `save` says why nothing could
148
+ * keep it in `noSave`: no workspace mounted unless the caller words it
149
+ * otherwise, as the framework does for a host whose only writable mount
150
+ * is direct.
140
151
  */
141
152
  export async function normalizeStandardToolResult(
142
153
  result: StandardToolResult,
143
154
  label: string,
144
155
  save: SaveToolPayload | null,
145
156
  noWords = 'Tool call failed',
157
+ noSave = 'no workspace is mounted to save it',
146
158
  ): Promise<ToolResult> {
147
159
  const malformed = result.content !== undefined && !Array.isArray(result.content);
148
160
  const raw: unknown[] = Array.isArray(result.content) ? result.content : [];
@@ -162,7 +174,7 @@ export async function normalizeStandardToolResult(
162
174
  const what = `[${kind}${opts.origin ? ` ${bounded(opts.origin, URI_POINTS)}` : ''}: ${bounded(mimeType)}, ${size}`;
163
175
  if (!save) {
164
176
  lost++;
165
- return `${what}. Not retained: ${why}, and no workspace is mounted to save it]`;
177
+ return `${what}. Not retained: ${why}, and ${noSave}]`;
166
178
  }
167
179
  const fileName = `${label}-${++saved}.${extensionFor(mimeType)}`;
168
180
  const path = await save(fileName, Buffer.from(base64, 'base64'), mimeType).catch((error: unknown) => {
@@ -202,7 +214,7 @@ export async function normalizeStandardToolResult(
202
214
  // provider may refuse, and with it the whole request.
203
215
  blocks.push({ type: 'image', data: block.data.replace(/\s+/g, ''), mimeType: native });
204
216
  } else {
205
- const why = native ? `over the ${humanSize(NATIVE_IMAGE_MAX_BYTES)} a model takes per image` : UNSUPPORTED;
217
+ const why = native ? `over the ${humanSize(NATIVE_IMAGE_MAX_BYTES)} this host sends inline per image` : UNSUPPORTED;
206
218
  blocks.push({ type: 'text', text: await payloadStub(kind, block.data, mimeType, { why }) });
207
219
  }
208
220
  break;
@@ -0,0 +1,285 @@
1
+ /**
2
+ * How a dynamic tool reads to a model when it reaches the conversation
3
+ * instead of the tool list (MCPL RFC-014 §8.3): a compact signature, its
4
+ * first sentence the first time a definition is shown, the whole definition
5
+ * on request. Also the host's own argument check against a tool's current
6
+ * definition (RFC-014 §6.1: "the host checks arguments for that instance
7
+ * against it").
8
+ *
9
+ * Ported from mcpl-cc-bridge's tool-render.ts and schema-check.ts, the
10
+ * reference host of RFC-014. Dependency-free.
11
+ */
12
+ import { createHash } from 'node:crypto';
13
+
14
+ export type JsonSchema = Record<string, unknown>;
15
+
16
+ /** A tool definition as a server lists it (MCP), with its `_meta` hints. */
17
+ export interface HintedTool {
18
+ name: string;
19
+ description?: string;
20
+ inputSchema?: unknown;
21
+ annotations?: unknown;
22
+ _meta?: Record<string, unknown>;
23
+ }
24
+
25
+ const isObj = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
26
+
27
+ function typeSig(schema: JsonSchema | undefined, depth = 0): string {
28
+ if (!schema || typeof schema !== 'object') return 'any';
29
+ if (Array.isArray(schema.enum)) {
30
+ // The enum is often the menu (a decision's options): all of it unless it is long.
31
+ const vals = (schema.enum as unknown[]).map((v) => JSON.stringify(v));
32
+ if (vals.length <= 8 || vals.join('|').length <= 400) return vals.join('|');
33
+ let shown = 0;
34
+ let len = 0;
35
+ while (shown < vals.length && len + vals[shown]!.length < 300) {
36
+ len += vals[shown]!.length + 1;
37
+ shown++;
38
+ }
39
+ return `${vals.slice(0, Math.max(shown, 4)).join('|')}|…(${vals.length} in all)`;
40
+ }
41
+ if (schema.const !== undefined) return JSON.stringify(schema.const);
42
+ const alts = (schema.oneOf ?? schema.anyOf) as JsonSchema[] | undefined;
43
+ if (Array.isArray(alts)) return alts.map((s) => typeSig(s, depth + 1)).join(' | ');
44
+ const t = Array.isArray(schema.type) ? (schema.type as string[]).join('|') : (schema.type as string | undefined);
45
+ switch (t) {
46
+ case 'integer':
47
+ case 'number': {
48
+ const base = t === 'integer' ? 'int' : 'number';
49
+ const lo = typeof schema.minimum === 'number' ? schema.minimum : undefined;
50
+ const hi = typeof schema.maximum === 'number' ? schema.maximum : undefined;
51
+ if (lo !== undefined && hi !== undefined) return `${base} ${lo}..${hi}`;
52
+ if (lo !== undefined) return `${base} ≥${lo}`;
53
+ if (hi !== undefined) return `${base} ≤${hi}`;
54
+ return base;
55
+ }
56
+ case 'array': {
57
+ const inner = typeSig(schema.items as JsonSchema | undefined, depth + 1);
58
+ const wrapped = /[ |]/.test(inner) && !inner.startsWith('{') ? `(${inner})` : inner;
59
+ const max = typeof schema.maxItems === 'number' ? `≤${schema.maxItems}` : '';
60
+ return `${wrapped}[${max}]`;
61
+ }
62
+ case 'object': {
63
+ const props = schema.properties as Record<string, JsonSchema> | undefined;
64
+ if (!isObj(props) || Object.keys(props).length === 0) return 'object';
65
+ if (depth >= 3) return '{…}';
66
+ const req = new Set(Array.isArray(schema.required) ? (schema.required as string[]) : []);
67
+ return `{${Object.entries(props).map(([k, v]) => `${k}${req.has(k) ? '' : '?'}: ${typeSig(v, depth + 1)}`).join(', ')}}`;
68
+ }
69
+ case undefined:
70
+ return 'any';
71
+ default:
72
+ return t;
73
+ }
74
+ }
75
+
76
+ const schemaOf = (tool: HintedTool): JsonSchema => (isObj(tool.inputSchema) ? tool.inputSchema : {});
77
+
78
+ /** `play(point: string)`, `move(forward?: int -10..10 =0, turn?: int)` */
79
+ export function signature(tool: HintedTool): string {
80
+ const s = schemaOf(tool);
81
+ const props = (isObj(s.properties) ? s.properties : {}) as Record<string, JsonSchema>;
82
+ const req = new Set(Array.isArray(s.required) ? (s.required as string[]) : []);
83
+ const params = Object.entries(props).map(([k, v]) => {
84
+ const def = isObj(v) && v.default !== undefined ? ` =${JSON.stringify(v.default)}` : '';
85
+ return `${k}${req.has(k) ? '' : '?'}: ${typeSig(v, 1)}${def}`;
86
+ });
87
+ return `${tool.name}(${params.join(', ')})`;
88
+ }
89
+
90
+ /** An options schema as `{size?: 9|13|19 =9, …}` (a toolset's options). */
91
+ export function objectSignature(schema: unknown): string {
92
+ const inner = signature({ name: '', inputSchema: schema }).slice(1, -1);
93
+ return inner ? `{${inner}}` : '{}';
94
+ }
95
+
96
+ /** First sentence of a description, bounded. */
97
+ export function firstSentence(text: string | undefined, max = 140): string {
98
+ const t = (text ?? '').trim().replace(/\s+/g, ' ');
99
+ // A sentence ends at . ! ? followed by a capital, a bracket, or the end, so "e.g. \"D4\"" does not end it.
100
+ const m = t.match(/^(.+?[.!?])(?=\s+[A-Z([]|$)/);
101
+ const s = m ? m[1]! : t;
102
+ return s.length > max ? s.slice(0, max - 1) + '…' : s;
103
+ }
104
+
105
+ function canonicalJson(value: unknown): string {
106
+ if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null';
107
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
108
+ const obj = value as Record<string, unknown>;
109
+ return `{${Object.keys(obj)
110
+ .sort()
111
+ .filter((k) => obj[k] !== undefined)
112
+ .map((k) => `${JSON.stringify(k)}:${canonicalJson(obj[k])}`)
113
+ .join(',')}}`;
114
+ }
115
+
116
+ /** Identity of a definition as the model reads it (name, description, schema). */
117
+ export function toolDigest(tool: HintedTool): string {
118
+ return createHash('sha256')
119
+ .update(canonicalJson({ name: tool.name, description: tool.description ?? '', inputSchema: tool.inputSchema ?? null }))
120
+ .digest('base64url')
121
+ .slice(0, 22);
122
+ }
123
+
124
+ /**
125
+ * Tool lines as the model reads them. A definition the model has been shown
126
+ * gets its signature only; a new or changed one also gets its first
127
+ * sentence. `shown` is updated.
128
+ */
129
+ export function renderToolLines(tools: HintedTool[], shown: Set<string>, opts: { describeAll?: boolean; indent?: string } = {}): string[] {
130
+ const indent = opts.indent ?? ' ';
131
+ return tools.map((t) => {
132
+ const d = toolDigest(t);
133
+ const describe = opts.describeAll || !shown.has(d);
134
+ shown.add(d);
135
+ const sentence = describe ? firstSentence(t.description) : '';
136
+ return `${indent}${signature(t)}${sentence ? ` — ${sentence}` : ''}`;
137
+ });
138
+ }
139
+
140
+ /** The whole definition, for `look` naming a tool. */
141
+ export function renderToolDefinition(tool: HintedTool): string {
142
+ const cls = tool._meta?.['mcpl/class'];
143
+ return [
144
+ signature(tool),
145
+ tool.description ?? '',
146
+ ...(Array.isArray(cls) ? [`class: ${cls.join(', ')}`] : []),
147
+ 'inputSchema:',
148
+ JSON.stringify(tool.inputSchema ?? {}, null, 2),
149
+ ]
150
+ .filter((l) => l !== '')
151
+ .join('\n');
152
+ }
153
+
154
+ // ── Argument checks ──────────────────────────────────────────────────────────
155
+ //
156
+ // A deliberate subset of JSON Schema: type, enum, const, required, properties,
157
+ // additionalProperties, items, min/maxItems, minimum, maximum,
158
+ // exclusiveMinimum/Maximum, min/maxLength, anyOf, oneOf, allOf. Anything else
159
+ // isn't checked, so the check can only fail to refuse what the server would
160
+ // refuse on those keywords, never refuse what it would accept; the server
161
+ // checks every call itself.
162
+ //
163
+ // The schema is the server's, so its cost is bounded here: `pattern` is never
164
+ // evaluated (a server's regular expression run on the host's event loop could
165
+ // backtrack for as long as it likes), and a schema nested deeper than
166
+ // MAX_CHECK_DEPTH, or whose check would take more than MAX_CHECK_STEPS steps
167
+ // (anyOf fan-out, a long enum), isn't checked at all.
168
+
169
+ /** Schema levels the check descends before it gives up and lets the server decide. */
170
+ export const MAX_CHECK_DEPTH = 24;
171
+ /** Schema nodes and enum entries the check visits before it gives up. */
172
+ export const MAX_CHECK_STEPS = 5_000;
173
+
174
+ const OVER_BUDGET: unique symbol = Symbol('over budget');
175
+
176
+ function typeOk(t: string, v: unknown): boolean {
177
+ switch (t) {
178
+ case 'object':
179
+ return isObj(v);
180
+ case 'array':
181
+ return Array.isArray(v);
182
+ case 'string':
183
+ return typeof v === 'string';
184
+ case 'number':
185
+ return typeof v === 'number' && Number.isFinite(v);
186
+ case 'integer':
187
+ return Number.isInteger(v);
188
+ case 'boolean':
189
+ return typeof v === 'boolean';
190
+ case 'null':
191
+ return v === null;
192
+ default:
193
+ return true;
194
+ }
195
+ }
196
+
197
+ /**
198
+ * The first problem with `value` against `schema`, as a short phrase naming
199
+ * where; null when it fits, or when the schema is past the check's bounds
200
+ * (the server decides those).
201
+ */
202
+ export function checkArgs(schema: unknown, value: unknown): string | null {
203
+ const budget = { steps: 0 };
204
+ try {
205
+ return check(schema, value, 'args', 0, budget);
206
+ } catch (error) {
207
+ if (error === OVER_BUDGET) return null;
208
+ throw error;
209
+ }
210
+ }
211
+
212
+ function spend(budget: { steps: number }, steps: number): void {
213
+ budget.steps += steps;
214
+ if (budget.steps > MAX_CHECK_STEPS) throw OVER_BUDGET;
215
+ }
216
+
217
+ function check(schema: unknown, value: unknown, at: string, depth: number, budget: { steps: number }): string | null {
218
+ if (!isObj(schema)) return null;
219
+ if (depth > MAX_CHECK_DEPTH) throw OVER_BUDGET;
220
+ spend(budget, 1);
221
+ const s = schema;
222
+ if (s.type !== undefined) {
223
+ const types = Array.isArray(s.type) ? (s.type as string[]) : [s.type as string];
224
+ if (!types.some((t) => typeOk(t, value))) return `${at}: must be ${types.join(' or ')}`;
225
+ }
226
+ if (Array.isArray(s.enum) || s.const !== undefined) {
227
+ const own = canonicalJson(value);
228
+ if (Array.isArray(s.enum)) {
229
+ spend(budget, s.enum.length);
230
+ if (!s.enum.some((e) => canonicalJson(e) === own)) return `${at}: must be one of ${s.enum.map((e) => JSON.stringify(e)).join('|')}`;
231
+ }
232
+ if (s.const !== undefined && canonicalJson(s.const) !== own) return `${at}: must be ${JSON.stringify(s.const)}`;
233
+ }
234
+ if (typeof value === 'string') {
235
+ const n = [...value].length;
236
+ if (typeof s.minLength === 'number' && n < s.minLength) return `${at}: at least ${s.minLength} characters`;
237
+ if (typeof s.maxLength === 'number' && n > s.maxLength) return `${at}: at most ${s.maxLength} characters`;
238
+ }
239
+ if (typeof value === 'number') {
240
+ if (typeof s.minimum === 'number' && value < s.minimum) return `${at}: at least ${s.minimum}`;
241
+ if (typeof s.maximum === 'number' && value > s.maximum) return `${at}: at most ${s.maximum}`;
242
+ if (typeof s.exclusiveMinimum === 'number' && value <= s.exclusiveMinimum) return `${at}: more than ${s.exclusiveMinimum}`;
243
+ if (typeof s.exclusiveMaximum === 'number' && value >= s.exclusiveMaximum) return `${at}: less than ${s.exclusiveMaximum}`;
244
+ }
245
+ if (Array.isArray(value)) {
246
+ if (typeof s.minItems === 'number' && value.length < s.minItems) return `${at}: at least ${s.minItems} items`;
247
+ if (typeof s.maxItems === 'number' && value.length > s.maxItems) return `${at}: at most ${s.maxItems} items`;
248
+ if (isObj(s.items)) {
249
+ for (let i = 0; i < value.length; i++) {
250
+ const e = check(s.items, value[i], `${at}[${i}]`, depth + 1, budget);
251
+ if (e) return e;
252
+ }
253
+ }
254
+ }
255
+ if (isObj(value)) {
256
+ const props = isObj(s.properties) ? s.properties : {};
257
+ for (const r of Array.isArray(s.required) ? (s.required as unknown[]) : []) {
258
+ if (typeof r === 'string' && !Object.hasOwn(value, r)) return `${at}.${r}: required`;
259
+ }
260
+ for (const [k, v] of Object.entries(value)) {
261
+ if (Object.hasOwn(props, k)) {
262
+ const e = check(props[k], v, `${at}.${k}`, depth + 1, budget);
263
+ if (e) return e;
264
+ } else if (s.additionalProperties === false) {
265
+ return `${at}.${k}: not an argument of this tool`;
266
+ } else if (isObj(s.additionalProperties)) {
267
+ const e = check(s.additionalProperties, v, `${at}.${k}`, depth + 1, budget);
268
+ if (e) return e;
269
+ }
270
+ }
271
+ }
272
+ for (const k of ['anyOf', 'oneOf'] as const) {
273
+ const alts = s[k];
274
+ if (Array.isArray(alts) && alts.length && !alts.some((a) => check(a, value, at, depth + 1, budget) === null)) {
275
+ return check(alts[0], value, at, depth + 1, budget) ?? `${at}: fits none of the allowed forms`;
276
+ }
277
+ }
278
+ if (Array.isArray(s.allOf)) {
279
+ for (const a of s.allOf) {
280
+ const e = check(a, value, at, depth + 1, budget);
281
+ if (e) return e;
282
+ }
283
+ }
284
+ return null;
285
+ }