@spendgraph/tools 0.6.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 (186) hide show
  1. package/README.md +3 -2
  2. package/dist/agents/index.d.ts +1 -0
  3. package/dist/agents/index.js +1 -0
  4. package/dist/{builtin/write-document/write-document.d.ts → agents/moa/index.d.ts} +7 -2
  5. package/dist/agents/moa/index.js +1 -0
  6. package/dist/{builtin/write-document/commission.d.ts → agents/moa/moa.d.ts} +15 -15
  7. package/dist/agents/moa/moa.js +13 -0
  8. package/dist/{builtin/write-document → agents/moa}/types.d.ts +9 -9
  9. package/dist/builtin/calculate/calculate.d.ts +2 -0
  10. package/dist/builtin/calculate/calculate.js +1 -1
  11. package/dist/builtin/calculate/index.d.ts +2 -1
  12. package/dist/builtin/calculate/index.js +1 -1
  13. package/dist/builtin/code-exec/code-exec.d.ts +14 -0
  14. package/dist/builtin/date/date.d.ts +1 -1
  15. package/dist/builtin/date/date.js +1 -1
  16. package/dist/builtin/date/index.d.ts +1 -0
  17. package/dist/builtin/date/index.js +1 -1
  18. package/dist/builtin/date/shift.d.ts +1 -1
  19. package/dist/builtin/date/shift.js +1 -1
  20. package/dist/builtin/date/words.d.ts +13 -0
  21. package/dist/builtin/date/words.js +1 -0
  22. package/dist/builtin/decline/decline.d.ts +49 -0
  23. package/dist/builtin/decline/decline.js +1 -0
  24. package/dist/builtin/decline/index.d.ts +1 -0
  25. package/dist/builtin/decline/index.js +1 -0
  26. package/dist/builtin/escalate/escalate.d.ts +19 -0
  27. package/dist/builtin/escalate/escalate.js +1 -1
  28. package/dist/builtin/escalate/index.d.ts +1 -1
  29. package/dist/builtin/escalate/index.js +1 -1
  30. package/dist/builtin/extract/extract.d.ts +25 -0
  31. package/dist/builtin/extract/extract.js +1 -0
  32. package/dist/builtin/extract/index.d.ts +3 -0
  33. package/dist/builtin/extract/index.js +1 -0
  34. package/dist/builtin/extract/kinds.d.ts +6 -0
  35. package/dist/builtin/extract/kinds.js +1 -0
  36. package/dist/builtin/extract/readers.d.ts +7 -0
  37. package/dist/builtin/extract/readers.js +9 -0
  38. package/dist/builtin/finish/finish.d.ts +4 -0
  39. package/dist/builtin/finish/finish.js +1 -1
  40. package/dist/builtin/http/allow.d.ts +2 -0
  41. package/dist/builtin/http/allow.js +1 -1
  42. package/dist/builtin/http/hops.d.ts +10 -0
  43. package/dist/builtin/http/hops.js +1 -0
  44. package/dist/builtin/http/http.d.ts +9 -1
  45. package/dist/builtin/http/http.js +1 -1
  46. package/dist/builtin/http/index.d.ts +2 -0
  47. package/dist/builtin/http/index.js +1 -1
  48. package/dist/builtin/http/retry-after.d.ts +14 -0
  49. package/dist/builtin/http/retry-after.js +1 -0
  50. package/dist/builtin/index.d.ts +10 -6
  51. package/dist/builtin/index.js +1 -1
  52. package/dist/builtin/json/index.d.ts +1 -0
  53. package/dist/builtin/json/index.js +1 -1
  54. package/dist/builtin/json/json.js +1 -1
  55. package/dist/builtin/json/parse.d.ts +15 -0
  56. package/dist/builtin/json/parse.js +1 -0
  57. package/dist/builtin/notes/index.d.ts +1 -0
  58. package/dist/builtin/notes/index.js +1 -0
  59. package/dist/builtin/notes/notes.d.ts +45 -0
  60. package/dist/builtin/notes/notes.js +2 -0
  61. package/dist/builtin/openalex/api.d.ts +22 -0
  62. package/dist/builtin/openalex/api.js +1 -0
  63. package/dist/builtin/openalex/index.d.ts +4 -0
  64. package/dist/builtin/openalex/index.js +1 -0
  65. package/dist/builtin/openalex/openalex.d.ts +23 -0
  66. package/dist/builtin/openalex/openalex.js +1 -0
  67. package/dist/builtin/openalex/render.d.ts +2 -0
  68. package/dist/builtin/openalex/render.js +4 -0
  69. package/dist/builtin/polite.d.ts +14 -0
  70. package/dist/builtin/polite.js +1 -0
  71. package/dist/builtin/publish/index.d.ts +1 -0
  72. package/dist/builtin/publish/index.js +1 -0
  73. package/dist/builtin/publish/publish.d.ts +46 -0
  74. package/dist/builtin/publish/publish.js +1 -0
  75. package/dist/builtin/web-search/web-search.d.ts +10 -35
  76. package/dist/builtin/web-search/web-search.js +1 -1
  77. package/dist/builtin/wikidata/api.d.ts +40 -0
  78. package/dist/builtin/wikidata/api.js +1 -0
  79. package/dist/builtin/wikidata/index.d.ts +5 -0
  80. package/dist/builtin/wikidata/index.js +1 -0
  81. package/dist/builtin/wikidata/render.d.ts +13 -0
  82. package/dist/builtin/wikidata/render.js +2 -0
  83. package/dist/builtin/wikidata/wikidata.d.ts +21 -0
  84. package/dist/builtin/wikidata/wikidata.js +1 -0
  85. package/dist/bus/bus.d.ts +23 -1
  86. package/dist/bus/bus.js +1 -1
  87. package/dist/bus/group.d.ts +14 -1
  88. package/dist/bus/group.js +1 -1
  89. package/dist/bus/index.d.ts +4 -1
  90. package/dist/bus/index.js +1 -1
  91. package/dist/bus/invoke.d.ts +1 -1
  92. package/dist/bus/invoke.js +3 -1
  93. package/dist/bus/namespace.d.ts +21 -0
  94. package/dist/bus/namespace.js +1 -0
  95. package/dist/bus/once.d.ts +32 -0
  96. package/dist/bus/once.js +1 -0
  97. package/dist/bus/only.d.ts +29 -0
  98. package/dist/bus/only.js +0 -0
  99. package/dist/bus/options.d.ts +31 -0
  100. package/dist/bus/select.d.ts +0 -16
  101. package/dist/bus/select.js +1 -1
  102. package/dist/bus/spent.d.ts +16 -0
  103. package/dist/bus/spent.js +1 -0
  104. package/dist/bus/trace.d.ts +14 -1
  105. package/dist/bus/trace.js +1 -1
  106. package/dist/index.d.ts +6 -4
  107. package/dist/index.js +1 -1
  108. package/dist/internals.d.ts +8 -1
  109. package/dist/internals.js +1 -1
  110. package/dist/memories/client.d.ts +91 -0
  111. package/dist/memories/client.js +0 -0
  112. package/dist/memories/context.d.ts +5 -0
  113. package/dist/memories/context.js +1 -0
  114. package/dist/{builtin → memories}/deep-recall/deep-recall.d.ts +3 -0
  115. package/dist/memories/deep-recall/deep-recall.js +1 -0
  116. package/dist/memories/explore/explore.d.ts +18 -0
  117. package/dist/memories/explore/explore.js +7 -0
  118. package/dist/memories/explore/subgraph.d.ts +12 -0
  119. package/dist/memories/explore/subgraph.js +3 -0
  120. package/dist/memories/index.d.ts +30 -0
  121. package/dist/memories/index.js +1 -0
  122. package/dist/memories/observe/observe.d.ts +7 -0
  123. package/dist/memories/observe/observe.js +1 -0
  124. package/dist/memories/recall/recall.d.ts +42 -0
  125. package/dist/memories/recall/recall.js +1 -0
  126. package/dist/memories/recall/render.d.ts +5 -0
  127. package/dist/memories/recall/render.js +3 -0
  128. package/dist/memories/remember/remember.d.ts +49 -0
  129. package/dist/memories/remember/remember.js +1 -0
  130. package/dist/search/index.d.ts +5 -0
  131. package/dist/search/index.js +1 -0
  132. package/dist/search/retry.d.ts +6 -0
  133. package/dist/search/retry.js +1 -0
  134. package/dist/search/search.d.ts +63 -0
  135. package/dist/search/search.js +1 -0
  136. package/dist/search/searxng/index.d.ts +2 -0
  137. package/dist/search/searxng/index.js +1 -0
  138. package/dist/search/searxng/searxng.d.ts +14 -0
  139. package/dist/search/searxng/searxng.js +5 -0
  140. package/dist/search/searxng/text.d.ts +2 -0
  141. package/dist/search/searxng/text.js +3 -0
  142. package/dist/search/sonar/index.d.ts +2 -0
  143. package/dist/search/sonar/index.js +1 -0
  144. package/dist/search/sonar/sonar.d.ts +13 -0
  145. package/dist/search/sonar/sonar.js +1 -0
  146. package/dist/search/tavily/index.d.ts +4 -0
  147. package/dist/search/tavily/index.js +1 -0
  148. package/dist/search/tavily/reply.d.ts +22 -0
  149. package/dist/search/tavily/reply.js +5 -0
  150. package/dist/search/tavily/tavily.d.ts +14 -0
  151. package/dist/search/tavily/tavily.js +1 -0
  152. package/dist/testing/index.d.ts +2 -0
  153. package/dist/testing/index.js +1 -0
  154. package/dist/testing/replay.d.ts +55 -0
  155. package/dist/testing/replay.js +1 -0
  156. package/dist/testing/store.d.ts +35 -0
  157. package/dist/testing/store.js +4 -0
  158. package/dist/tool/tool.d.ts +4 -1
  159. package/dist/tool/tool.js +1 -1
  160. package/dist/types/index.d.ts +1 -1
  161. package/dist/types/result.d.ts +38 -0
  162. package/dist/types/tool.d.ts +61 -0
  163. package/docs/bogus.mdx +85 -0
  164. package/docs/builtins.mdx +156 -0
  165. package/docs/bus.mdx +90 -0
  166. package/docs/declaring.mdx +85 -0
  167. package/docs/effects.mdx +49 -0
  168. package/docs/overview.mdx +76 -0
  169. package/docs/selecting.mdx +79 -0
  170. package/docs/stored.mdx +83 -0
  171. package/docs/turns.mdx +57 -0
  172. package/package.json +22 -9
  173. package/dist/builtin/deep-recall/deep-recall.js +0 -1
  174. package/dist/builtin/write-document/commission.js +0 -13
  175. package/dist/builtin/write-document/index.d.ts +0 -6
  176. package/dist/builtin/write-document/index.js +0 -1
  177. package/dist/builtin/write-document/write-document.js +0 -1
  178. /package/dist/{builtin/write-document → agents/moa}/design.d.ts +0 -0
  179. /package/dist/{builtin/write-document → agents/moa}/design.js +0 -0
  180. /package/dist/{builtin/write-document → agents/moa}/http.d.ts +0 -0
  181. /package/dist/{builtin/write-document → agents/moa}/http.js +0 -0
  182. /package/dist/{builtin/write-document → agents/moa}/types.js +0 -0
  183. /package/dist/{builtin → memories}/deep-recall/index.d.ts +0 -0
  184. /package/dist/{builtin → memories}/deep-recall/index.js +0 -0
  185. /package/dist/{builtin/web-search → search/sonar}/reply.d.ts +0 -0
  186. /package/dist/{builtin/web-search → search/sonar}/reply.js +0 -0
