@spendgraph/tools 0.5.0 → 0.6.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 (112) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +25 -258
  3. package/dist/bogus/ask-mum.d.ts +5 -0
  4. package/dist/bogus/ask-mum.js +1 -0
  5. package/dist/bogus/book-taxi.d.ts +9 -0
  6. package/dist/bogus/book-taxi.js +1 -0
  7. package/dist/bogus/bus.d.ts +8 -0
  8. package/dist/bogus/bus.js +1 -0
  9. package/dist/bogus/calculator.d.ts +14 -0
  10. package/dist/bogus/calculator.js +1 -0
  11. package/dist/bogus/dictionary.d.ts +6 -0
  12. package/dist/bogus/dictionary.js +1 -0
  13. package/dist/bogus/feed-cat.d.ts +8 -0
  14. package/dist/bogus/feed-cat.js +1 -0
  15. package/dist/bogus/football-score.d.ts +6 -0
  16. package/dist/bogus/football-score.js +1 -0
  17. package/dist/bogus/gold-price.d.ts +7 -0
  18. package/dist/bogus/gold-price.js +1 -0
  19. package/dist/bogus/index.d.ts +14 -0
  20. package/dist/bogus/index.js +1 -0
  21. package/dist/bogus/my-calendar.d.ts +6 -0
  22. package/dist/bogus/my-calendar.js +1 -0
  23. package/dist/bogus/my-notes.d.ts +6 -0
  24. package/dist/bogus/my-notes.js +1 -0
  25. package/dist/bogus/my-recipes.d.ts +6 -0
  26. package/dist/bogus/my-recipes.js +1 -0
  27. package/dist/bogus/order-pizza.d.ts +12 -0
  28. package/dist/bogus/order-pizza.js +1 -0
  29. package/dist/bogus/prayer-times.d.ts +6 -0
  30. package/dist/bogus/prayer-times.js +1 -0
  31. package/dist/bogus/receipt.d.ts +8 -0
  32. package/dist/bogus/receipt.js +1 -0
  33. package/dist/builtin/calculate/calculate.js +1 -21
  34. package/dist/builtin/calculate/exact.d.ts +16 -0
  35. package/dist/builtin/calculate/exact.js +1 -0
  36. package/dist/builtin/calculate/index.js +1 -2
  37. package/dist/builtin/calculate/parse.js +1 -137
  38. package/dist/builtin/clock/clock.js +1 -61
  39. package/dist/builtin/clock/index.js +1 -1
  40. package/dist/builtin/code-exec/code-exec.d.ts +28 -0
  41. package/dist/builtin/code-exec/code-exec.js +1 -0
  42. package/dist/builtin/code-exec/index.d.ts +1 -0
  43. package/dist/builtin/code-exec/index.js +1 -0
  44. package/dist/builtin/date/date.js +1 -41
  45. package/dist/builtin/date/index.js +1 -2
  46. package/dist/builtin/date/shift.js +1 -47
  47. package/dist/builtin/deep-recall/deep-recall.d.ts +39 -2
  48. package/dist/builtin/deep-recall/deep-recall.js +1 -45
  49. package/dist/builtin/deep-recall/index.d.ts +1 -1
  50. package/dist/builtin/deep-recall/index.js +1 -1
  51. package/dist/builtin/escalate/escalate.js +1 -20
  52. package/dist/builtin/escalate/index.js +1 -1
  53. package/dist/builtin/finish/finish.js +1 -26
  54. package/dist/builtin/finish/index.js +1 -1
  55. package/dist/builtin/http/allow.js +1 -37
  56. package/dist/builtin/http/http.js +1 -41
  57. package/dist/builtin/http/index.js +1 -2
  58. package/dist/builtin/index.d.ts +2 -1
  59. package/dist/builtin/index.js +1 -11
  60. package/dist/builtin/json/index.js +1 -2
  61. package/dist/builtin/json/json.js +1 -37
  62. package/dist/builtin/json/path.js +1 -56
  63. package/dist/builtin/think/index.js +1 -1
  64. package/dist/builtin/think/think.js +1 -12
  65. package/dist/builtin/web-search/index.js +1 -1
  66. package/dist/builtin/web-search/reply.js +1 -27
  67. package/dist/builtin/web-search/web-search.d.ts +6 -1
  68. package/dist/builtin/web-search/web-search.js +1 -103
  69. package/dist/builtin/write-document/commission.js +13 -152
  70. package/dist/builtin/write-document/design.js +5 -86
  71. package/dist/builtin/write-document/http.js +1 -71
  72. package/dist/builtin/write-document/index.js +1 -4
  73. package/dist/builtin/write-document/types.js +0 -1
  74. package/dist/builtin/write-document/write-document.js +1 -142
  75. package/dist/bus/args.d.ts +11 -0
  76. package/dist/bus/args.js +1 -0
  77. package/dist/bus/bus.js +1 -49
  78. package/dist/bus/group.d.ts +8 -0
  79. package/dist/bus/group.js +1 -0
  80. package/dist/bus/index.d.ts +1 -0
  81. package/dist/bus/index.js +1 -2
  82. package/dist/bus/invoke.js +1 -43
  83. package/dist/bus/options.js +0 -1
  84. package/dist/bus/output.d.ts +26 -0
  85. package/dist/bus/output.js +2 -0
  86. package/dist/bus/overlap.js +1 -31
  87. package/dist/bus/score.d.ts +11 -9
  88. package/dist/bus/score.js +1 -12
  89. package/dist/bus/select.d.ts +2 -1
  90. package/dist/bus/select.js +1 -44
  91. package/dist/bus/step.js +1 -11
  92. package/dist/bus/terms.d.ts +25 -0
  93. package/dist/bus/terms.js +1 -0
  94. package/dist/bus/trace.js +1 -22
  95. package/dist/index.d.ts +13 -3
  96. package/dist/index.js +1 -3
  97. package/dist/internals.d.ts +1 -1
  98. package/dist/internals.js +1 -10
  99. package/dist/tool/bind.js +1 -64
  100. package/dist/tool/index.js +1 -2
  101. package/dist/tool/tool.d.ts +1 -0
  102. package/dist/tool/tool.js +1 -51
  103. package/dist/types/index.js +0 -1
  104. package/dist/types/result.js +0 -1
  105. package/dist/types/tool.d.ts +13 -0
  106. package/dist/types/tool.js +0 -1
  107. package/dist/wire/index.js +1 -3
  108. package/dist/wire/json-schema.js +1 -65
  109. package/dist/wire/markdown.d.ts +5 -3
  110. package/dist/wire/markdown.js +9 -29
  111. package/dist/wire/providers.js +1 -18
  112. package/package.json +11 -7
