@spendgraph/tools 0.5.0 → 0.7.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 (258) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +26 -258
  3. package/dist/agents/index.d.ts +1 -0
  4. package/dist/agents/index.js +1 -0
  5. package/dist/agents/moa/design.js +5 -0
  6. package/dist/agents/moa/http.js +1 -0
  7. package/dist/{builtin/write-document/write-document.d.ts → agents/moa/index.d.ts} +7 -2
  8. package/dist/agents/moa/index.js +1 -0
  9. package/dist/{builtin/write-document/commission.d.ts → agents/moa/moa.d.ts} +15 -15
  10. package/dist/agents/moa/moa.js +13 -0
  11. package/dist/{builtin/write-document → agents/moa}/types.d.ts +9 -9
  12. package/dist/agents/moa/types.js +0 -0
  13. package/dist/bogus/ask-mum.d.ts +5 -0
  14. package/dist/bogus/ask-mum.js +1 -0
  15. package/dist/bogus/book-taxi.d.ts +9 -0
  16. package/dist/bogus/book-taxi.js +1 -0
  17. package/dist/bogus/bus.d.ts +8 -0
  18. package/dist/bogus/bus.js +1 -0
  19. package/dist/bogus/calculator.d.ts +14 -0
  20. package/dist/bogus/calculator.js +1 -0
  21. package/dist/bogus/dictionary.d.ts +6 -0
  22. package/dist/bogus/dictionary.js +1 -0
  23. package/dist/bogus/feed-cat.d.ts +8 -0
  24. package/dist/bogus/feed-cat.js +1 -0
  25. package/dist/bogus/football-score.d.ts +6 -0
  26. package/dist/bogus/football-score.js +1 -0
  27. package/dist/bogus/gold-price.d.ts +7 -0
  28. package/dist/bogus/gold-price.js +1 -0
  29. package/dist/bogus/index.d.ts +14 -0
  30. package/dist/bogus/index.js +1 -0
  31. package/dist/bogus/my-calendar.d.ts +6 -0
  32. package/dist/bogus/my-calendar.js +1 -0
  33. package/dist/bogus/my-notes.d.ts +6 -0
  34. package/dist/bogus/my-notes.js +1 -0
  35. package/dist/bogus/my-recipes.d.ts +6 -0
  36. package/dist/bogus/my-recipes.js +1 -0
  37. package/dist/bogus/order-pizza.d.ts +12 -0
  38. package/dist/bogus/order-pizza.js +1 -0
  39. package/dist/bogus/prayer-times.d.ts +6 -0
  40. package/dist/bogus/prayer-times.js +1 -0
  41. package/dist/bogus/receipt.d.ts +8 -0
  42. package/dist/bogus/receipt.js +1 -0
  43. package/dist/builtin/calculate/calculate.d.ts +2 -0
  44. package/dist/builtin/calculate/calculate.js +1 -21
  45. package/dist/builtin/calculate/exact.d.ts +16 -0
  46. package/dist/builtin/calculate/exact.js +1 -0
  47. package/dist/builtin/calculate/index.d.ts +2 -1
  48. package/dist/builtin/calculate/index.js +1 -2
  49. package/dist/builtin/calculate/parse.js +1 -137
  50. package/dist/builtin/clock/clock.js +1 -61
  51. package/dist/builtin/clock/index.js +1 -1
  52. package/dist/builtin/code-exec/code-exec.d.ts +42 -0
  53. package/dist/builtin/code-exec/code-exec.js +1 -0
  54. package/dist/builtin/code-exec/index.d.ts +1 -0
  55. package/dist/builtin/code-exec/index.js +1 -0
  56. package/dist/builtin/date/date.d.ts +1 -1
  57. package/dist/builtin/date/date.js +1 -41
  58. package/dist/builtin/date/index.d.ts +1 -0
  59. package/dist/builtin/date/index.js +1 -2
  60. package/dist/builtin/date/shift.d.ts +1 -1
  61. package/dist/builtin/date/shift.js +1 -47
  62. package/dist/builtin/date/words.d.ts +13 -0
  63. package/dist/builtin/date/words.js +1 -0
  64. package/dist/builtin/decline/decline.d.ts +49 -0
  65. package/dist/builtin/decline/decline.js +1 -0
  66. package/dist/builtin/decline/index.d.ts +1 -0
  67. package/dist/builtin/decline/index.js +1 -0
  68. package/dist/builtin/escalate/escalate.d.ts +19 -0
  69. package/dist/builtin/escalate/escalate.js +1 -20
  70. package/dist/builtin/escalate/index.d.ts +1 -1
  71. package/dist/builtin/escalate/index.js +1 -1
  72. package/dist/builtin/extract/extract.d.ts +25 -0
  73. package/dist/builtin/extract/extract.js +1 -0
  74. package/dist/builtin/extract/index.d.ts +3 -0
  75. package/dist/builtin/extract/index.js +1 -0
  76. package/dist/builtin/extract/kinds.d.ts +6 -0
  77. package/dist/builtin/extract/kinds.js +1 -0
  78. package/dist/builtin/extract/readers.d.ts +7 -0
  79. package/dist/builtin/extract/readers.js +9 -0
  80. package/dist/builtin/finish/finish.d.ts +4 -0
  81. package/dist/builtin/finish/finish.js +1 -26
  82. package/dist/builtin/finish/index.js +1 -1
  83. package/dist/builtin/http/allow.d.ts +2 -0
  84. package/dist/builtin/http/allow.js +1 -37
  85. package/dist/builtin/http/hops.d.ts +10 -0
  86. package/dist/builtin/http/hops.js +1 -0
  87. package/dist/builtin/http/http.d.ts +9 -1
  88. package/dist/builtin/http/http.js +1 -41
  89. package/dist/builtin/http/index.d.ts +2 -0
  90. package/dist/builtin/http/index.js +1 -2
  91. package/dist/builtin/http/retry-after.d.ts +14 -0
  92. package/dist/builtin/http/retry-after.js +1 -0
  93. package/dist/builtin/index.d.ts +11 -6
  94. package/dist/builtin/index.js +1 -11
  95. package/dist/builtin/json/index.d.ts +1 -0
  96. package/dist/builtin/json/index.js +1 -2
  97. package/dist/builtin/json/json.js +1 -37
  98. package/dist/builtin/json/parse.d.ts +15 -0
  99. package/dist/builtin/json/parse.js +1 -0
  100. package/dist/builtin/json/path.js +1 -56
  101. package/dist/builtin/notes/index.d.ts +1 -0
  102. package/dist/builtin/notes/index.js +1 -0
  103. package/dist/builtin/notes/notes.d.ts +45 -0
  104. package/dist/builtin/notes/notes.js +2 -0
  105. package/dist/builtin/openalex/api.d.ts +22 -0
  106. package/dist/builtin/openalex/api.js +1 -0
  107. package/dist/builtin/openalex/index.d.ts +4 -0
  108. package/dist/builtin/openalex/index.js +1 -0
  109. package/dist/builtin/openalex/openalex.d.ts +23 -0
  110. package/dist/builtin/openalex/openalex.js +1 -0
  111. package/dist/builtin/openalex/render.d.ts +2 -0
  112. package/dist/builtin/openalex/render.js +4 -0
  113. package/dist/builtin/polite.d.ts +14 -0
  114. package/dist/builtin/polite.js +1 -0
  115. package/dist/builtin/publish/index.d.ts +1 -0
  116. package/dist/builtin/publish/index.js +1 -0
  117. package/dist/builtin/publish/publish.d.ts +46 -0
  118. package/dist/builtin/publish/publish.js +1 -0
  119. package/dist/builtin/think/index.js +1 -1
  120. package/dist/builtin/think/think.js +1 -12
  121. package/dist/builtin/web-search/index.js +1 -1
  122. package/dist/builtin/web-search/web-search.d.ts +10 -30
  123. package/dist/builtin/web-search/web-search.js +1 -103
  124. package/dist/builtin/wikidata/api.d.ts +40 -0
  125. package/dist/builtin/wikidata/api.js +1 -0
  126. package/dist/builtin/wikidata/index.d.ts +5 -0
  127. package/dist/builtin/wikidata/index.js +1 -0
  128. package/dist/builtin/wikidata/render.d.ts +13 -0
  129. package/dist/builtin/wikidata/render.js +2 -0
  130. package/dist/builtin/wikidata/wikidata.d.ts +21 -0
  131. package/dist/builtin/wikidata/wikidata.js +1 -0
  132. package/dist/bus/args.d.ts +11 -0
  133. package/dist/bus/args.js +1 -0
  134. package/dist/bus/bus.d.ts +23 -1
  135. package/dist/bus/bus.js +1 -49
  136. package/dist/bus/group.d.ts +21 -0
  137. package/dist/bus/group.js +1 -0
  138. package/dist/bus/index.d.ts +4 -0
  139. package/dist/bus/index.js +1 -2
  140. package/dist/bus/invoke.d.ts +1 -1
  141. package/dist/bus/invoke.js +3 -43
  142. package/dist/bus/namespace.d.ts +21 -0
  143. package/dist/bus/namespace.js +1 -0
  144. package/dist/bus/once.d.ts +32 -0
  145. package/dist/bus/once.js +1 -0
  146. package/dist/bus/only.d.ts +29 -0
  147. package/dist/bus/only.js +0 -0
  148. package/dist/bus/options.d.ts +31 -0
  149. package/dist/bus/options.js +0 -1
  150. package/dist/bus/output.d.ts +26 -0
  151. package/dist/bus/output.js +2 -0
  152. package/dist/bus/overlap.js +1 -31
  153. package/dist/bus/score.d.ts +11 -9
  154. package/dist/bus/score.js +1 -12
  155. package/dist/bus/select.d.ts +2 -17
  156. package/dist/bus/select.js +1 -44
  157. package/dist/bus/spent.d.ts +16 -0
  158. package/dist/bus/spent.js +1 -0
  159. package/dist/bus/step.js +1 -11
  160. package/dist/bus/terms.d.ts +25 -0
  161. package/dist/bus/terms.js +1 -0
  162. package/dist/bus/trace.d.ts +14 -1
  163. package/dist/bus/trace.js +1 -22
  164. package/dist/index.d.ts +16 -4
  165. package/dist/index.js +1 -3
  166. package/dist/internals.d.ts +9 -2
  167. package/dist/internals.js +1 -10
  168. package/dist/memories/client.d.ts +91 -0
  169. package/dist/memories/client.js +0 -0
  170. package/dist/memories/context.d.ts +5 -0
  171. package/dist/memories/context.js +1 -0
  172. package/dist/memories/deep-recall/deep-recall.d.ts +67 -0
  173. package/dist/memories/deep-recall/deep-recall.js +1 -0
  174. package/dist/memories/deep-recall/index.d.ts +1 -0
  175. package/dist/memories/deep-recall/index.js +1 -0
  176. package/dist/memories/explore/explore.d.ts +18 -0
  177. package/dist/memories/explore/explore.js +7 -0
  178. package/dist/memories/explore/subgraph.d.ts +12 -0
  179. package/dist/memories/explore/subgraph.js +3 -0
  180. package/dist/memories/index.d.ts +30 -0
  181. package/dist/memories/index.js +1 -0
  182. package/dist/memories/observe/observe.d.ts +7 -0
  183. package/dist/memories/observe/observe.js +1 -0
  184. package/dist/memories/recall/recall.d.ts +42 -0
  185. package/dist/memories/recall/recall.js +1 -0
  186. package/dist/memories/recall/render.d.ts +5 -0
  187. package/dist/memories/recall/render.js +3 -0
  188. package/dist/memories/remember/remember.d.ts +49 -0
  189. package/dist/memories/remember/remember.js +1 -0
  190. package/dist/search/index.d.ts +5 -0
  191. package/dist/search/index.js +1 -0
  192. package/dist/search/retry.d.ts +6 -0
  193. package/dist/search/retry.js +1 -0
  194. package/dist/search/search.d.ts +63 -0
  195. package/dist/search/search.js +1 -0
  196. package/dist/search/searxng/index.d.ts +2 -0
  197. package/dist/search/searxng/index.js +1 -0
  198. package/dist/search/searxng/searxng.d.ts +14 -0
  199. package/dist/search/searxng/searxng.js +5 -0
  200. package/dist/search/searxng/text.d.ts +2 -0
  201. package/dist/search/searxng/text.js +3 -0
  202. package/dist/search/sonar/index.d.ts +2 -0
  203. package/dist/search/sonar/index.js +1 -0
  204. package/dist/search/sonar/reply.js +1 -0
  205. package/dist/search/sonar/sonar.d.ts +13 -0
  206. package/dist/search/sonar/sonar.js +1 -0
  207. package/dist/search/tavily/index.d.ts +4 -0
  208. package/dist/search/tavily/index.js +1 -0
  209. package/dist/search/tavily/reply.d.ts +22 -0
  210. package/dist/search/tavily/reply.js +5 -0
  211. package/dist/search/tavily/tavily.d.ts +14 -0
  212. package/dist/search/tavily/tavily.js +1 -0
  213. package/dist/testing/index.d.ts +2 -0
  214. package/dist/testing/index.js +1 -0
  215. package/dist/testing/replay.d.ts +55 -0
  216. package/dist/testing/replay.js +1 -0
  217. package/dist/testing/store.d.ts +35 -0
  218. package/dist/testing/store.js +4 -0
  219. package/dist/tool/bind.js +1 -64
  220. package/dist/tool/index.js +1 -2
  221. package/dist/tool/tool.d.ts +5 -1
  222. package/dist/tool/tool.js +1 -51
  223. package/dist/types/index.d.ts +1 -1
  224. package/dist/types/index.js +0 -1
  225. package/dist/types/result.d.ts +38 -0
  226. package/dist/types/result.js +0 -1
  227. package/dist/types/tool.d.ts +74 -0
  228. package/dist/types/tool.js +0 -1
  229. package/dist/wire/index.js +1 -3
  230. package/dist/wire/json-schema.js +1 -65
  231. package/dist/wire/markdown.d.ts +5 -3
  232. package/dist/wire/markdown.js +9 -29
  233. package/dist/wire/providers.js +1 -18
  234. package/docs/bogus.mdx +85 -0
  235. package/docs/builtins.mdx +156 -0
  236. package/docs/bus.mdx +90 -0
  237. package/docs/declaring.mdx +85 -0
  238. package/docs/effects.mdx +49 -0
  239. package/docs/overview.mdx +76 -0
  240. package/docs/selecting.mdx +79 -0
  241. package/docs/stored.mdx +83 -0
  242. package/docs/turns.mdx +57 -0
  243. package/package.json +28 -11
  244. package/dist/builtin/deep-recall/deep-recall.d.ts +0 -27
  245. package/dist/builtin/deep-recall/deep-recall.js +0 -45
  246. package/dist/builtin/deep-recall/index.d.ts +0 -1
  247. package/dist/builtin/deep-recall/index.js +0 -1
  248. package/dist/builtin/web-search/reply.js +0 -27
  249. package/dist/builtin/write-document/commission.js +0 -152
  250. package/dist/builtin/write-document/design.js +0 -86
  251. package/dist/builtin/write-document/http.js +0 -71
  252. package/dist/builtin/write-document/index.d.ts +0 -6
  253. package/dist/builtin/write-document/index.js +0 -4
  254. package/dist/builtin/write-document/types.js +0 -1
  255. package/dist/builtin/write-document/write-document.js +0 -142
  256. /package/dist/{builtin/write-document → agents/moa}/design.d.ts +0 -0
  257. /package/dist/{builtin/write-document → agents/moa}/http.d.ts +0 -0
  258. /package/dist/{builtin/web-search → search/sonar}/reply.d.ts +0 -0