@@ -0,0 +1,14 @@
1
+ /**
2
+ * How long a server asked you to wait, in milliseconds, or null.
3
+ *
4
+ * `Retry-After` is the one part of a 429 or a 503 that is not guesswork: the
5
+ * server knows when its window resets and says so. Dropping it leaves a caller
6
+ * backing off by a schedule it invented, which is either too slow or — the
7
+ * expensive one — too fast, and a retry inside the window spends a request to
8
+ * be told to wait again.
9
+ *
10
+ * Two forms, both in the spec: whole seconds, or an HTTP date. A date in the
11
+ * past is zero rather than negative, because "wait -4 seconds" is not something
12
+ * a caller should have to guard against.
13
+ */
14
+ export declare function retryAfterMs(header: string | null, now?: () => number): number | null;
@@ -0,0 +1 @@
1
+ function u(r,n=Date.now){if(!r)return null;const t=r.trim();if(/^\d+$/.test(t))return Number(t)*1e3;const e=Date.parse(t);return Number.isNaN(e)?null:Math.max(0,e-n())}export{u as retryAfterMs};
@@ -1,12 +1,16 @@
1
1
  export { calculate, evaluate } from "./calculate/index.js";
2
2
  export { type ClockOptions, currentTime } from "./clock/index.js";
3
3
  export { type CodeExecOptions, codeExec, type Sandbox, type SandboxRequest, type SandboxResult, } from "./code-exec/index.js";
4
- export { dateMath } from "./date/index.js";
5
- export { type DeepRecallOptions, deepRecall, type RecalledFact, type Recaller, type RecallResult, } from "./deep-recall/index.js";
6
- export { type EscalateOptions, type EscalationRequest, escalate } from "./escalate/index.js";
4
+ export { dateMath, dates } from "./date/index.js";
5
+ export { type Declined, decline, declinedFor, isDeclined, type Unanswerable, } from "./decline/index.js";
6
+ export { type Escalated, type EscalateOptions, type EscalationRequest, escalate, escalationOf, isEscalated, } from "./escalate/index.js";
7
+ export { type ExtractOptions, extract } from "./extract/index.js";
7
8
  export { answerOf, type Finished, finish, isFinished } from "./finish/index.js";
8
- export { checkUrl, type HttpOptions, httpRequest } from "./http/index.js";
9
- export { type JsonQueryOptions, jsonQuery } from "./json/index.js";
9
+ export { checkUrl, type HttpOptions, httpRequest, retryAfterMs } from "./http/index.js";
10
+ export { type JsonQueryOptions, jsonFrom, jsonQuery } from "./json/index.js";
11
+ export { type NotesOptions, type NotesStore, notes, notesStore } from "./notes/index.js";
12
+ export { type OpenAlexApi, type OpenAlexOptions, openalex } from "./openalex/index.js";
13
+ export { type Publisher, type PublishOptions, publish } from "./publish/index.js";
10
14
  export { think } from "./think/index.js";