@@ -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 u}from"./terms.js";function v(m,e,h,a,p,f){const d=u(p),n=[],r=t=>{!n.includes(t)&&n.length<f&&n.push(t)};for(const t of m)t.pinned&&r(t);const i=m.filter(t=>!t.pinned),o=i.map(t=>({t,n:a(t,d)})).filter(t=>t.n>0).sort((t,s)=>s.n-t.n||(e.get(s.t.name)??0)-(e.get(t.t.name)??0)),c=f-n.length,k=o.length>1&&o[0].n>=o[1].n*2,g=n.length+(k?Math.min(3,c):c);for(const{t}of o){if(n.length>=g)break;r(t)}const b=[...i].sort((t,s)=>(e.get(s.name)??0)-(e.get(t.name)??0));for(const t of b){if(n.length>=g||(e.get(t.name)??0)===0)break;r(t)}if(o.length===0)for(const t of i){if(n.length>=f)break;r(t)}return n.map(h)}export{v as selectTools};
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};
package/dist/bus/trace.js CHANGED
@@ -1,22 +1 @@
1
- import { toAnthropic, toMarkdown, toOpenAI, } from "../wire/index.js";
2
- import { toStep } from "./step.js";
3
- export function newTrace(offered, invoke) {
4
- const steps = [];
5
- return {
6
- offered,
7
- anthropic: () => toAnthropic(offered),
8
- openai: () => toOpenAI(offered),
9
- markdown: (heading) => toMarkdown(offered, heading),
10
- effectOf: (name) => offered.find((d) => d.name === name)?.effect,
11
- invoke: async (name, args) => {
12
- const result = await invoke(name, args);
13
- steps.push(toStep(result, steps.length));
14
- return result;
15
- },
16
- steps: () => [...steps],
17
- record: () => ({
18
- offeredTools: offered.map((d) => d.name),
19
- steps: [...steps],
20
- }),
21
- };
22
- }
1
+ import{toAnthropic as s,toMarkdown as a,toOpenAI as c}from"../wire/index.js";import{toStep as i}from"./step.js";function h(n,r){const o=[];return{offered:n,anthropic:()=>s(n),openai:()=>c(n),markdown:t=>a(n,t),effectOf:t=>n.find(e=>e.name===t)?.effect,invoke:async(t,e)=>{const p=await r(t,e);return o.push(i(p,o.length)),p},steps:()=>[...o],record:()=>({offeredTools:n.map(t=>t.name),steps:[...o]})}}export{h as newTrace};
package/dist/index.d.ts CHANGED
@@ -1,6 +1,16 @@
1
- export { type ArtifactFormat, answerOf, type ClockOptions, CommissionAbortedError, type CommissionClient, CommissionTimeoutError, calculate, currentTime, DEFAULT_DESIGN, type DeepRecallOptions, type DesignGuide, dateMath, deepRecall, type EscalateOptions, type EscalationRequest, escalate, type Finished, finish, type HttpOptions, httpRequest, isFinished, type JsonQueryOptions, jsonQuery, mergeDesign, renderDesign, type SearchDepth, think, type WebSearchOptions, type WriteDocumentOptions, webSearch, writeDocument, } from "./builtin/index.js";
2
- export type { ToolBus, ToolBusOptions, ToolTrace } from "./bus/index.js";
3
- export { toolbus } from "./bus/index.js";
1
+ /**
2
+ * What `bus.anthropic()`, `bus.openai()` and `turn.record()` hand back.
3
+ *
4
+ * On the main entry because the functions that return them are: naming the
5
+ * result of a method you called from here should not send you to a second
6
+ * subpath, or to a second package. `@spendgraph/tools/wire` still holds the
7
+ * converters themselves, for a caller adapting a declaration by hand.
8
+ */
9
+ export type { RolloutStep } from "@spendgraph/sdk";
10
+ export { type ArtifactFormat, answerOf, type ClockOptions, type CodeExecOptions, CommissionAbortedError, type CommissionClient, CommissionTimeoutError, calculate, codeExec, currentTime, DEFAULT_DESIGN, type DeepRecallOptions, type DesignGuide, dateMath, deepRecall, type EscalateOptions, type EscalationRequest, escalate, type Finished, finish, type HttpOptions, httpRequest, isFinished, type JsonQueryOptions, jsonQuery, mergeDesign, type RecalledFact, type Recaller, type RecallResult, renderDesign, type Sandbox, type SandboxRequest, type SandboxResult, type SearchDepth, think, type WebSearchOptions, type WriteDocumentOptions, webSearch, writeDocument, } from "./builtin/index.js";
11
+ export type { BusMember, ToolBus, ToolBusOptions, ToolTrace } from "./bus/index.js";
12
+ export { busgroup, toolbus } from "./bus/index.js";
4
13
  export type { BoundSpec, ToolSpec } from "./tool/index.js";