@@ -0,0 +1 @@
1
+ import{tool as w}from"../../tool/index.js";import{wikidataApi as b}from"./api.js";import{factsOf as g,referenced as k,rendered as y,statementsOf as x}from"./render.js";const E=30;function L(t){const a=t.indexOf(":");if(a<1)return{name:t.trim(),about:null};const o=t.slice(a+1).trim();return{name:t.slice(0,a).trim(),about:o||null}}function A(t,a){const o=a.toLowerCase().split(/\s+/).filter(Boolean);return t.filter(i=>{const r=i.property.toLowerCase();return o.some(n=>r.includes(n))})}function $(t={}){const a=t.api??b(t),o=t.maxFacts??E;return w({name:"wikidata",effect:"readonly",trust:"external",description:"Looks a thing up on Wikidata and returns what is recorded about it as plain text: what it is, when it began, where it is, who runs it, and so on. Name the thing \u2014 a company, person, place, product or event \u2014 and add ': ' and a topic to narrow it, as in 'Cloudflare: founded' or 'Paris: population'. Not for prices, versions or news.",args:[{name:"question",type:"string",required:!0,maxLength:200,description:"The thing's name, optionally followed by ': ' and what about it."}],run:async({question:i})=>{const{name:r,about:n}=L(i);if(!r)throw new Error("Name the thing to look up.");const s=await a.search(r);if(!s)throw new Error(`Wikidata has nothing named "${r}".`);const e=await a.entity(s.id);if(!e)throw new Error(`Wikidata has no record for ${s.id}.`);const c=x(e).slice(0,o),f=c.flatMap(([p,m])=>[p,...m.flatMap(h=>k(h.mainsnak))]),u=await a.labels(f),d=g(c,u),l=n?A(d,n):d;if(n&&l.length===0)throw new Error(`Wikidata records nothing about "${n}" for ${e.label}.`);return y(e,l)}})}export{$ as wikidata};
@@ -0,0 +1,11 @@
1
+ import type { ArgSpec } from "../types/index.js";
2
+ /**
3
+ * The arguments a handler is given: declared defaults filled in, and every
4
+ * declared value at its declared type.
5
+ *
6
+ * Validation runs before this and on the raw arguments, so what is required
7
+ * stays required — a `default` fills a gap, it does not satisfy a demand.
8
+ * Anything the tool did not declare is passed through untouched: the model may
9
+ * have sent it, and dropping it would be this package deciding it knows better.
10
+ */
11
+ export declare function readyArgs(specs: readonly ArgSpec[], args: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1 @@
1
+ import{isMissing as s,toBoolean as c,toNumber as f}from"@spendgraph/sdk";function i(e,n){switch(e.type){case"number":return f(n);case"boolean":return c(n);case"list":return Array.isArray(n)?n.map(String):n;case"json":return n;default:return String(n)}}function a(e,n){const t={...n};for(const r of e){const o=t[r.name];if(s(o)){if(r.default===void 0)continue;t[r.name]=i(r,r.default);continue}t[r.name]=i(r,o)}return t}export{a as readyArgs};
package/dist/bus/bus.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import type { RolloutStep } from "@spendgraph/sdk";
2
- import type { Effect, Tool, ToolDeclaration, ToolResult } from "../types/index.js";
2
+ import type { Effect, Tool, ToolDeclaration, ToolResult, Trust } from "../types/index.js";
3
3
  import { type AnthropicTool, type OpenAITool } from "../wire/index.js";
4
+ import type { Offering } from "./only.js";
4
5
  import type { ToolBusOptions } from "./options.js";
5
6
  import { type ToolTrace } from "./trace.js";
6
7
  /**
@@ -32,6 +33,8 @@ export declare function toolbus(tools: Tool<never>[], opts?: ToolBusOptions): {
32
33
  * `readonly` — nothing has claimed that.
33
34
  */
34
35
  effectOf: (name: string) => Effect | undefined;
36
+ /** What a tool declared about where its content comes from. Unset means unknown. */
37
+ trustOf: (name: string) => Trust | undefined;
35
38
  /**
36
39
  * Names of the tools declared `destructive`.
37
40
  *
@@ -72,5 +75,24 @@ export declare function toolbus(tools: Tool<never>[], opts?: ToolBusOptions): {
72
75
  step: (result: ToolResult, index: number) => RolloutStep;
73
76
  /** One turn's tool use, selected once and recorded for the rollout. */
74
77
  trace: (query?: string, limit?: number) => ToolTrace;
78
+ /**
79
+ * The same bus, offering only these tools.
80
+ *
81
+ * What progressive disclosure needs: a model is shown the handful this turn
82
+ * is about while everything else stays reachable. Declaring fifty tools
83
+ * costs their whole schema on every call before anyone has said anything,
84
+ * and the cost is not only tokens — a shortlist is chosen from more
85
+ * accurately than a shelf.
86
+ *
87
+ * A view rather than a second bus. `invoke` still goes through this one, so
88
+ * usage, the output cap and a once-each memory stay the ones the caller
89
+ * configured — a `toolbus` rebuilt over a filtered list looks identical and
90
+ * quietly keeps its own, including a fresh record of which destructive
91
+ * calls have already run.
92
+ *
93
+ * Names it does not have are ignored, the way selection ignores a term it
94
+ * cannot match.
95
+ */
96
+ only: (names: string[]) => Offering;
75
97
  };