11
15
  export { type SearchDepth, type WebSearchOptions, webSearch } from "./web-search/index.js";
12
- export { type ArtifactFormat, CommissionAbortedError, type CommissionClient, CommissionTimeoutError, DEFAULT_DESIGN, type DesignGuide, mergeDesign, renderDesign, type WriteDocumentOptions, writeDocument, } from "./write-document/index.js";
16
+ export { type WikidataApi, type WikidataOptions, wikidata } from "./wikidata/index.js";
@@ -1 +1 @@
1
- import{calculate as o,evaluate as t}from"./calculate/index.js";import{currentTime as i}from"./clock/index.js";import{codeExec as p}from"./code-exec/index.js";import{dateMath as x}from"./date/index.js";import{deepRecall as a}from"./deep-recall/index.js";import{escalate as h}from"./escalate/index.js";import{answerOf as u,finish as d,isFinished as D}from"./finish/index.js";import{checkUrl as g,httpRequest as w}from"./http/index.js";import{jsonQuery as b}from"./json/index.js";import{think as A}from"./think/index.js";import{webSearch as F}from"./web-search/index.js";import{CommissionAbortedError as S,CommissionTimeoutError as U,DEFAULT_DESIGN as j,mergeDesign as q,renderDesign as v,writeDocument as y}from"./write-document/index.js";export{S as CommissionAbortedError,U as CommissionTimeoutError,j as DEFAULT_DESIGN,u as answerOf,o as calculate,g as checkUrl,p as codeExec,i as currentTime,x as dateMath,a as deepRecall,h as escalate,t as evaluate,d as finish,w as httpRequest,D as isFinished,b as jsonQuery,q as mergeDesign,v as renderDesign,A as think,F as webSearch,y as writeDocument};
1
+ import{calculate as o,evaluate as t}from"./calculate/index.js";import{currentTime as a}from"./clock/index.js";import{codeExec as x}from"./code-exec/index.js";import{dateMath as s,dates as i}from"./date/index.js";import{decline as n,declinedFor as l,isDeclined as d}from"./decline/index.js";import{escalate as u,escalationOf as k,isEscalated as w}from"./escalate/index.js";import{extract as b}from"./extract/index.js";import{answerOf as y,finish as E,isFinished as M}from"./finish/index.js";import{checkUrl as S,httpRequest as q,retryAfterMs as v}from"./http/index.js";import{jsonFrom as D,jsonQuery as Q}from"./json/index.js";import{notes as T,notesStore as U}from"./notes/index.js";import{openalex as z}from"./openalex/index.js";import{publish as C}from"./publish/index.js";import{think as H}from"./think/index.js";import{webSearch as J}from"./web-search/index.js";import{wikidata as L}from"./wikidata/index.js";export{y as answerOf,o as calculate,S as checkUrl,x as codeExec,a as currentTime,s as dateMath,i as dates,n as decline,l as declinedFor,u as escalate,k as escalationOf,t as evaluate,b as extract,E as finish,q as httpRequest,d as isDeclined,w as isEscalated,M as isFinished,D as jsonFrom,Q as jsonQuery,T as notes,U as notesStore,z as openalex,C as publish,v as retryAfterMs,H as think,J as webSearch,L as wikidata};
@@ -1,2 +1,3 @@
1
1
  export { type JsonQueryOptions, jsonQuery } from "./json.js";
2
+ export { jsonFrom } from "./parse.js";
2
3
  export { readPath } from "./path.js";