5
14
  export { bindTool, ToolDriftError, ToolNotStoredError, tool } from "./tool/index.js";
6
15
  export type { ArgSpec, ArgsOf, ArgValue, Effect, Tool, ToolDeclaration, ToolResult, } from "./types/index.js";
16
+ export type { AnthropicTool, JsonSchema, JsonSchemaProperty, OpenAITool, } from "./wire/index.js";
package/dist/index.js CHANGED
@@ -1,3 +1 @@
1
- export { answerOf, CommissionAbortedError, CommissionTimeoutError, calculate, currentTime, DEFAULT_DESIGN, dateMath, deepRecall, escalate, finish, httpRequest, isFinished, jsonQuery, mergeDesign, renderDesign, think, webSearch, writeDocument, } from "./builtin/index.js";
2
- export { toolbus } from "./bus/index.js";
3
- export { bindTool, ToolDriftError, ToolNotStoredError, tool } from "./tool/index.js";
1
+ import{answerOf as r,CommissionAbortedError as t,CommissionTimeoutError as i,calculate as s,codeExec as n,currentTime as m,DEFAULT_DESIGN as l,dateMath as a,deepRecall as c,escalate as u,finish as d,httpRequest as E,isFinished as f,jsonQuery as h,mergeDesign as p,renderDesign as D,think as T,webSearch as b,writeDocument as g}from"./builtin/index.js";import{busgroup as w,toolbus as S}from"./bus/index.js";import{bindTool as C,ToolDriftError as F,ToolNotStoredError as N,tool as R}from"./tool/index.js";export{t as CommissionAbortedError,i as CommissionTimeoutError,l as DEFAULT_DESIGN,F as ToolDriftError,N as ToolNotStoredError,r as answerOf,C as bindTool,w as busgroup,s as calculate,n as codeExec,m as currentTime,a as dateMath,c as deepRecall,u as escalate,d as finish,E as httpRequest,f as isFinished,h as jsonQuery,p as mergeDesign,D as renderDesign,T as think,R as tool,S as toolbus,b as webSearch,g as writeDocument};
@@ -10,7 +10,7 @@ export { checkUrl } from "./builtin/http/index.js";
10
10
  export { readPath } from "./builtin/json/index.js";