76
98
  export type ToolBus = ReturnType<typeof toolbus>;
package/dist/bus/bus.js CHANGED
@@ -1,49 +1 @@
1
- import { toAnthropic, toMarkdown, toOpenAI, } from "../wire/index.js";
2
- import { invokeTool } from "./invoke.js";
3
- import { warnOnOverlap } from "./overlap.js";
4
- import { selectTools } from "./select.js";
5
- import { toStep } from "./step.js";
6
- import { newTrace } from "./trace.js";
7
- export function toolbus(tools, opts = {}) {
8
- const now = opts.now ?? (() => Date.now());
9
- const ceiling = Math.max(1, opts.limit ?? 6);
10
- const byName = new Map();
11
- for (const t of tools) {
12
- if (byName.has(t.name))
13
- throw new Error(`Two tools are called "${t.name}".`);
14
- byName.set(t.name, t);
15
- }
16
- const calls = new Map();
17
- if (opts.warnOnOverlap ?? true)
18
- warnOnOverlap([...byName.values()]);
19
- const declare = (t) => ({
20
- name: t.name,
21
- description: t.description,
22
- args: t.args,
23
- ...(t.effect ? { effect: t.effect } : {}),
24
- });
25
- const all = () => [...byName.values()].map(declare);
26
- const bus = {
27
- all: () => [...byName.values()],
28
- missing: (names) => names.filter((n) => !byName.has(n)),
29
- get: (name) => byName.get(name),
30
- effectOf: (name) => byName.get(name)?.effect,
31
- destructive: () => [...byName.values()].filter((t) => t.effect === "destructive").map((t) => t.name),
32
- unannotated: () => [...byName.values()].filter((t) => t.effect === undefined).map((t) => t.name),
33
- declarations: (names) => names
34
- ? names
35
- .map((n) => byName.get(n))
36
- .filter(Boolean)
37
- .map((t) => declare(t))
38
- : all(),
39
- anthropic: (decls) => toAnthropic(decls ?? all()),
40
- openai: (decls) => toOpenAI(decls ?? all()),
41
- markdown: (decls, heading) => toMarkdown(decls ?? all(), heading),
42
- select: (query = "", limit) => selectTools([...byName.values()], calls, declare, query, Math.max(1, limit ?? ceiling)),
43
- invoke: (name, args) => invokeTool(byName, calls, now, name, args),
44
- usage: () => Object.fromEntries(calls),
45
- step: (result, index) => toStep(result, index),
46
- trace: (query = "", limit) => newTrace(bus.select(query, limit), (name, args) => bus.invoke(name, args)),
47
- };
48
- return bus;
49
- }
1
+ import{toAnthropic as g,toMarkdown as h,toOpenAI as O}from"../wire/index.js";import{invokeTool as M}from"./invoke.js";import{onceEach as x}from"./once.js";import{warnOnOverlap as A}from"./overlap.js";import{scorerFor as F}from"./score.js";import{selectTools as k}from"./select.js";import{toStep as j}from"./step.js";import{ceilingFor as u}from"./terms.js";import{newTrace as b}from"./trace.js";function G(y,i={}){const E=i.now??(()=>Date.now()),d=u(i.limit,6),o=new Map;for(const e of y){if(o.has(e.name))throw new Error(`Two tools are called "${e.name}".`);o.set(e.name,e)}const f=new Map;(i.warnOnOverlap??!0)&&A([...o.values()]);const w=F([...o.values()]),v=i.onceEach?x():{earlier:()=>{},keep:()=>{}},c=e=>({name:e.name,description:e.description,args:e.args,...e.aliases?.length?{aliases:e.aliases}:{},...e.effect?{effect:e.effect}:{}}),m=()=>[...o.values()].map(c),l={all:()=>[...o.values()],missing:e=>e.filter(t=>!o.has(t)),get:e=>o.get(e),effectOf:e=>o.get(e)?.effect,trustOf:e=>o.get(e)?.trust,destructive:()=>[...o.values()].filter(e=>e.effect==="destructive").map(e=>e.name),unannotated:()=>[...o.values()].filter(e=>e.effect===void 0).map(e=>e.name),declarations:e=>e?e.map(t=>o.get(t)).filter(Boolean).map(t=>c(t)):m(),anthropic:e=>g(e??m()),openai:e=>O(e??m()),markdown:(e,t)=>h(e??m(),t),select:(e="",t)=>k([...o.values()],f,c,w,e,u(t,d)),invoke:async(e,t)=>{const n=v.earlier(o.get(e),e,t);if(n)return{...n,repeated:!0};const s=await M(o,f,E,e,t,i.maxOutputChars);return v.keep(o.get(e),s),s},usage:()=>Object.fromEntries(f),step:(e,t)=>j(e,t),trace:(e="",t)=>b(l.select(e,t),(n,s)=>l.invoke(n,s)),only:e=>{const t=e.map(r=>o.get(r)).filter(r=>r!==void 0),n=()=>t.map(c),s={...l,all:()=>[...t],missing:r=>r.filter(a=>!t.some(p=>p.name===a)),declarations:r=>r?n().filter(a=>r.includes(a.name)):n(),anthropic:r=>g(r??n()),openai:r=>O(r??n()),markdown:(r,a)=>h(r??n(),a),select:(r="",a)=>k(t,f,c,w,r,u(a,d)),trace:(r="",a)=>b(s.select(r,a),(p,T)=>l.invoke(p,T))};return s}};return l}export{G as toolbus};
@@ -0,0 +1,21 @@
1
+ import type { ToolBus } from "./bus.js";
2
+ import { type Namespacing } from "./namespace.js";
3
+ import type { ToolBusOptions } from "./options.js";
4
+ export interface BusMember {
5
+ name: string;
6
+ description: string;
7
+ bus: ToolBus;
8
+ }
9
+ export interface BusGroupOptions extends ToolBusOptions {
10
+ /**
11
+ * Name every tool after the bus that runs it — `billing__search`.
12
+ *
13
+ * Without it two buses offering the same tool name is refused, on the
14
+ * grounds that a name has to say which one runs. That is the right default
15
+ * for tools you wrote and the wrong one for servers you did not: `search` is
16
+ * the commonest tool name there is, and a group that will not start is worse
17
+ * than one that disambiguates. Pass a function to name them yourself.
18
+ */
19
+ namespace?: Namespacing;
20
+ }
21
+ export declare function busgroup(members: BusMember[], opts?: BusGroupOptions): ToolBus;
@@ -0,0 +1 @@
1
+ import{toAnthropic as T,toMarkdown as b,toOpenAI as E}from"../wire/index.js";import{naming as N}from"./namespace.js";import{scorerFor as I}from"./score.js";import{toStep as S}from"./step.js";import{ceilingFor as $,termsIn as A}from"./terms.js";import{newTrace as j}from"./trace.js";const C=6,x=3;function L(r){return{name:r.name,description:r.description,args:[]}}function G(r,f){const a=new Map;for(const o of r)for(const c of o.bus.all()){const l=f.offered(o,c.name),w=a.get(l);if(w)throw new Error(`Two buses offer a tool called "${l}": "${w.name}" and "${o.name}". A name has to say which one runs \u2014 set \`namespace\` to name them after their bus.`);a.set(l,o)}return a}function U(r,f){return r.flatMap(a=>{const o=a.bus.all().filter(c=>c.pinned).map(c=>c.name);return o.length>0?v(a,f,a.bus.declarations(o)):[]})}const v=(r,f,a)=>a.map(o=>({...o,name:f.offered(r,o.name)}));function _(r){const f=[],a=new Set;let o=r;return{chosen:f,holds:c=>a.has(c),full:()=>f.length>=o,room:()=>r-f.length,narrowTo:c=>{o=Math.min(o,c)},take(c){a.has(c.name)||f.length>=o||(a.add(c.name),f.push(c))}}}function K(r,f,a){const o=r.flat(),c=I(o),l=o.map(d=>({declaration:d,n:c(d,f)})).sort((d,O)=>O.n-d.n);if(l.length===0||l[0].n===0)return!1;l.length>1&&l[0].n>=l[1].n*2&&a.narrowTo(a.chosen.length+x);for(const{declaration:d}of l){if(a.full())break;a.take(d)}return!0}function P(r,f){for(let a=0;!f.full();a++){let o=!1;for(const c of r){const l=c[a];if(l!==void 0&&(o=!0,f.take(l),f.full()))break}if(!o)break}}function W(r,f={}){const a=new Set;for(const n of r){if(a.has(n.name))throw new Error(`Two buses are called "${n.name}".`);a.add(n.name)}const o=N(r,f.namespace),c=G(r,o),l=U(r,o),w=new Map(r.map(n=>[n,L(n)])),d=I([...w.values()]),O=$(f.limit,C),m=n=>c.get(n),g=(n,t)=>o.owned(n,t),y=()=>r.flatMap(n=>n.bus.all().map(t=>({...t,name:o.offered(n,t.name)}))),k=()=>r.flatMap(n=>v(n,o,n.bus.declarations())),B=(n,t)=>Object.fromEntries(Object.entries(t).map(([e,u])=>[o.offered(n,e),u])),D=n=>r.map(t=>({member:t,n:d(w.get(t),n)})).sort((t,e)=>e.n-t.n),h={all:y,missing:n=>n.filter(t=>!c.has(t)),get:n=>{const t=m(n),e=t?.bus.get(g(t,n));return e&&t?{...e,name:n}:e},effectOf:n=>{const t=m(n);return t?.bus.effectOf(g(t,n))},trustOf:n=>{const t=m(n);return t?.bus.trustOf(g(t,n))},destructive:()=>r.flatMap(n=>n.bus.destructive().map(t=>o.offered(n,t))),unannotated:()=>r.flatMap(n=>n.bus.unannotated().map(t=>o.offered(n,t))),declarations:n=>n?r.flatMap(t=>{const e=n.filter(s=>c.get(s)===t);if(e.length===0)return[];const u=e.map(s=>g(t,s));return v(t,o,t.bus.declarations(u))}):k(),anthropic:n=>T(n??k()),openai:n=>E(n??k()),markdown:(n,t)=>b(n??k(),t),select:(n="",t)=>{const e=_($(t,O));for(const p of l)e.take(p);const u=e.room();if(u<=0)return e.chosen;const s=D(A(n)),i=s.map(({member:p})=>v(p,o,p.bus.select(n,u)).filter(F=>!e.holds(F.name)));if(s.length>1&&s[0].n>0&&s[0].n>=s[1].n*2&&i[0].length>0){e.narrowTo(e.chosen.length+x);for(const p of i[0])e.take(p);return e.chosen}return K(i,A(n),e)||P(i,e),e.chosen},invoke:async(n,t)=>{const e=m(n);return e?{...await e.bus.invoke(g(e,n),t),name:n}:{name:n,args:t,status:"failed",output:"",error:`No tool called "${n}". Offered: ${[...c.keys()].join(", ")}`,latencyMs:0}},usage:()=>Object.assign({},...r.map(n=>B(n,n.bus.usage()))),step:(n,t)=>S(n,t),trace:(n="",t)=>j(h.select(n,t),(e,u)=>h.invoke(e,u)),only:n=>{const t=n.filter(s=>c.has(s)),e=()=>h.declarations(t),u={...h,all:()=>y().filter(s=>t.includes(s.name)),missing:s=>s.filter(i=>!t.includes(i)),declarations:s=>s?e().filter(i=>s.includes(i.name)):e(),anthropic:s=>T(s??e()),openai:s=>E(s??e()),markdown:(s,i)=>b(s??e(),i),select:(s="",i)=>h.select(s,i).filter(M=>t.includes(M.name)),trace:(s="",i)=>j(u.select(s,i),(M,p)=>h.invoke(M,p))};return u}};return h}export{W as busgroup};
@@ -1,3 +1,7 @@
1
1
  export { type ToolBus, toolbus } from "./bus.js";