@@ -1 +1 @@
1
- import{jsonQuery as e}from"./json.js";import{readPath as a}from"./path.js";export{e as jsonQuery,a as readPath};
1
+ import{jsonQuery as e}from"./json.js";import{jsonFrom as t}from"./parse.js";import{readPath as p}from"./path.js";export{t as jsonFrom,e as jsonQuery,p as readPath};
@@ -1 +1 @@
1
- import{tool as i}from"../../tool/index.js";import{readPath as l}from"./path.js";const d=2e4;function f(a={}){const e=a.maxChars??d;return i({name:"json_query",effect:"readonly",description:"Reads one value out of a JSON document by path. Paths look like `invoices[0].total`, or `invoices[*].total` for every element. Use it instead of reading a long document yourself.",args:[{name:"json",type:"text",required:!0,maxLength:2e5},{name:"path",type:"string",required:!0,maxLength:500}],run:({json:s,path:t})=>{let o;try{o=JSON.parse(s)}catch(u){throw new Error(`json is not valid JSON: ${u.message}`)}const n=l(o,t);if(n===void 0)return{found:!1,path:t,value:null};const r=JSON.stringify(n)??"null";return{found:!0,path:t,value:n,truncated:r.length>e,...r.length>e?{value:`${r.slice(0,e)}\u2026`}:{}}}})}export{f as jsonQuery};
1
+ import{tool as s}from"../../tool/index.js";import{jsonFrom as i}from"./parse.js";import{readPath as l}from"./path.js";const d=2e4;function g(r={}){const e=r.maxChars??d;return s({name:"json_query",effect:"readonly",description:"Reads one value out of a JSON document by path. Paths look like `invoices[0].total`, or `invoices[*].total` for every element. Use it instead of reading a long document yourself.",args:[{name:"json",type:"text",required:!0,maxLength:2e5},{name:"path",type:"string",required:!0,maxLength:500}],run:({json:a,path:n})=>{const u=i(a),t=l(u,n);if(t===void 0)return{found:!1,path:n,value:null};const o=JSON.stringify(t)??"null";return{found:!0,path:n,value:t,truncated:o.length>e,...o.length>e?{value:`${o.slice(0,e)}\u2026`}:{}}}})}export{g as jsonQuery};
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The JSON inside a model's reply, whatever it wrapped it in.
3
+ *
4
+ * The two commonest structured-output failures are not bad JSON at all: a
5
+ * fenced block, and a sentence before or after it. Both parse perfectly once
6
+ * the payload is found, and every caller otherwise writes the same fence
7
+ * stripping by hand and gets the second case wrong.
8
+ *
9
+ * It finds the JSON; it never rewrites it. A trailing comma, a single-quoted
10
+ * key or a missing brace stays an error, because repairing those is guessing at
11
+ * what was meant and a wrong guess is a confidently wrong record. Those belong
12
+ * in a bounded repair round, where the model is told what failed and answers
13
+ * again.
14
+ */
15
+ export declare function jsonFrom(text: string): unknown;
@@ -0,0 +1 @@
1
+ const l=/```(?:[a-zA-Z0-9_-]+)?\s*\n?([\s\S]*?)```/,f={"{":"}","[":"]"};function a(e){const n=e.search(/[[{]/);if(n===-1)return null;const t=f[e[n]];let o=0,i=!1,s=!1;for(let r=n;r<e.length;r+=1){const c=e[r];if(s){s=!1;continue}if(c==="\\"&&i){s=!0;continue}if(c==='"'){i=!i;continue}if(!i){if(c===e[n])o+=1;else if(c===t&&(o-=1,o===0))return e.slice(n,r+1)}}return null}function u(e){const n=e.trim();for(const t of[n,l.exec(n)?.[1]?.trim(),a(n)])if(t)try{return JSON.parse(t)}catch{}throw new Error(`No JSON in ${n.length>80?`${n.slice(0,80)}\u2026`:n||"an empty reply"}`)}export{u as jsonFrom};
@@ -0,0 +1 @@
1
+ export { ACTIONS, type NotesOptions, type NotesStore, notes, notesStore } from "./notes.js";
@@ -0,0 +1 @@
1
+ import{ACTIONS as t,notes as r,notesStore as n}from"./notes.js";export{t as ACTIONS,r as notes,n as notesStore};
@@ -0,0 +1,45 @@
1
+ /** Boards by session, one per process unless you hand every bus the same one. */
2
+ export type NotesStore = Map<string, string[]>;
3
+ export interface NotesOptions {
4
+ /** Whose board: a session id, so a follow-up question sees what the last run worked out. */
5
+ session: string;
6
+ /** Where boards live. Default one store for the process; pass your own to share or to inspect. */
7
+ store?: NotesStore;
8
+ /** Notes a board holds before `post` is refused. Default 50. */
9
+ maxNotes?: number;
10
+ }
11
+ export declare const ACTIONS: readonly ["post", "read", "replace", "clear"];
12
+ export declare function notesStore(): NotesStore;
13
+ /**
14
+ * A board the model writes to and reads back during a session — what it is
15
+ * doing, what is still missing, what it decided — kept in memory and gone when
16
+ * the process is.
17
+ *
18
+ * `think` is write-once and never seen again; this is the model's own chosen
19
+ * state, readable three steps later. It is not memory: a note worth keeping
20
+ * goes to `remember`, on purpose, so half-thoughts never surface as facts.
21
+ *
22
+ * `replace` is what makes it usable as a running plan. Append-only, a plan
23
+ * re-posted each turn becomes twenty notes where the twentieth contradicts the
24
+ * third and `read` shows both — and the only way back is `clear`, which throws
25
+ * the plan away with the noise. One note, rewritten, is the whole pattern.
26
+ *
27
+ * No effect label: `read` changes nothing and `post` changes only this board.
28
+ */
29
+ export declare function notes(opts: NotesOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
30
+ readonly name: "action";
31
+ readonly type: "enum";
32
+ readonly required: true;
33
+ readonly options: readonly ["post", "read", "replace", "clear"];
34
+ }, {
35
+ readonly name: "note";
36
+ readonly type: "text";
37
+ readonly required: false;
38
+ readonly maxLength: 2000;
39
+ }, {
40
+ readonly name: "number";
41
+ readonly type: "number";
42
+ readonly required: false;
43
+ readonly min: 1;
44
+ readonly description: "Which note to replace, as `read` numbers them.";
45
+ }]>>;
@@ -0,0 +1,2 @@
1
+ import{tool as d}from"../../tool/index.js";const l=["post","read","replace","clear"],u=50,c=2e3,w=new Map;function p(){return new Map}function m(e){return e.map((t,n)=>`#${n+1} ${t}`).join(`
2
+ `)}function f(e,t,n){if(t===void 0)throw new Error("replace needs the `number` of the note.");if(e.length===0)throw new Error("There is nothing on the board to replace.");if(t<1||t>e.length)throw new Error(`There is no note #${t}; the board holds #1 to #${e.length}.`);const o=[...e];return o[t-1]=n,o}function y(e){if(!e.session?.trim())throw new Error("notes needs the session whose board this is.");const t=e.store??w,n=e.maxNotes??u,o=()=>t.get(e.session)??[];return d({name:"notes",description:"Your notes for this session. `post` writes one \u2014 what you are doing, what is still missing, what you decided. `read` shows them all, numbered, in order. `replace` rewrites one by its number \u2014 keep a plan as a single note and rewrite it as you go, rather than posting its status again each turn. `clear` wipes them. They last the session and nobody else reads them; use `remember` for anything that should outlast it.",args:[{name:"action",type:"enum",required:!0,options:[...l]},{name:"note",type:"text",required:!1,maxLength:c},{name:"number",type:"number",required:!1,min:1,description:"Which note to replace, as `read` numbers them."}],run:({action:s,note:h,number:a})=>{const r=o();if(s==="read")return r.length?m(r):"Nothing on the board yet.";if(s==="clear")return t.delete(e.session),`Cleared ${r.length} note${r.length===1?"":"s"}.`;const i=h?.trim();if(!i)throw new Error(`${s} needs a \`note\`.`);if(s==="replace")return t.set(e.session,f(r,a,i)),`#${a} rewritten.`;if(r.length>=n)throw new Error(`The board holds ${n} notes; clear it, or say less.`);return t.set(e.session,[...r,i]),`#${r.length+1} noted.`}})}export{l as ACTIONS,y as notes,p as notesStore};
@@ -0,0 +1,22 @@
1
+ import { type PoliteOptions } from "../polite.js";
2
+ export interface Paper {
3
+ title: string;
4
+ year: number | null;
5
+ authors: string[];
6
+ venue: string | null;
7
+ citations: number;
8
+ doi: string | null;
9
+ pdf: string | null;
10
+ abstract: string;
11
+ }
12
+ export interface OpenAlexApi {
13
+ search(query: string, limit: number): Promise<Paper[]>;
14
+ }
15
+ export interface ApiOptions extends Omit<PoliteOptions, "userAgent"> {
16
+ /** Your address, sent as `mailto`; OpenAlex answers named callers from a faster pool. */
17
+ mailto?: string;
18
+ }
19
+ /** OpenAlex stores an abstract as word → positions; this puts the words back in order. */
20
+ export declare function abstractOf(index: Record<string, number[]> | null | undefined): string;
21
+ /** The one OpenAlex call the tool makes, behind an interface a test can fake. */
22
+ export declare function openAlexApi(opts?: ApiOptions): OpenAlexApi;
@@ -0,0 +1 @@
1
+ import{politeGet as n}from"../polite.js";const i="https://api.openalex.org/works",c="spendgraph-tools (https://spendgraph.locusgraph.com)",l=["id","doi","title","publication_year","cited_by_count","authorships","primary_location","open_access","abstract_inverted_index"].join(",");function p(t){if(!t)return"";const e=[];for(const[o,s]of Object.entries(t))for(const r of s)e[r]=o;return e.filter(o=>o!==void 0).join(" ")}function u(t){return{title:t.title?.trim()||"(untitled)",year:t.publication_year??null,authors:(t.authorships??[]).flatMap(e=>e.author?.display_name?[e.author.display_name]:[]),venue:t.primary_location?.source?.display_name??null,citations:t.cited_by_count??0,doi:t.doi?.replace(/^https?:\/\/doi\.org\//,"")??null,pdf:t.open_access?.oa_url??t.primary_location?.pdf_url??null,abstract:p(t.abstract_inverted_index)}}function h(t={}){const e=n({...t,userAgent:c});return{async search(o,s){const r=new URL(i);r.searchParams.set("search",o),r.searchParams.set("per-page",String(s)),r.searchParams.set("select",l),t.mailto&&r.searchParams.set("mailto",t.mailto);const a=await e(r);if(!a.ok)throw new Error(`OpenAlex answered ${a.status}.`);return((await a.json()).results??[]).map(u)}}}export{p as abstractOf,h as openAlexApi};
@@ -0,0 +1,4 @@
1
+ export type { ApiOptions, OpenAlexApi, Paper } from "./api.js";
2
+ export { abstractOf, openAlexApi } from "./api.js";
3
+ export { type OpenAlexOptions, openalex } from "./openalex.js";
4
+ export { rendered } from "./render.js";
@@ -0,0 +1 @@
1
+ import{abstractOf as o,openAlexApi as p}from"./api.js";import{openalex as x}from"./openalex.js";import{rendered as a}from"./render.js";export{o as abstractOf,p as openAlexApi,x as openalex,a as rendered};
@@ -0,0 +1,23 @@
1
+ import { type ApiOptions, type OpenAlexApi } from "./api.js";
2
+ export interface OpenAlexOptions extends ApiOptions {
3
+ /** Papers returned. Default 5. */
4
+ maxResults?: number;
5
+ /** Abstract kept per paper. Default 400 characters. */
6
+ maxAbstractChars?: number;
7
+ /** Injected for tests, in place of the call to OpenAlex. */
8
+ api?: OpenAlexApi;
9
+ }
10
+ /**
11
+ * Papers on a topic from OpenAlex, as text — title, authors, venue, how often
12
+ * cited, a DOI, a PDF when one is open, and the start of the abstract.
13
+ *
14
+ * Free and unauthenticated. For what the literature says; not for anything
15
+ * recent, commercial, or outside a published paper.
16
+ */
17
+ export declare function openalex(opts?: OpenAlexOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
18
+ readonly name: "question";
19
+ readonly type: "string";
20
+ readonly required: true;
21
+ readonly maxLength: 300;
22
+ readonly description: "The topic, as a phrase a paper title or abstract would contain.";
23
+ }]>>;
@@ -0,0 +1 @@
1
+ import{tool as i}from"../../tool/index.js";import{openAlexApi as c}from"./api.js";import{rendered as h}from"./render.js";const p=5,m=400;function l(t={}){const a=t.api??c(t),n=t.maxResults??p,o=t.maxAbstractChars??m;return i({name:"paper_search",effect:"readonly",trust:"external",description:"Finds published papers on a topic and returns them as text: title, authors, venue, citation count, DOI, an open PDF when there is one, and the start of the abstract. Use it for what research has found or measured, and cite the DOI. Not for prices, products, news or anything from the last few months.",args:[{name:"question",type:"string",required:!0,maxLength:300,description:"The topic, as a phrase a paper title or abstract would contain."}],run:async({question:s})=>{const e=s.trim();if(!e)throw new Error("Name the topic to search for.");const r=await a.search(e,n);if(r.length===0)throw new Error(`OpenAlex has no papers matching "${e}".`);return h(e,r,o)}})}export{l as openalex};
@@ -0,0 +1,2 @@
1
+ import type { Paper } from "./api.js";
2
+ export declare function rendered(query: string, papers: readonly Paper[], maxChars: number): string;
@@ -0,0 +1,4 @@
1
+ const h=3;function c(n){const t=n.authors.slice(0,3).map(e=>e.split(" ").at(-1)??e),o=n.authors.length-t.length;return t.length===0?"unknown authors":o>0?`${t.join(", ")} +${o}`:t.join(", ")}function $(n){const t=n.year===null?"":` (${n.year})`,o=n.venue?` \xB7 ${n.venue}`:"";return`${n.title}${t} \u2014 ${c(n)}${o} \xB7 ${n.citations} citations`}function d(n){const o=[n.doi?`doi:${n.doi}`:null,n.pdf?`pdf: ${n.pdf}`:null].filter(e=>e!==null);return o.length>0?o.join(" \xB7 "):null}function f(n,t){const o=n.abstract.trim();return o?o.length>t?`${o.slice(0,t).trimEnd()}\u2026`:o:null}function m(n,t,o){const e=`${t.length} paper${t.length===1?"":"s"} on "${n}", most relevant first`,u=t.map((l,s)=>[`${s+1}. ${$(l)}`,d(l),f(l,o)].filter(i=>i!==null).map((i,r)=>r===0?i:` ${i}`).join(`
2
+ `));return[e,...u].join(`
3
+
4
+ `)}export{m as rendered};
@@ -0,0 +1,14 @@
1
+ export interface PoliteOptions {
2
+ /** Named on every request, as the open APIs ask, so a shared IP is still identifiable. */
3
+ userAgent: string;
4
+ timeoutMs?: number;
5
+ /** Injected for tests. */
6
+ fetch?: typeof fetch;
7
+ /** Injected for tests, in place of sleeping out a Retry-After. */
8
+ wait?: (ms: number) => Promise<void>;
9
+ }
10
+ /**
11
+ * A GET that behaves the way the free, keyless APIs ask: named, timed out,
12
+ * and waiting out a 429 for as long as Retry-After says before asking again.
13
+ */
14
+ export declare function politeGet(opts: PoliteOptions): (url: URL) => Promise<Response>;
@@ -0,0 +1 @@
1
+ const a=429,_=2,c=1e3,R=5e3,u=1e4,o=t=>new Promise(e=>setTimeout(e,t));function E(t){const e=Number(t.headers.get("retry-after")),n=Number.isFinite(e)&&e>0?e*1e3:1e3;return Math.min(n,5e3)}function A(t){const e=t.fetch??globalThis.fetch.bind(globalThis),n=t.wait??o,T=t.timeoutMs??1e4;return async i=>{for(let r=0;;r++){const s=await e(i,{signal:AbortSignal.timeout(T),headers:{"user-agent":t.userAgent,"api-user-agent":t.userAgent}});if(s.status!==429||r>=2)return s;await n(E(s))}}}export{A as politeGet};
@@ -0,0 +1 @@
1
+ export { type Publisher, type PublishOptions, publish } from "./publish.js";
@@ -0,0 +1 @@
1
+ import{publish as r}from"./publish.js";export{r as publish};
@@ -0,0 +1,46 @@
1
+ import type { PublishedFile, PublishInput, PublishTo } from "@spendgraph/sdk";
2
+ /** The part of the spendgraph client this needs: `new Spendgraph({ apiKey }).files` satisfies it. */
3
+ export interface Publisher {
4
+ publish(input: PublishInput): Promise<{
5
+ file: PublishedFile;
6
+ }>;
7
+ }
8
+ export interface PublishOptions {
9
+ /** Reaches the app with the key and the project pin; the tool never holds a credential. */
10
+ files: Publisher;
11
+ /** The project a file is filed under. A key publishes only into its own. */
12
+ projectId: string;
13
+ /** Formats this deployment will produce. Default all four. */
14
+ formats?: readonly PublishTo[];
15
+ /** Longest content accepted. Default 200000 characters. */
16
+ maxChars?: number;
17
+ }
18
+ /**
19
+ * Content out as a file at a public URL — markdown, a page, plain text or a
20
+ * PDF — so a model can hand a person something to open, not a wall of text.
21
+ *
22
+ * The app renders and stores it; this only asks. A new file every call, so it
23
+ * carries no effect label: a repeat is a second file, never a lost one.
24
+ */
25
+ export declare function publish(opts: PublishOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
26
+ readonly name: "content";
27
+ readonly type: "text";
28
+ readonly required: true;
29
+ readonly maxLength: number;
30
+ }, {
31
+ readonly name: "to";
32
+ readonly type: "enum";
33
+ readonly required: true;
34
+ readonly options: readonly ("md" | "pdf" | "html" | "txt")[];
35
+ }, {
36
+ readonly name: "from";
37
+ readonly type: "enum";
38
+ readonly required: false;
39
+ readonly options: readonly ["text", "md", "html"];
40
+ }, {
41
+ readonly name: "name";
42
+ readonly type: "string";
43
+ readonly required: false;
44
+ readonly maxLength: 120;
45
+ readonly description: "The file's name without its extension.";
46
+ }]>>;
@@ -0,0 +1 @@
1
+ import{PUBLISH_FROM as m,PUBLISH_TO as u}from"@spendgraph/sdk";import{tool as h}from"../../tool/index.js";const l=2e5,d="md";function p(e){if(!e.files)throw new Error("publish needs the client's files resource to reach the app.");if(!e.projectId?.trim())throw new Error("publish needs the projectId a file is filed under.");const r=e.formats?.length?e.formats:u,o=e.maxChars??l;return h({name:"publish",description:`Turns content into a file people can open and returns its public URL. Formats: ${r.join(", ")}. Say what the content is written in \u2014 markdown unless told otherwise \u2014 and what to make of it. Use it when the person asked for a document, a report or a download, not for a short answer.`,args:[{name:"content",type:"text",required:!0,maxLength:o},{name:"to",type:"enum",required:!0,options:[...r]},{name:"from",type:"enum",required:!1,options:[...m]},{name:"name",type:"string",required:!1,maxLength:120,description:"The file's name without its extension."}],run:async({content:i,to:s,from:a,name:n})=>{const{file:t}=await e.files.publish({projectId:e.projectId,content:i,from:a??d,to:s,...n?{name:n}:{}});return`Published ${t.name} (${t.bytes} bytes): ${t.url}`}})}export{p as publish};
@@ -1,47 +1,22 @@
1
- /** How much searching one call is worth. */
2
- export type SearchDepth = "quick" | "pro" | "deep";
1
+ import { type SearchDepth, type Searcher } from "../../search/search.js";
2
+ export type { SearchDepth };
3
3
  export interface WebSearchOptions {
4
- /** Perplexity key. Required, and read from here rather than the environment. */
5
- apiKey: string;
4
+ /** Where answers come from: `sonar(...)`, `searxng(...)`, or your own. Required. */
5
+ search: Searcher;
6
6
  /**
7
7
  * Deepest tier the model may reach. Default `"pro"`.
8
8
  *
9
- * `"deep"` is opt-in because `sonar-deep-research` runs for minutes and bills
9
+ * `"deep"` is opt-in because on Sonar it runs for minutes and bills
10
10
  * reasoning and citation tokens on top of the usual two.
11
11
  */
12
12
  maxDepth?: SearchDepth;
13
- /** Hosts results are restricted to. Set here, so the model cannot widen it. */
14
- domains?: string[];
15
- /** Give up after this. Defaults to 60000, or 300000 on a deep search. */
16
- timeoutMs?: number;
17
- /** Longest answer handed back. Default 20000 characters. */
18
- maxChars?: number;
19
- /** Longest source list handed back. Default 20. */
20
- maxResults?: number;
21
- /**
22
- * Tries for a rate limit or a bad minute on Perplexity's side. Default 3.
23
- *
24
- * A wave of sub-questions searches in parallel, so two calls landing together
25
- * is ordinary and a 429 on the second is not a failure worth surfacing — it
26
- * is a wait. `retry-after` is honoured where the server sends one, and the
27
- * wait is spread by up to one backoff on top: a whole wave limited at the
28
- * same instant computes the same delay, and retrying in step rebuilds the
29
- * burst that caused it.
30
- */
31
- attempts?: number;
32
- /** Injected for tests. */
33
- fetch?: typeof fetch;
34
- /** Injected for tests. */
35
- sleep?: (ms: number) => Promise<void>;
36
- /** Injected for tests. Returns 0 to 1, and spreads a retried wave apart. */
37
- random?: () => number;
38
13
  }
39
14
  /**
40
- * The live web, through Perplexity's Sonar models.
15
+ * The live web, through the searcher it is built on.
41
16
  *
42
- * The tier is the model's to choose and yours to cap: `sonar-deep-research` is
43
- * minutes and orders of magnitude, and a model reaching for it unprompted is
44
- * the failure `maxDepth` exists to prevent. Tokens come back rather than a
17
+ * The tier is the model's to choose and yours to cap: on Sonar, deep research
18
+ * is minutes and orders of magnitude, and a model reaching for it unprompted
19
+ * is the failure `maxDepth` exists to prevent. Tokens come back rather than a
45
20
  * price, because what a search costs is the pricing table's business.
46
21
  */
47
22
  export declare function webSearch(opts: WebSearchOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
@@ -55,7 +30,7 @@ export declare function webSearch(opts: WebSearchOptions): import("../../index.j
55
30
  readonly type: "enum";
56
31
  readonly required: false;
57
32
  readonly options: readonly ["quick", "pro", "deep"];
58
- readonly description: "Default quick. Raise it only when a first pass would not settle it.";
33
+ readonly description: string;
59
34
  }, {
60
35
  readonly name: "recency";
61
36
  readonly type: "enum";
@@ -1 +1 @@
1
- import{tool as w}from"../../tool/index.js";import{readReply as y}from"./reply.js";const l=["quick","pro","deep"],_={quick:"sonar",pro:"sonar-pro",deep:"sonar-deep-research"},g="https://api.perplexity.ai/chat/completions",b=6e4,E=3e5,S=2e4,x=20,A=500,M=500,T=8e3;function k(e){const t=e.headers.get("retry-after");if(!t)return null;const n=Number(t);return Number.isFinite(n)&&n>=0?n*1e3:null}const O=e=>e===429||e>=500;function q(e,t){return"Searches the live web and returns an answer with the sources it rests on. Use it for anything that turns on current facts \u2014 prices, releases, who holds a post, what changed. quick is one pass and is right for most lookups; pro searches wider and cites more."+(e==="deep"?" deep is an exhaustive multi-step review: minutes, and many times the cost. Ask for it only when a wrong answer is expensive.":" deep is not enabled here.")+(t?.length?` Results come only from ${t.join(", ")}.`:"")}function F(e){if(!e.apiKey?.trim())throw new Error("webSearch needs a Perplexity apiKey; it will not read one from the process.");const t=e.fetch??globalThis.fetch.bind(globalThis),n=e.maxDepth??"pro",d=Math.max(1,e.attempts??3),u=e.sleep??(o=>new Promise(r=>setTimeout(r,o)));return w({name:"web_search",description:q(n,e.domains),effect:"readonly",args:[{name:"query",type:"string",required:!0,maxLength:1e3,description:"What to find out, as a question. One thing, not several."},{name:"depth",type:"enum",required:!1,options:["quick","pro","deep"],description:"Default quick. Raise it only when a first pass would not settle it."},{name:"recency",type:"enum",required:!1,options:["day","week","month","year"],description:"Only consider pages published within this. Leave unset for anything."}],run:async({query:o,depth:r="quick",recency:c})=>{if(l.indexOf(r)>l.indexOf(n))throw new Error(`web_search was asked for "${r}", above the "${n}" this deployment allows.`);const a=_[r],m=r==="deep"?E:b;for(let i=1;;i++){const s=await t(g,{method:"POST",signal:AbortSignal.timeout(e.timeoutMs??m),headers:{authorization:`Bearer ${e.apiKey}`,"content-type":"application/json"},body:JSON.stringify({model:a,messages:[{role:"user",content:o}],...c?{search_recency_filter:c}:{},...e.domains?.length?{search_domain_filter:e.domains}:{}})});if(s.ok)return y(await s.json(),a,e.maxChars??S,e.maxResults??x);const p=(await s.text()).slice(0,A);if(!O(s.status)||i===d)throw new Error(`Perplexity returned ${s.status} for ${a}: ${p}`);const h=Math.min(T,M*2**(i-1)),f=k(s)??h;await u(f+h*(e.random??Math.random)())}}})}export{F as webSearch};
1
+ import{DEPTHS as t}from"../../search/search.js";import{tool as h}from"../../tool/index.js";const s="Searches the live web and returns what it found with the sources it rests on. Use it for anything that turns on current facts \u2014 laws, regulations, standards, prices, releases, dates, versions, who holds a post, what changed \u2014 and never answer those from memory. Given a link, reads that page.";function u(e){return e?.length?`${s} Results come only from ${e.join(", ")}; anything elsewhere is out of reach.`:s}function c(e){return e==="deep"?"Default quick, one pass, right for most lookups. pro searches wider. deep is an exhaustive review: minutes, and many times the cost; ask for it only when a wrong answer is expensive.":"Default quick, one pass, right for most lookups. Raise to pro when a first pass would not settle it. deep is not enabled here."}function f(e){if(typeof e?.search!="function")throw new Error("webSearch needs a searcher to run on: sonar({ apiKey }) or searxng({ baseUrl }).");const{search:o}=e,r=e.maxDepth??"pro";return h({name:"web_search",description:u(e.search.domains),effect:"readonly",trust:"external",args:[{name:"query",type:"string",required:!0,maxLength:1e3,description:"What to find out, as a question. One thing, not several."},{name:"depth",type:"enum",required:!1,options:["quick","pro","deep"],description:c(r)},{name:"recency",type:"enum",required:!1,options:["day","week","month","year"],description:"Only consider pages published within this. Leave unset for anything."}],run:async({query:a,depth:n="quick",recency:i})=>{if(t.indexOf(n)>t.indexOf(r))throw new Error(`web_search was asked for "${n}", above the "${r}" this deployment allows.`);return o({query:a,depth:n,recency:i})}})}export{f as webSearch};
@@ -0,0 +1,40 @@
1
+ import { type PoliteOptions } from "../polite.js";
2
+ export interface Found {
3
+ id: string;
4
+ label: string;
5
+ description: string;
6
+ }
7
+ export interface Snak {
8
+ snaktype: string;
9
+ datatype?: string;
10
+ datavalue?: {
11
+ type: string;
12
+ value: unknown;
13
+ };
14
+ }
15
+ export interface Statement {
16
+ mainsnak: Snak;
17
+ rank: "preferred" | "normal" | "deprecated";
18
+ references?: {
19
+ snaks?: Record<string, Snak[]>;
20
+ }[];
21
+ }
22
+ export interface Entity {
23
+ id: string;
24
+ label: string;
25
+ description: string;
26
+ claims: Record<string, Statement[]>;
27
+ /** The Wikipedia article in the tool's language, when there is one. */
28
+ wikipedia: string | null;
29
+ }
30
+ export declare const ITEM_URL = "https://www.wikidata.org/wiki/";
31
+ export interface WikidataApi {
32
+ search(name: string): Promise<Found | null>;
33
+ entity(id: string): Promise<Entity | null>;
34
+ labels(ids: readonly string[]): Promise<Map<string, string>>;
35
+ }
36
+ export interface ApiOptions extends Omit<PoliteOptions, "userAgent"> {
37
+ language?: string;
38
+ }
39
+ /** The three Wikidata calls the tool makes, behind one interface a test can fake. */
40
+ export declare function wikidataApi(opts?: ApiOptions): WikidataApi;
@@ -0,0 +1 @@
1
+ import{politeGet as u}from"../polite.js";const d="https://www.wikidata.org/w/api.php",f="spendgraph-tools (https://spendgraph.locusgraph.com)",g=50,y="https://www.wikidata.org/wiki/";function h(i,s){const e=i.sitelinks?.[`${s}wiki`]?.title;return e?`https://${s}.wikipedia.org/wiki/${encodeURIComponent(e.replace(/ /g,"_"))}`:null}function l(i,s){return i?.[s]?.value??""}function k(i,s){const e=[];for(let o=0;o<i.length;o+=s)e.push(i.slice(o,o+s));return e}function m(i={}){const s=u({...i,userAgent:f}),e=i.language??"en",o=async n=>{const a=new URL(d);for(const[r,c]of Object.entries({...n,format:"json"}))a.searchParams.set(r,c);const t=await s(a);if(!t.ok)throw new Error(`Wikidata answered ${t.status} to ${n.action}.`);return await t.json()};return{async search(n){const t=(await o({action:"wbsearchentities",search:n,language:e,limit:"1"})).search?.[0];return t?{id:t.id,label:t.label??t.id,description:t.description??""}:null},async entity(n){const t=(await o({action:"wbgetentities",ids:n,props:"claims|labels|descriptions|sitelinks",languages:e,sitefilter:`${e}wiki`})).entities?.[n];return!t||t.missing!==void 0?null:{id:n,label:l(t.labels,e)||n,description:l(t.descriptions,e),claims:t.claims??{},wikipedia:h(t,e)}},async labels(n){const a=new Map;for(const t of k([...new Set(n)],g)){const r=await o({action:"wbgetentities",ids:t.join("|"),props:"labels",languages:e});for(const[c,w]of Object.entries(r.entities??{})){const p=l(w.labels,e);p&&a.set(c,p)}}return a}}}export{y as ITEM_URL,m as wikidataApi};
@@ -0,0 +1,5 @@
1
+ export type { ApiOptions, Entity, Found, Statement, WikidataApi } from "./api.js";
2
+ export { wikidataApi } from "./api.js";
3
+ export type { Fact } from "./render.js";
4
+ export { factsOf, rendered, statementsOf } from "./render.js";
5
+ export { type WikidataOptions, wikidata } from "./wikidata.js";
@@ -0,0 +1 @@
1
+ import{wikidataApi as r}from"./api.js";import{factsOf as f,rendered as o,statementsOf as i}from"./render.js";import{wikidata as m}from"./wikidata.js";export{f as factsOf,o as rendered,i as statementsOf,m as wikidata,r as wikidataApi};
@@ -0,0 +1,13 @@
1
+ import { type Entity, type Snak, type Statement } from "./api.js";
2
+ export interface Fact {
3
+ property: string;
4
+ values: string[];
5
+ /** Reference URLs the statements cite, where the value can be checked. */
6
+ sources: string[];
7
+ }
8
+ /** Entity ids a snak refers to — its value, or the unit of a quantity — for one labels call. */
9
+ export declare function referenced(snak: Snak): string[];
10
+ /** The statements worth reading, in the order Wikidata lists them, deprecated ones dropped. */
11
+ export declare function statementsOf(entity: Entity): [string, Statement[]][];
12
+ export declare function factsOf(statements: readonly [string, Statement[]][], labels: ReadonlyMap<string, string>): Fact[];
13
+ export declare function rendered(entity: Entity, facts: readonly Fact[]): string;
@@ -0,0 +1,2 @@
1
+ import{ITEM_URL as c}from"./api.js";const l=new Set(["wikibase-item","time","quantity","string","monolingualtext","url","globe-coordinate"]),d=9,p=10,f=11,s="http://www.wikidata.org/entity/",$="P854";function g(t){return(t.references??[]).flatMap(n=>(n.snaks?.[$]??[]).flatMap(r=>{const e=r.datavalue?.value;return typeof e=="string"?[e]:[]}))}function m(t){const n=t.filter(e=>e.rank!=="deprecated"),r=n.filter(e=>e.rank==="preferred");return r.length>0?r:n}function h(t){return t.snaktype==="value"&&l.has(t.datatype??"")&&t.datavalue!==void 0}function w(t){const n=t.time.replace(/^\+/,""),[r]=n.split("T"),[e="",i="",o=""]=(r??"").split("-");return t.precision>=f?`${e}-${i}-${o}`:t.precision===p?`${e}-${i}`:t.precision===d?e:`c. ${e}`}function u(t){const n=t?.id;return typeof n=="string"?n:null}function y(t){const n=t.datavalue?.value,r=u(n),e=n?.unit?.startsWith(s)?n.unit.slice(s.length):null;return[r,e].filter(i=>i!==null)}function v(t,n){const r=t.datavalue?.value;switch(t.datatype){case"wikibase-item":{const e=u(r);return e?n.get(e)??e:null}case"time":return w(r);case"quantity":{const{amount:e,unit:i}=r,o=i.startsWith(s)?i.slice(s.length):null,a=o?n.get(o)??o:"";return`${e.replace(/^\+/,"")}${a?` ${a}`:""}`}case"monolingualtext":return r.text;case"globe-coordinate":{const{latitude:e,longitude:i}=r;return`${e}, ${i}`}default:return typeof r=="string"?r:null}}function x(t){return Object.entries(t.claims).map(([n,r])=>[n,m(r).filter(e=>h(e.mainsnak))]).filter(([,n])=>n.length>0)}function M(t,n){return t.map(([r,e])=>({property:n.get(r)??r,values:e.flatMap(i=>{const o=v(i.mainsnak,n);return o?[o]:[]}),sources:[...new Set(e.flatMap(g))]}))}function k(t){const n=t.sources.length>0?` \u2014 source: ${t.sources.join(", ")}`:"";return`${t.property}: ${t.values.join(", ")}${n}`}function R(t,n){const r=`${t.label} (${t.id})${t.description?` \u2014 ${t.description}`:""}`,e=[`${c}${t.id}`,...t.wikipedia?[t.wikipedia]:[]];return[r,...n.map(k),`source: ${e.join(" \xB7 ")}`].join(`
2
+ `)}export{M as factsOf,y as referenced,R as rendered,x as statementsOf};
@@ -0,0 +1,21 @@
1
+ import { type ApiOptions, type WikidataApi } from "./api.js";
2
+ export interface WikidataOptions extends ApiOptions {
3
+ /** Facts kept per thing. Default 30; a country has hundreds. */
4
+ maxFacts?: number;
5
+ /** Injected for tests, in place of the three calls to Wikidata. */
6
+ api?: WikidataApi;
7
+ }
8
+ /**
9
+ * Facts about a thing from Wikidata, as text — its label, what it is, and
10
+ * the statements made about it, with every id resolved to a name.
11
+ *
12
+ * Free and unauthenticated. Entity, date, number and place claims are what it
13
+ * settles; prices, versions and anything recent are not in it.
14
+ */
15
+ export declare function wikidata(opts?: WikidataOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
16
+ readonly name: "question";
17
+ readonly type: "string";
18
+ readonly required: true;
19
+ readonly maxLength: 200;
20
+ readonly description: "The thing's name, optionally followed by ': ' and what about it.";
21
+ }]>>;
@@ -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};
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 +1 @@
1
- import{toAnthropic as w,toMarkdown as d,toOpenAI as g}from"../wire/index.js";import{invokeTool as O}from"./invoke.js";import{warnOnOverlap as h}from"./overlap.js";import{scorerFor as k}from"./score.js";import{selectTools as b}from"./select.js";import{toStep as T}from"./step.js";import{ceilingFor as l}from"./terms.js";import{newTrace as M}from"./trace.js";function I(c,a={}){const f=a.now??(()=>Date.now()),m=l(a.limit,6),t=new Map;for(const e of c){if(t.has(e.name))throw new Error(`Two tools are called "${e.name}".`);t.set(e.name,e)}const n=new Map;(a.warnOnOverlap??!0)&&h([...t.values()]);const p=k([...t.values()]),s=e=>({name:e.name,description:e.description,args:e.args,...e.aliases?.length?{aliases:e.aliases}:{},...e.effect?{effect:e.effect}:{}}),r=()=>[...t.values()].map(s),i={all:()=>[...t.values()],missing:e=>e.filter(o=>!t.has(o)),get:e=>t.get(e),effectOf:e=>t.get(e)?.effect,destructive:()=>[...t.values()].filter(e=>e.effect==="destructive").map(e=>e.name),unannotated:()=>[...t.values()].filter(e=>e.effect===void 0).map(e=>e.name),declarations:e=>e?e.map(o=>t.get(o)).filter(Boolean).map(o=>s(o)):r(),anthropic:e=>w(e??r()),openai:e=>g(e??r()),markdown:(e,o)=>d(e??r(),o),select:(e="",o)=>b([...t.values()],n,s,p,e,l(o,m)),invoke:(e,o)=>O(t,n,f,e,o),usage:()=>Object.fromEntries(n),step:(e,o)=>T(e,o),trace:(e="",o)=>M(i.select(e,o),(u,v)=>i.invoke(u,v))};return i}export{I as toolbus};
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};
@@ -1,8 +1,21 @@
1
1
  import type { ToolBus } from "./bus.js";
2
+ import { type Namespacing } from "./namespace.js";
2
3
  import type { ToolBusOptions } from "./options.js";
3
4
  export interface BusMember {
4
5
  name: string;
5
6
  description: string;
6
7
  bus: ToolBus;
7
8
  }
8
- export declare function busgroup(members: BusMember[], opts?: ToolBusOptions): ToolBus;
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;