11
11
  export { invokeTool } from "./bus/invoke.js";
12
12
  export { warnOnOverlap } from "./bus/overlap.js";
13
- export { score } from "./bus/score.js";
13
+ export { type Describable, type Scorer, scorerFor } from "./bus/score.js";
14
14
  export { selectTools } from "./bus/select.js";
15
15
  export { toStep } from "./bus/step.js";
16
16
  export { newTrace } from "./bus/trace.js";
package/dist/internals.js CHANGED
@@ -1,10 +1 @@
1
- export { evaluate } from "./builtin/calculate/index.js";
2
- export { between, parseDate, shift } from "./builtin/date/index.js";
3
- export { checkUrl } from "./builtin/http/index.js";
4
- export { readPath } from "./builtin/json/index.js";
5
- export { invokeTool } from "./bus/invoke.js";
6
- export { warnOnOverlap } from "./bus/overlap.js";
7
- export { score } from "./bus/score.js";
8
- export { selectTools } from "./bus/select.js";
9
- export { toStep } from "./bus/step.js";
10
- export { newTrace } from "./bus/trace.js";
1
+ import{evaluate as e}from"./builtin/calculate/index.js";import{between as p,parseDate as f,shift as m}from"./builtin/date/index.js";import{checkUrl as a}from"./builtin/http/index.js";import{readPath as c}from"./builtin/json/index.js";import{invokeTool as s}from"./bus/invoke.js";import{warnOnOverlap as v}from"./bus/overlap.js";import{scorerFor as T}from"./bus/score.js";import{selectTools as k}from"./bus/select.js";import{toStep as b}from"./bus/step.js";import{newTrace as u}from"./bus/trace.js";export{p as between,a as checkUrl,e as evaluate,s as invokeTool,u as newTrace,f as parseDate,c as readPath,T as scorerFor,k as selectTools,m as shift,b as toStep,v as warnOnOverlap};
package/dist/tool/bind.js CHANGED
@@ -1,64 +1 @@
1
- import { tool } from "./tool.js";
2
- export class ToolDriftError extends Error {
3
- toolName;
4
- differences;
5
- constructor(toolName, differences) {
6
- super(`Tool "${toolName}" is stored differently from the code binding it: ${differences.join("; ")}.`);
7
- this.toolName = toolName;
8
- this.differences = differences;
9
- this.name = "ToolDriftError";
10
- }
11
- }
12
- export class ToolNotStoredError extends Error {
13
- toolName;
14
- constructor(toolName) {
15
- super(`No stored tool named "${toolName}".`);
16
- this.toolName = toolName;
17
- this.name = "ToolNotStoredError";
18
- }
19
- }
20
- function drift(declared, stored) {
21
- const out = [];
22
- const byName = new Map(stored.map((a) => [a.name, a]));
23
- for (const arg of declared) {
24
- const found = byName.get(arg.name);
25
- if (!found) {
26
- out.push(`"${arg.name}" is declared here but not stored`);
27
- continue;
28
- }
29
- byName.delete(arg.name);
30
- if (found.type !== arg.type) {
31
- out.push(`"${arg.name}" is ${arg.type} here and ${found.type} stored`);
32
- }
33
- if (found.required !== arg.required) {
34
- out.push(`"${arg.name}" is ${arg.required ? "required" : "optional"} here and the reverse stored`);
35
- }
36
- }
37
- for (const name of byName.keys()) {
38
- out.push(`"${name}" is stored but not declared here`);
39
- }
40
- return out;
41
- }
42
- export async function bindTool(sg, spec, query = {}) {
43
- let stored;
44
- try {
45
- ({ tool: stored } = await sg.tools.get(spec.name, query));
46
- }
47
- catch (err) {
48
- if (err && typeof err === "object" && "status" in err && err.status === 404) {
49
- throw new ToolNotStoredError(spec.name);
50
- }
51
- throw err;
52
- }
53
- const differences = drift(spec.args, stored.args ?? []);
54
- if (differences.length)
55
- throw new ToolDriftError(spec.name, differences);
56
- return tool({
57
- name: stored.name,
58
- description: stored.description,
59
- args: spec.args,
60
- effect: stored.effect ?? undefined,
61
- pinned: stored.pinned,
62
- run: spec.run,
63
- });
64
- }
1
+ import{tool as i}from"./tool.js";class a extends Error{toolName;differences;constructor(r,t){super(`Tool "${r}" is stored differently from the code binding it: ${t.join("; ")}.`),this.toolName=r,this.differences=t,this.name="ToolDriftError"}}class d extends Error{toolName;constructor(r){super(`No stored tool named "${r}".`),this.toolName=r,this.name="ToolNotStoredError"}}function f(s,r){const t=[],o=new Map(r.map(e=>[e.name,e]));for(const e of s){const n=o.get(e.name);if(!n){t.push(`"${e.name}" is declared here but not stored`);continue}o.delete(e.name),n.type!==e.type&&t.push(`"${e.name}" is ${e.type} here and ${n.type} stored`),n.required!==e.required&&t.push(`"${e.name}" is ${e.required?"required":"optional"} here and the reverse stored`)}for(const e of o.keys())t.push(`"${e}" is stored but not declared here`);return t}async function c(s,r,t={}){let o;try{({tool:o}=await s.tools.get(r.name,t))}catch(n){throw n&&typeof n=="object"&&"status"in n&&n.status===404?new d(r.name):n}const e=f(r.args,o.args??[]);if(e.length)throw new a(r.name,e);return i({name:o.name,description:o.description,args:r.args,effect:o.effect??void 0,pinned:o.pinned,run:r.run})}export{a as ToolDriftError,d as ToolNotStoredError,c as bindTool};
@@ -1,2 +1 @@
1
- export { bindTool, ToolDriftError, ToolNotStoredError } from "./bind.js";
2
- export { tool } from "./tool.js";
1
+ import{bindTool as t,ToolDriftError as l,ToolNotStoredError as e}from"./bind.js";import{tool as T}from"./tool.js";export{l as ToolDriftError,e as ToolNotStoredError,t as bindTool,T as tool};
@@ -3,6 +3,7 @@ export interface ToolSpec<T extends readonly ArgSpec[]> {
3
3
  name: string;
4
4
  description: string;
5
5
  args: T;
6
+ aliases?: readonly string[];
6
7
  pinned?: boolean;
7
8
  effect?: Effect;
8
9
  run(args: ArgsOf<T>): Promise<unknown> | unknown;
package/dist/tool/tool.js CHANGED
@@ -1,51 +1 @@
1
- const VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
2
- const EFFECTS = ["readonly", "idempotent", "destructive"];
3
- function checkEnum(toolName, arg) {
4
- if (!arg.options?.length) {
5
- throw new Error(`Tool "${toolName}" argument "${arg.name}" is an enum with no options, so nothing can satisfy it.`);
6
- }
7
- const choices = new Set();
8
- for (const option of arg.options) {
9
- if (!option.trim()) {
10
- throw new Error(`Tool "${toolName}" argument "${arg.name}" has a blank option, which the model can pick and you cannot act on.`);
11
- }
12
- if (choices.has(option)) {
13
- throw new Error(`Tool "${toolName}" argument "${arg.name}" lists "${option}" twice.`);
14
- }
15
- choices.add(option);
16
- }
17
- }
18
- function checkArgs(toolName, args) {
19
- const seen = new Set();
20
- for (const arg of args) {
21
- if (!VALID_NAME.test(arg.name)) {
22
- throw new Error(`Tool "${toolName}" argument "${arg.name}" is not a usable name.`);
23
- }
24
- if (seen.has(arg.name)) {
25
- throw new Error(`Tool "${toolName}" declares "${arg.name}" twice.`);
26
- }
27
- seen.add(arg.name);
28
- if (arg.type === "enum")
29
- checkEnum(toolName, arg);
30
- }
31
- }
32
- export function tool(spec) {
33
- if (!VALID_NAME.test(spec.name)) {
34
- throw new Error(`Tool name "${spec.name}" must be letters, digits and underscores, starting with a letter.`);
35
- }
36
- if (!spec.description.trim()) {
37
- throw new Error(`Tool "${spec.name}" has no description. It is the only thing the model reads to decide whether to call it.`);
38
- }
39
- checkArgs(spec.name, spec.args);
40
- if (spec.effect !== undefined && !EFFECTS.includes(spec.effect)) {
41
- throw new Error(`Tool "${spec.name}" declares effect "${spec.effect}", which is not one of ${EFFECTS.join(", ")}.`);
42
- }
43
- return {
44
- name: spec.name,
45
- description: spec.description,
46
- args: spec.args.map((a) => ({ ...a, options: a.options ? [...a.options] : undefined })),
47
- pinned: spec.pinned,
48
- ...(spec.effect ? { effect: spec.effect } : {}),
49
- run: spec.run,
50
- };
51
- }
1
+ const r=/^[A-Za-z_][A-Za-z0-9_]*$/,i=["readonly","idempotent","destructive"];function a(n,e){if(!e.options?.length)throw new Error(`Tool "${n}" argument "${e.name}" is an enum with no options, so nothing can satisfy it.`);const t=new Set;for(const o of e.options){if(!o.trim())throw new Error(`Tool "${n}" argument "${e.name}" has a blank option, which the model can pick and you cannot act on.`);if(t.has(o))throw new Error(`Tool "${n}" argument "${e.name}" lists "${o}" twice.`);t.add(o)}}function s(n,e){if(e){for(const t of e)if(!t.trim())throw new Error(`Tool "${n}" has a blank alias, which matches every query or none.`)}}function f(n,e){const t=new Set;for(const o of e){if(!r.test(o.name))throw new Error(`Tool "${n}" argument "${o.name}" is not a usable name.`);if(t.has(o.name))throw new Error(`Tool "${n}" declares "${o.name}" twice.`);t.add(o.name),o.type==="enum"&&a(n,o)}}function h(n){if(!r.test(n.name))throw new Error(`Tool name "${n.name}" must be letters, digits and underscores, starting with a letter.`);if(!n.description.trim())throw new Error(`Tool "${n.name}" has no description. It is the only thing the model reads to decide whether to call it.`);if(f(n.name,n.args),s(n.name,n.aliases),n.effect!==void 0&&!i.includes(n.effect))throw new Error(`Tool "${n.name}" declares effect "${n.effect}", which is not one of ${i.join(", ")}.`);return{name:n.name,description:n.description,args:n.args.map(e=>({...e,options:e.options?[...e.options]:void 0})),...n.aliases?.length?{aliases:[...n.aliases]}:{},pinned:n.pinned,...n.effect?{effect:n.effect}:{},run:n.run}}export{h as tool};
@@ -1 +0,0 @@
1
- export {};
@@ -1 +0,0 @@
1
- export {};
@@ -82,6 +82,14 @@ export interface Tool<Args = Record<string, unknown>> {
82
82
  */
83
83
  description: string;
84
84
  args: FieldSpec[];
85
+ /**
86
+ * Extra wording selection matches on, never sent to the model.
87
+ *
88
+ * Lexical ranking cannot reach a synonym: a question about money back scores
89
+ * zero against a tool called `refund`, whatever the description says. This is
90
+ * where the words a caller actually types are declared.
91
+ */
92
+ aliases?: string[];
85
93
  /**
86
94
  * Offered whatever selection decides — an escalation path, a refusal, where
87
95
  * losing a similarity search is not an acceptable outcome.
@@ -96,6 +104,11 @@ export interface ToolDeclaration {
96
104
  name: string;
97
105
  description: string;
98
106
  args: FieldSpec[];
107
+ /**
108
+ * Carried so a nested bus can rank on it. Dropped by every provider shape,
109
+ * exactly as `effect` is.
110
+ */
111
+ aliases?: string[];
99
112
  /**
100
113
  * Carried so a caller can gate on it. Dropped by every provider shape — no
101
114
  * API has a field for it, and an unknown key is rejected or ignored.
@@ -1 +0,0 @@
1
- export {};
@@ -1,3 +1 @@
1
- export { toJsonSchema, } from "./json-schema.js";
2
- export { toMarkdown } from "./markdown.js";
3
- export { toAnthropic, toOpenAI, } from "./providers.js";
1
+ import{toJsonSchema as t}from"./json-schema.js";import{toMarkdown as p}from"./markdown.js";import{toAnthropic as n,toOpenAI as f}from"./providers.js";export{n as toAnthropic,t as toJsonSchema,p as toMarkdown,f as toOpenAI};
@@ -1,65 +1 @@
1
- export const JSON_TYPE = {
2
- string: "string",
3
- text: "string",
4
- number: "number",
5
- boolean: "boolean",
6
- enum: "string",
7
- list: "array",
8
- json: "object",
9
- };
10
- function coerceDefault(field) {
11
- const raw = field.default;
12
- if (raw === undefined)
13
- return undefined;
14
- switch (field.type) {
15
- case "number": {
16
- const n = Number(raw);
17
- return Number.isFinite(n) ? n : undefined;
18
- }
19
- case "boolean": {
20
- const t = raw.trim().toLowerCase();
21
- if (t === "true" || t === field.trueText?.trim().toLowerCase())
22
- return true;
23
- if (t === "false" || t === field.falseText?.trim().toLowerCase())
24
- return false;
25
- return undefined;
26
- }
27
- case "enum":
28
- return field.options?.includes(raw) ? raw : undefined;
29
- case "list":
30
- case "json":
31
- return undefined;
32
- default:
33
- return raw;
34
- }
35
- }
36
- export function toJsonSchema(args) {
37
- const properties = {};
38
- for (const field of args) {
39
- const property = { type: JSON_TYPE[field.type] };
40
- if (field.description)
41
- property.description = field.description;
42
- if (field.type === "enum" && field.options?.length)
43
- property.enum = [...field.options];
44
- if (field.type === "list")
45
- property.items = { type: "string" };
46
- if (field.type === "number") {
47
- if (field.min !== undefined)
48
- property.minimum = field.min;
49
- if (field.max !== undefined)
50
- property.maximum = field.max;
51
- }
52
- if ((field.type === "string" || field.type === "text") && field.maxLength !== undefined) {
53
- property.maxLength = field.maxLength;
54
- }
55
- const fallback = coerceDefault(field);
56
- if (fallback !== undefined)
57
- property.default = fallback;
58
- properties[field.name] = property;
59
- }
60
- return {
61
- type: "object",
62
- properties,
63
- required: args.filter((a) => a.required).map((a) => a.name),
64
- };
65
- }
1
+ const o={string:"string",text:"string",number:"number",boolean:"boolean",enum:"string",list:"array",json:"object"};function u(t){const n=t.default;if(n!==void 0)switch(t.type){case"number":{const e=Number(n);return Number.isFinite(e)?e:void 0}case"boolean":{const e=n.trim().toLowerCase();return e==="true"||e===t.trueText?.trim().toLowerCase()?!0:e==="false"||e===t.falseText?.trim().toLowerCase()?!1:void 0}case"enum":return t.options?.includes(n)?n:void 0;case"list":case"json":return;default:return n}}function s(t){const n={};for(const e of t){const r={type:o[e.type]};e.description&&(r.description=e.description),e.type==="enum"&&e.options?.length&&(r.enum=[...e.options]),e.type==="list"&&(r.items={type:"string"}),e.type==="number"&&(e.min!==void 0&&(r.minimum=e.min),e.max!==void 0&&(r.maximum=e.max)),(e.type==="string"||e.type==="text")&&e.maxLength!==void 0&&(r.maxLength=e.maxLength);const i=u(e);i!==void 0&&(r.default=i),n[e.name]=r}return{type:"object",properties:n,required:t.filter(e=>e.required).map(e=>e.name)}}export{o as JSON_TYPE,s as toJsonSchema};
@@ -2,8 +2,10 @@ import type { ToolDeclaration } from "../types/index.js";
2
2
  /**
3
3
  * Declarations as markdown, for a model with no tools API.
4
4
  *
5
- * About a quarter cheaper than the JSON, since JSON Schema is mostly
6
- * scaffolding — but you give up structured tool calls and parse intent out of
7
- * prose instead. Only worth it where there is no native `tools` array.
5
+ * A third to two fifths cheaper than the JSON, since JSON Schema is mostly
6
+ * scaffolding — 31% under the Anthropic shape on a bus of nine tools, 38% on a
7
+ * bus of three, and more again against OpenAI's extra envelope. But you give up
8
+ * structured tool calls and parse intent out of prose instead. Only worth it
9
+ * where there is no native `tools` array.
8
10
  */
9
11
  export declare function toMarkdown(decls: ToolDeclaration[], heading?: string): string;
@@ -1,29 +1,9 @@
1
- import { JSON_TYPE } from "./json-schema.js";
2
- function escapeHeadings(text) {
3
- return text.replace(/^(\s*)(#{1,6})(\s|$)/gm, "$1\\$2$3");
4
- }
5
- function argLine(field) {
6
- const type = JSON_TYPE[field.type];
7
- const notes = [];
8
- if (field.description)
9
- notes.push(escapeHeadings(field.description.trim()).replace(/\.$/, ""));
10
- if (field.type === "enum" && field.options?.length) {
11
- notes.push(`one of ${field.options.join(", ")}`);
12
- }
13
- if (field.type === "number" && (field.min !== undefined || field.max !== undefined)) {
14
- notes.push(`between ${field.min ?? "any"} and ${field.max ?? "any"}`);
15
- }
16
- if (field.default !== undefined)
17
- notes.push(`defaults to ${field.default}`);
18
- const head = `- \`${field.name}\` (${type}${field.required ? ", required" : ""})`;
19
- return notes.length ? `${head} ${notes.join(". ")}.` : `${head}`;
20
- }
21
- export function toMarkdown(decls, heading = "## Tools") {
22
- if (decls.length === 0)
23
- return "";
24
- const sections = decls.map((d) => {
25
- const args = d.args.length ? d.args.map(argLine).join("\n") : "_Takes no arguments._";
26
- return `### ${d.name}\n${escapeHeadings(d.description)}\n\n${args}`;
27
- });
28
- return `${heading}\n\n${sections.join("\n\n")}`;
29
- }
1
+ import{JSON_TYPE as a}from"./json-schema.js";function r(n){return n.replace(/^(\s*)(#{1,6})(\s|$)/gm,"$1\\$2$3")}function u(n){const o=a[n.type],t=[];n.description&&t.push(r(n.description.trim()).replace(/\.$/,"")),n.type==="enum"&&n.options?.length&&t.push(`one of ${n.options.join(", ")}`),n.type==="number"&&(n.min!==void 0||n.max!==void 0)&&t.push(`between ${n.min??"any"} and ${n.max??"any"}`),n.default!==void 0&&t.push(`defaults to ${n.default}`);const e=`- \`${n.name}\` (${o}${n.required?", required":""})`;return t.length?`${e} ${t.join(". ")}.`:`${e}`}function $(n,o="## Tools"){if(n.length===0)return"";const t=n.map(e=>{const s=e.args.length?e.args.map(u).join(`
2
+ `):"_Takes no arguments._";return`### ${e.name}
3
+ ${r(e.description)}
4
+
5
+ ${s}`});return`${o}
6
+
7
+ ${t.join(`
8
+
9
+ `)}`}export{$ as toMarkdown};
@@ -1,18 +1 @@
1
- import { toJsonSchema } from "./json-schema.js";
2
- export function toAnthropic(decls) {
3
- return decls.map((d) => ({
4
- name: d.name,
5
- description: d.description,
6
- input_schema: toJsonSchema(d.args),
7
- }));
8
- }
9
- export function toOpenAI(decls) {
10
- return decls.map((d) => ({
11
- type: "function",
12
- function: {
13
- name: d.name,
14
- description: d.description,
15
- parameters: toJsonSchema(d.args),
16
- },
17
- }));
18
- }
1
+ import{toJsonSchema as e}from"./json-schema.js";function r(t){return t.map(n=>({name:n.name,description:n.description,input_schema:e(n.args)}))}function i(t){return t.map(n=>({type:"function",function:{name:n.name,description:n.description,parameters:e(n.args)}}))}export{r as toAnthropic,i as toOpenAI};
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@spendgraph/tools",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
4
4
  "description": "Declare a tool once, offer the right few, and record what was called.",
5
- "license": "MIT",
5
+ "license": "Apache-2.0",
6
6
  "repository": {
7
7
  "type": "git",
8
8
  "url": "git+https://github.com/fnLog0/spendgraph.git",
@@ -28,6 +28,10 @@
28
28
  "types": "./dist/index.d.ts",
29
29
  "import": "./dist/index.js"
30
30
  },
31
+ "./bogus": {
32
+ "types": "./dist/bogus/index.d.ts",
33
+ "import": "./dist/bogus/index.js"
34
+ },
31
35
  "./wire": {
32
36
  "types": "./dist/wire/index.d.ts",
33
37
  "import": "./dist/wire/index.js"
@@ -43,16 +47,16 @@
43
47
  "README.md"
44
48
  ],
45
49
  "scripts": {
46
- "build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
47
- "test": "vitest run",
50
+ "build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
51
+ "test": "npm run build && vitest run",
48
52
  "examples": "npm run build && node examples/02-select/01-shortlist.mjs"
49
53
  },
50
54
  "dependencies": {
51
- "@locusgraph/client": "^0.8.1",
52
- "@spendgraph/sdk": "^0.5.0"
55
+ "@spendgraph/sdk": "^0.6.0"
53
56
  },
54
57
  "devDependencies": {
55
- "@spendgraph/prompt": "^0.5.0",
58
+ "@locusgraph/client": "^0.8.1",
59
+ "@spendgraph/prompt": "^0.6.0",
56
60
  "typescript": "^5"
57
61
  },
58
62
  "engines": {