2
+ export { type BusGroupOptions, type BusMember, busgroup } from "./group.js";
3
+ export type { Namespacing } from "./namespace.js";
4
+ export type { Offering } from "./only.js";
2
5
  export type { ToolBusOptions } from "./options.js";
6
+ export { priced, spentOn } from "./spent.js";
3
7
  export { newTrace, type ToolTrace } from "./trace.js";
package/dist/bus/index.js CHANGED
@@ -1,2 +1 @@
1
- export { toolbus } from "./bus.js";
2
- export { newTrace } from "./trace.js";
1
+ import{toolbus as e}from"./bus.js";import{busgroup as t}from"./group.js";import{priced as m,spentOn as x}from"./spent.js";import{newTrace as s}from"./trace.js";export{t as busgroup,s as newTrace,m as priced,x as spentOn,e as toolbus};
@@ -6,4 +6,4 @@ import type { Tool, ToolResult } from "../types/index.js";
6
6
  * step worth recording, not an exception that ends the run — the same argument
7
7
  * `status` and `error` exist on a rollout for.
8
8
  */
9
- export declare function invokeTool(byName: Map<string, Tool<never>>, calls: Map<string, number>, now: () => number, name: string, args: Record<string, unknown>): Promise<ToolResult>;
9
+ export declare function invokeTool(byName: Map<string, Tool<never>>, calls: Map<string, number>, now: () => number, name: string, args: Record<string, unknown>, maxOutputChars?: number): Promise<ToolResult>;
@@ -1,43 +1,3 @@
1
- import { FieldValidationError, validateFields } from "@spendgraph/sdk";
2
- export async function invokeTool(byName, calls, now, name, args) {
3
- const started = now();
4
- const found = byName.get(name);
5
- const base = { name, args, latencyMs: 0 };
6
- if (!found) {
7
- return {
8
- ...base,
9
- status: "failed",
10
- output: "",
11
- error: `No tool called "${name}". Offered: ${[...byName.keys()].join(", ")}`,
12
- };
13
- }
14
- const errors = validateFields(args, found.args);
15
- if (errors.length > 0) {
16
- return {
17
- ...base,
18
- status: "failed",
19
- output: "",
20
- error: new FieldValidationError(errors).message,
21
- latencyMs: now() - started,
22
- };
23
- }
24
- calls.set(name, (calls.get(name) ?? 0) + 1);
25
- try {
26
- const value = await found.run(args);
27
- return {
28
- ...base,
29
- status: "completed",
30
- output: typeof value === "string" ? value : JSON.stringify(value),
31
- latencyMs: now() - started,
32
- };
33
- }
34
- catch (err) {
35
- return {
36
- ...base,
37
- status: "failed",
38
- output: "",
39
- error: err instanceof Error ? err.message : "Tool failed.",
40
- latencyMs: now() - started,
41
- };
42
- }
43
- }
1
+ import{FieldValidationError as d,validateFields as y}from"@spendgraph/sdk";import{readyArgs as g}from"./args.js";import{reasonFor as h,render as p}from"./output.js";function M(r,e){if(e===void 0||r.length<=e)return{output:r};const t=Math.max(0,e);return{output:`${r.slice(0,t)}
2
+
3
+ [cut: showing the first ${t} of ${r.length} characters]`,truncated:{kept:t,of:r.length}}}function $(r){const e=r?.results;if(!Array.isArray(e))return{};const t=e.filter(s=>!!s&&typeof s.url=="string"&&s.url!=="");return t.length?{sources:t}:{}}function k(r,e){if(!r.costOf)return{};try{const t=r.costOf(e);return typeof t=="number"&&Number.isFinite(t)&&t>=0?{costMicros:t}:{}}catch{return{}}}async function A(r,e,t,s,u,f){const i=t(),n=r.get(s),c={name:s,args:u,latencyMs:0,...n?.trust?{trust:n.trust}:{}};if(!n)return{...c,status:"failed",output:"",error:`No tool called "${s}". Offered: ${[...r.keys()].join(", ")}`};const a=y(u,n.args);if(a.length>0)return{...c,status:"failed",output:"",error:new d(a).message,latencyMs:t()-i};e.set(s,(e.get(s)??0)+1);const l=g(n.args,u);try{const o=await n.run(l);return{...c,status:"completed",...M(p(o),f),latencyMs:t()-i,...k(n,o),...$(o)}}catch(o){return{...c,status:"failed",output:"",error:h(o),latencyMs:t()-i}}}export{A as invokeTool};
@@ -0,0 +1,21 @@
1
+ import type { BusMember } from "./group.js";
2
+ /** How a grouped tool is named to the model. */
3
+ export type Namespacing = boolean | ((member: string, tool: string) => string);
4
+ export interface Naming {
5
+ /** What the model is offered, given a tool's own name on its bus. */
6
+ offered(member: BusMember, tool: string): string;
7
+ /** Back to the name the owning bus knows, given a name the model used. */
8
+ owned(member: BusMember, offered: string): string;
9
+ }
10
+ /**
11
+ * Names every tool after the bus that runs it, or leaves them all alone.
12
+ *
13
+ * Off by default, because a name that says nothing about who runs it is a name
14
+ * worth fixing at the source. It is not always yours to fix: a group standing
15
+ * in front of several servers you do not own will meet two of them offering
16
+ * `search`, and refusing to start is not better than disambiguating.
17
+ *
18
+ * Built eagerly from what each bus holds, so the reverse lookup is complete
19
+ * before the first call rather than filled in as names happen to be asked for.
20
+ */
21
+ export declare function naming(members: BusMember[], how: Namespacing | undefined): Naming;
@@ -0,0 +1 @@
1
+ const d="__",p=(o,t)=>`${o}__${t}`;function u(o,t){if(!t)return{offered:(n,e)=>e,owned:(n,e)=>e};const r=typeof t=="function"?t:p,m=new Map,s=new Map;for(const n of o){const e=new Map,f=new Map;for(const a of n.bus.all()){const c=r(n.name,a.name);e.set(a.name,c),f.set(c,a.name)}m.set(n.name,e),s.set(n.name,f)}return{offered:(n,e)=>m.get(n.name)?.get(e)??e,owned:(n,e)=>s.get(n.name)?.get(e)??e}}export{u as naming};
@@ -0,0 +1,32 @@
1
+ import type { Tool, ToolResult } from "../types/index.js";
2
+ /**
3
+ * The same call again: the tool, and the arguments in a stable order.
4
+ *
5
+ * Sorted, because a model writing `{invoice, amount}` on one turn and
6
+ * `{amount, invoice}` on the next means the same call, and a key that said
7
+ * otherwise would let the second one through — which is the whole failure.
8
+ */
9
+ export declare const callKey: (name: string, args: Record<string, unknown>) => string;
10
+ /**
11
+ * Remembers what each destructive call already did, so a repeat does not do it twice.
12
+ *
13
+ * The agent runtime retries — after a timeout, a crash, a redelivery — and that
14
+ * is correct; the tool layer is where a repeat has to stop being a second
15
+ * refund. `effect` has always said which calls need this and nothing has ever
16
+ * read it: `destructive` is precisely the label for a call that is not safe to
17
+ * repeat, and `idempotent` is the label for one that is.
18
+ *
19
+ * Only completed calls are remembered. A failed one is exactly what a retry is
20
+ * for, and refusing to try again because the first attempt fell over would turn
21
+ * every transient error into a permanent one.
22
+ *
23
+ * A tool that declared no effect is not covered. Unset means nobody has said,
24
+ * which is not the same as safe — the package cannot invent the label, and
25
+ * quietly deduplicating an unlabelled call would be a guess about somebody
26
+ * else's API.
27
+ */
28
+ export declare function onceEach(): {
29
+ /** The earlier result for this exact destructive call, when there is one. */
30
+ earlier(tool: Tool<never> | undefined, name: string, args: Record<string, unknown>): ToolResult | undefined;
31
+ keep(tool: Tool<never> | undefined, result: ToolResult): void;
32
+ };
@@ -0,0 +1 @@
1
+ const r=t=>{if(t===null||typeof t!="object")return JSON.stringify(t)??"";if(Array.isArray(t))return`[${t.map(r).join(",")}]`;const n=t;return`{${Object.keys(n).sort().map(e=>`${JSON.stringify(e)}:${r(n[e])}`).join(",")}}`},c=(t,n)=>`${t}::${r(n)}`;function o(){const t=new Map;return{earlier(n,e,i){if(n?.effect==="destructive")return t.get(c(e,i))},keep(n,e){n?.effect!=="destructive"||e.status!=="completed"||t.set(c(e.name,e.args),e)}}}export{c as callKey,o as onceEach};
@@ -0,0 +1,29 @@
1
+ import type { Effect, Tool, ToolDeclaration, ToolResult } from "../types/index.js";
2
+ import type { AnthropicTool, OpenAITool } from "../wire/index.js";
3
+ import type { ToolTrace } from "./trace.js";
4
+ /**
5
+ * A bus narrowed to some of its tools.
6
+ *
7
+ * The declaring half is the half that changes: a model is shown these, and
8
+ * everything reaches the same `invoke` underneath. Written out rather than
9
+ * derived from `toolbus`, because a type that referred back to its own return
10
+ * would be circular — and because what a view promises is worth saying once.
11
+ */
12
+ export interface Offering {
13
+ /** The tools in this view, in declaration order. */
14
+ all(): Tool<never>[];
15
+ get(name: string): Tool<never> | undefined;
16
+ /** Names this view does not offer, whether or not the bus behind it has them. */
17
+ missing(names: string[]): string[];
18
+ effectOf(name: string): Effect | undefined;
19
+ declarations(names?: string[]): ToolDeclaration[];
20
+ anthropic(decls?: ToolDeclaration[]): AnthropicTool[];
21
+ openai(decls?: ToolDeclaration[]): OpenAITool[];
22
+ markdown(decls?: ToolDeclaration[], heading?: string): string;
23
+ select(query?: string, limit?: number): ToolDeclaration[];
24
+ /** Runs one, through the bus this view came from. */
25
+ invoke(name: string, args: Record<string, unknown>): Promise<ToolResult>;
26
+ /** How often each has been called on the bus behind this view. */
27
+ usage(): Record<string, number>;
28
+ trace(query?: string, limit?: number): ToolTrace;
29
+ }
File without changes
@@ -19,4 +19,35 @@ export interface ToolBusOptions {
19
19
  * often, which is why it is worth saying at construction.
20
20
  */
21
21
  warnOnOverlap?: boolean;
22
+ /**
23
+ * Longest output a tool may return, in characters. No cap by default.
24
+ *
25
+ * One tool handing back a whole document fills a window on its own, and the
26
+ * failure is silent: no error, a clean call, a clean reply, and an answer
27
+ * that is wrong in the same tone as a right one. Nothing downstream can tell
28
+ * a tool that found three matches from one that found three hundred and was
29
+ * cut after three.
30
+ *
31
+ * Cut output says so, twice: a line the model reads at the end of the text,
32
+ * and `truncated` on the result for code that needs to count it. A model told
33
+ * it is looking at part of something asks again; one that is not, answers.
34
+ *
35
+ * No default, because a cap is a statement about your context window and your
36
+ * tools, and a number chosen here would be wrong for both. The builtins that
37
+ * fetch carry their own `maxChars` for the same reason.
38
+ */
39
+ maxOutputChars?: number;
40
+ /**
41
+ * Run each destructive call once, however many times it is asked for.
42
+ *
43
+ * A repeat of a call that already completed gets the first result back
44
+ * without the tool running again, marked `repeated` so an operator can count
45
+ * it. `readonly` and `idempotent` calls run every time, which is what those
46
+ * labels mean; a tool that declared no effect is not covered.
47
+ *
48
+ * Off by default. Deduplication is scoped to one bus, so what counts as "the
49
+ * same call again" is a decision about how long a bus lives — one per run, one
50
+ * per session, one per process — and the package cannot make it for you.
51
+ */
52
+ onceEach?: boolean;
22
53
  }
@@ -1 +0,0 @@
1
- export {};
@@ -0,0 +1,26 @@
1
+ /**
2
+ * What a handler returned, as the string a result carries.
3
+ *
4
+ * A string is itself; anything else is JSON. `graph`'s `stringify` describes
5
+ * this as "the same convention as a tool result" — it was not, and this is the
6
+ * side that had drifted.
7
+ *
8
+ * `JSON.stringify` returns `undefined` for a function or an undefined value, so
9
+ * a completed result carried `output: undefined` where its own type says
10
+ * `string`, and the model was handed nothing while being told it was handed
11
+ * something. And it *throws* on a circular object, a `BigInt`, or anything whose
12
+ * `toJSON` throws — a circular object being any value with a parent backlink.
13
+ *
14
+ * What comes back when a value cannot be written is a note saying so, never
15
+ * `""`: an empty output reads as a tool that produced nothing, and this is a
16
+ * tool that produced something unwritable.
17
+ */
18
+ export declare function render(value: unknown): string;
19
+ /**
20
+ * Why a handler failed, from whatever it threw.
21
+ *
22
+ * Anything can be thrown, and a string is thrown often enough. Reading only
23
+ * `Error.message` turned `throw "upstream refused"` into "Tool failed.", which
24
+ * discards the one thing the tool said about it.
25
+ */
26
+ export declare function reasonFor(err: unknown): string;
@@ -0,0 +1,2 @@
1
+ function r(n){if(n==null)return"";if(typeof n=="string")return n;try{const t=JSON.stringify(n);return t===void 0?`[not recordable: ${typeof n}]`:t}catch(t){return`[not recordable: ${(t instanceof Error?t.message:String(t)).split(`
2
+ `)[0]}]`}}function o(n){if(n instanceof Error&&n.message.trim())return n.message;if(typeof n=="string"&&n.trim())return n;const t=r(n);return t&&t!=="{}"?`Tool failed: ${t}`:"Tool failed."}export{o as reasonFor,r as render};
@@ -1,31 +1 @@
1
- function words(tool) {
2
- return new Set(tool.description
3
- .toLowerCase()
4
- .split(/[^a-z0-9]+/)
5
- .filter((w) => w.length > 3));
6
- }
7
- export function warnOnOverlap(tools) {
8
- const sets = tools.map(words);
9
- const named = [];
10
- let pairs = 0;
11
- for (let i = 0; i < tools.length; i++) {
12
- for (let j = i + 1; j < tools.length; j++) {
13
- const a = sets[i];
14
- const b = sets[j];
15
- if (a.size === 0 || b.size === 0)
16
- continue;
17
- const shared = [...a].filter((w) => b.has(w)).length;
18
- if (shared / Math.min(a.size, b.size) < 0.8)
19
- continue;
20
- pairs += 1;
21
- if (named.length < 3)
22
- named.push(`"${tools[i].name}" and "${tools[j].name}"`);
23
- }
24
- }
25
- if (pairs === 0)
26
- return;
27
- const rest = pairs - named.length;
28
- console.warn(`[toolbus] ${pairs} ${pairs === 1 ? "pair of tools describes" : "pairs of tools describe"} ` +
29
- `themselves almost identically: ${named.join(", ")}${rest > 0 ? `, and ${rest} more` : ""}. ` +
30
- `The model will struggle to choose between them — give them distinct descriptions, or merge them.`);
31
- }
1
+ import{termsIn as h}from"./terms.js";function m(e){return new Set(h(e.description).filter(i=>i.length>3))}function g(e){const i=e.map(m),s=[];let t=0;for(let n=0;n<e.length;n++)for(let r=n+1;r<e.length;r++){const o=i[n],l=i[r];o.size===0||l.size===0||[...o].filter(c=>l.has(c)).length/Math.min(o.size,l.size)<.8||(t+=1,s.length<3&&s.push(`"${e[n].name}" and "${e[r].name}"`))}if(t===0)return;const a=t-s.length;console.warn(`[toolbus] ${t} ${t===1?"pair of tools describes":"pairs of tools describe"} themselves almost identically: ${s.join(", ")}${a>0?`, and ${a} more`:""}. The model will struggle to choose between them \u2014 give them distinct descriptions, or merge them.`)}export{g as warnOnOverlap};
@@ -1,9 +1,11 @@
1
- import type { Tool } from "../types/index.js";
2
- /**
3
- * Overlap between the query and a tool's own words.
4
- *
5
- * The name counts for more than the description: "refund" in a question is a
6
- * stronger signal about the `refund` tool than the same word buried in a
7
- * paragraph about it.
8
- */
9
- export declare function score(tool: Tool<never>, terms: string[]): number;
1
+ export interface Describable {
2
+ name: string;
3
+ description: string;
4
+ args: readonly {
5
+ name: string;
6
+ description?: string;
7
+ }[];
8
+ aliases?: readonly string[];
9
+ }
10
+ export type Scorer = (subject: Describable, terms: string[]) => number;
11
+ export declare function scorerFor(corpus: readonly Describable[]): Scorer;
package/dist/bus/score.js CHANGED
@@ -1,12 +1 @@
1
- export function score(tool, terms) {
2
- const name = tool.name.toLowerCase().replace(/_/g, " ");
3
- const body = `${tool.description} ${tool.args.map((a) => `${a.name} ${a.description ?? ""}`).join(" ")}`.toLowerCase();
4
- let n = 0;
5
- for (const term of terms) {
6
- if (name.includes(term))
7
- n += 3;
8
- else if (body.includes(term))
9
- n += 1;
10
- }
11
- return n;
12
- }
1
+ import{termsIn as l}from"./terms.js";const h=3,m=2,w=1,d=new WeakMap,a=n=>new Set(l(n));function g(n){const t=d.get(n);if(t)return t;const r={name:a(n.name.replace(/_/g," ")),aliases:a((n.aliases??[]).join(" ")),body:a(`${n.description} ${n.args.map(o=>`${o.name} ${o.description??""}`).join(" ")}`)};return d.set(n,r),r}function p(n,t){return n.name.has(t)?h:n.aliases.has(t)?m:n.body.has(t)?w:0}function I(n){const t=new Map;for(const o of n){const e=g(o);for(const s of new Set([...e.name,...e.aliases,...e.body]))t.set(s,(t.get(s)??0)+1)}const r=o=>Math.log((n.length+1)/((t.get(o)??0)+.5));return(o,e)=>{const s=g(o);let i=0;for(const c of e){const f=p(s,c);f>0&&(i+=f*r(c))}return i}}export{I as scorerFor};
@@ -1,18 +1,3 @@
1
1
  import type { Tool, ToolDeclaration } from "../types/index.js";
2
- /**
3
- * The working set for one request: pinned, then what the query matches, then
4
- * what actually gets used.
5
- *
6
- * Relevance before usage, which is the opposite of what this did first. At a
7
- * shortlist of twelve the order was harmless; at six, three previously-used
8
- * tools take half the slots and push out the tool the query needs.
9
- *
10
- * Usage still earns its place as the tiebreaker among equal matches, and as the
11
- * fallback when nothing matches — the case where an agent would otherwise be
12
- * handed nothing.
13
- *
14
- * How many come back depends on how clearly the query matched. A single strong
15
- * match needs no company; a flat spread means the ranking does not know, and a
16
- * wider net is worth the distraction.
17
- */
18
- export declare function selectTools(tools: Tool<never>[], calls: Map<string, number>, declare: (t: Tool<never>) => ToolDeclaration, query: string, ceiling: number): ToolDeclaration[];
2
+ import type { Scorer } from "./score.js";
3
+ export declare function selectTools(tools: Tool<never>[], calls: Map<string, number>, declare: (t: Tool<never>) => ToolDeclaration, scoreOf: Scorer, query: string, ceiling: number): ToolDeclaration[];
@@ -1,44 +1 @@
1
- import { score } from "./score.js";
2
- export function selectTools(tools, calls, declare, query, ceiling) {
3
- const terms = query
4
- .toLowerCase()
5
- .split(/[^a-z0-9]+/)
6
- .filter(Boolean);
7
- const chosen = [];
8
- const take = (t) => {
9
- if (!chosen.includes(t) && chosen.length < ceiling)
10
- chosen.push(t);
11
- };
12
- for (const t of tools)
13
- if (t.pinned)
14
- take(t);
15
- const rest = tools.filter((t) => !t.pinned);
16
- const ranked = rest
17
- .map((t) => ({ t, n: score(t, terms) }))
18
- .filter((x) => x.n > 0)
19
- .sort((a, b) => b.n - a.n || (calls.get(b.t.name) ?? 0) - (calls.get(a.t.name) ?? 0));
20
- const room = ceiling - chosen.length;
21
- const decisive = ranked.length > 1 && ranked[0].n >= ranked[1].n * 2;
22
- const budget = chosen.length + (decisive ? Math.min(3, room) : room);
23
- for (const { t } of ranked) {
24
- if (chosen.length >= budget)
25
- break;
26
- take(t);
27
- }
28
- const byUse = [...rest].sort((a, b) => (calls.get(b.name) ?? 0) - (calls.get(a.name) ?? 0));
29
- for (const t of byUse) {
30
- if (chosen.length >= budget)
31
- break;
32
- if ((calls.get(t.name) ?? 0) === 0)
33
- break;
34
- take(t);
35
- }
36
- if (ranked.length === 0) {
37
- for (const t of rest) {
38
- if (chosen.length >= ceiling)
39
- break;
40
- take(t);
41
- }
42
- }
43
- return chosen.map(declare);
44
- }
1
+ import{termsIn as l}from"./terms.js";function w(t,o,g){if(!t.some(f=>f.needs?.length))return t;const r=[];for(const f of t){if(r.includes(f))continue;const s=(f.needs??[]).map(i=>o.get(i)).filter(i=>i!==void 0&&!r.includes(i));r.length+1+s.length>g||r.push(f,...s)}return r}function M(t,o,g,r,f,s){const i=new Map(t.map(n=>[n.name,n])),p=l(f),e=[],m=n=>{!e.includes(n)&&e.length<s&&e.push(n)};for(const n of t)n.pinned&&m(n);const d=t.filter(n=>!n.pinned),c=d.map(n=>({t:n,n:r(n,p)})).filter(n=>n.n>0).sort((n,a)=>a.n-n.n||(o.get(a.t.name)??0)-(o.get(n.t.name)??0)),u=s-e.length,k=c.length>1&&c[0].n>=c[1].n*2,h=e.length+(k?Math.min(3,u):u);for(const{t:n}of c){if(e.length>=h)break;m(n)}const b=[...d].sort((n,a)=>(o.get(a.name)??0)-(o.get(n.name)??0));for(const n of b){if(e.length>=h||(o.get(n.name)??0)===0)break;m(n)}if(c.length===0)for(const n of d){if(e.length>=s)break;m(n)}return w(e,i,s).map(g)}export{M as selectTools};
@@ -0,0 +1,16 @@
1
+ import type { ToolResult } from "../types/index.js";
2
+ /**
3
+ * What the tools themselves cost, in micro-USD.
4
+ *
5
+ * A run's token counts are the outer model's. A tool that spends on its own —
6
+ * a deep recall, a judge, a sub-agent — is not in them, so a loop that called
7
+ * three of those reports a bill that is missing most of itself. Only results
8
+ * from tools that declared a `costOf` count; the rest are not free, they are
9
+ * unmeasured, and those are different claims.
10
+ */
11
+ export declare function spentOn(results: readonly ToolResult[]): number;
12
+ /** The calls that reported a price, so an unmeasured one is visible as an absence. */
13
+ export declare function priced(results: readonly ToolResult[]): {
14
+ name: string;
15
+ costMicros: number;
16
+ }[];
@@ -0,0 +1 @@
1
+ function c(o){let t=0;for(const r of o)t+=r.costMicros??0;return t}function e(o){return o.filter(t=>t.costMicros!==void 0).map(t=>({name:t.name,costMicros:t.costMicros}))}export{e as priced,c as spentOn};
package/dist/bus/step.js CHANGED
@@ -1,11 +1 @@
1
- export function toStep(result, index) {
2
- return {
3
- index,
4
- source: result.name,
5
- input: result.args,
6
- output: result.output,
7
- status: result.status,
8
- error: result.error,
9
- latencyMs: result.latencyMs,
10
- };
11
- }
1
+ function n(t,o){return{index:o,source:t.name,input:t.args,output:t.output,status:t.status,error:t.error,latencyMs:t.latencyMs}}export{n as toStep};
@@ -0,0 +1,25 @@
1
+ /**
2
+ * A string as the words selection compares.
3
+ *
4
+ * Split on anything that is not a letter or a number **in any script**, rather
5
+ * than on `[^a-z0-9]`. That was the whole of the tokenizer, so a query with no
6
+ * ASCII letters produced no terms at all, nothing scored, and every request
7
+ * fell through to "the first six tools declared" — the same six for "返金して"
8
+ * as for "верните деньги", whatever the bus held.
9
+ *
10
+ * Diacritics are folded so `réfund` and `refund` are one word. Both sides are
11
+ * folded, so it holds for a tool named in a language that uses them as much as
12
+ * for a query typed in one.
13
+ */
14
+ export declare function fold(text: string): string;
15
+ export declare function termsIn(text: string): string[];
16
+ /**
17
+ * How many tools to offer, from what the caller asked for.
18
+ *
19
+ * `NaN` is refused rather than clamped. `Math.max(1, NaN)` is `NaN`, and
20
+ * `chosen.length < NaN` is false for every length, so a `NaN` ceiling offered
21
+ * the model **nothing** — the one outcome this file's own reasoning rules out:
22
+ * "an agent handed no tools cannot act". `Number(process.env.TOOL_LIMIT)` on an
23
+ * unset variable is how it arrives without anyone typing it.
24
+ */
25
+ export declare function ceilingFor(limit: number | undefined, fallback: number): number;
@@ -0,0 +1 @@
1
+ function f(e){return e.toLowerCase().normalize("NFD").replace(/\p{M}+/gu,"")}const r=typeof Intl<"u"&&"Segmenter"in Intl?new Intl.Segmenter(void 0,{granularity:"word"}):void 0;function i(e){const n=f(e);if(!r)return n.split(/[^\p{L}\p{N}]+/u).filter(Boolean);const o=[];for(const t of r.segment(n))t.isWordLike&&o.push(t.segment);return o}function u(e,n){if(e===void 0)return n;if(Number.isNaN(e))throw new Error(`A tool limit of ${e} is not a limit \u2014 nothing would ever be offered. Pass a count, or leave it out for the bus default.`);return Math.max(1,Math.floor(e))}export{u as ceilingFor,f as fold,i as termsIn};
@@ -18,7 +18,12 @@ export interface ToolTrace {
18
18
  * never said — and for one that was never offered.
19
19
  */
20
20
  effectOf(name: string): Effect | undefined;
21
- /** Runs a tool and records the step. Never throws, exactly as `invoke`. */
21
+ /**
22
+ * Runs a tool this turn offered and records the step. Never throws.
23
+ *
24
+ * A name that was not offered comes back as a failed result naming what was,
25
+ * without reaching the bus — see `newTrace`.
26
+ */
22
27
  invoke(name: string, args: Record<string, unknown>): Promise<ToolResult>;
23
28
  /** The steps so far, in call order. */
24
29
  steps(): RolloutStep[];
@@ -38,5 +43,13 @@ export interface ToolTrace {
38
43
  *
39
44
  * Selection happens once, here, and the same list is what gets reported. A trace
40
45
  * cannot disagree with itself about what the model saw.
46
+ *
47
+ * It also cannot run what it did not offer. A shortlist narrows the declarations
48
+ * and used to hand `invoke` the whole bus, so a name the model produced out of
49
+ * nowhere still ran if anything on the bus happened to be called that — and the
50
+ * rollout came back with a step that had no matching entry in `offeredTools`,
51
+ * which reads as impossible. The refusal is a failed result rather than a throw,
52
+ * carrying the names that were offered, because that is an observation the model
53
+ * can correct from on its next turn; an exception is one the run dies of.
41
54
  */
42
55
  export declare function newTrace(offered: ToolDeclaration[], invoke: (name: string, args: Record<string, unknown>) => Promise<ToolResult>): ToolTrace;