@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 @@
1
+ import{tool as f}from"../../tool/index.js";import{contextIds as l,fixedRoot as h,rootOf as p}from"../context.js";import{readSubgraph as x}from"../explore/subgraph.js";import{rendered as y}from"./render.js";const g="toon",u=10,w=50,I=3;async function T(e,n){if(!e.supersede||!n)return;const o=n.indexOf(":"),s=x(await e.client.getContextGraph({contextType:n.slice(0,o),contextName:n.slice(o+1),depth:I,...e.graphId?{graphId:e.graphId}:{}})),a=new Map;for(const r of s.edges)r.link_type==="contradicts"&&a.set(r.to,r.from);return r=>r.context_ids.map(t=>a.get(t)).find(t=>t!==void 0)}function k(e){const n=e.rootContextId!==void 0,o=h(e.rootContextId)??"your root";return f({name:"recall",effect:"readonly",description:"Searches your own memory for what was recorded about something and returns the matching notes, one per row with the contexts each is filed under. Cheap, so try it before a web search and before deep_recall. Use deep_recall instead when the answer is spread across several notes or needs a why. Ask one focused thing.",args:[{name:"question",type:"string",required:!0,maxLength:1e3,description:"What you want to know, in a sentence."},{name:"context_ids",type:"list",required:!1,description:n?`Contexts under ${o} to favour, as type:name. Take them from an earlier result or from explore_memory.`:"Only search these contexts, as type:name. Take them from an earlier result or from explore_memory."},{name:"limit",type:"number",required:!1,min:1,max:w,description:`Most notes to return. Default ${e.limit??u}.`}],run:async({question:s,context_ids:a,limit:r})=>{const t=p(e.rootContextId),i=l(a,e.defaultContextType),d=t?[t]:i,c=t&&i?i:void 0,m=(await e.client.retrieveMemories({query:s,limit:r??e.limit??u,format:"json",...e.graphId?{graphId:e.graphId}:{},...d?{contextIds:d}:{},...c?{boostContextIds:c}:{},...e.sources?.length?{sources:e.sources}:{}})).items??[];return m.length===0?"Nothing recorded about that.":y(m,e.format??g,await T(e,t))}})}export{k as recall};
@@ -0,0 +1,5 @@
1
+ import type { RecalledItem } from "../client.js";
2
+ export type RecallFormat = "toon" | "markdown";
3
+ /** Which context superseded this note's, when one did. */
4
+ export type Superseded = (item: RecalledItem) => string | undefined;
5
+ export declare const rendered: (items: RecalledItem[], format: RecallFormat, by?: Superseded) => string;
@@ -0,0 +1,3 @@
1
+ const t=/[,:"\n\\]|^\s|\s$|^$/,d=n=>t.test(n)?`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"').replace(/\n/g,"\\n")}"`:n,c=n=>String(Math.round(n*100)),$=(n,o,e)=>[n.context_ids.join(";"),n.text,n.source??"",c(n.score),...e?[o??""]:[]],a=(n,o)=>{const e=o?"contexts,text,source,relevance,superseded_by":"contexts,text,source,relevance";return[`memories[${n.length}]{${e}}:`,...n.map(s=>` ${$(s,o?.(s),o!==void 0).map(d).join(",")}`)].join(`
2
+ `)},p=(n,o)=>n.map(e=>{const s=o?.(e),r=s?` \u2014 superseded by ${s}`:"";return`- [${e.context_ids.join(", ")}] ${e.text} (${e.source??"unknown"}, ${c(e.score)}%)${r}`}).join(`
3
+ `),u=(n,o,e)=>o==="toon"?a(n,e):p(n,e);export{u as rendered};
@@ -0,0 +1,49 @@
1
+ import type { MemoryGraph, MemorySource } from "../client.js";
2
+ export interface RememberOptions extends MemoryGraph {
3
+ source?: MemorySource;
4
+ defaultContextType?: string;
5
+ }
6
+ export declare function remember(opts: RememberOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
7
+ readonly name: "context";
8
+ readonly type: "string";
9
+ readonly required: true;
10
+ readonly maxLength: 200;
11
+ readonly description: "Where it is filed, as type:name.";
12
+ }, {
13
+ readonly name: "text";
14
+ readonly type: "string";
15
+ readonly required: true;
16
+ readonly maxLength: 4000;
17
+ readonly description: "The thing to remember, as a complete sentence or two.";
18
+ }, {
19
+ readonly name: "kind";
20
+ readonly type: "enum";
21
+ readonly required: false;
22
+ readonly options: readonly ["fact", "decision", "constraint", "feedback", "observation"];
23
+ readonly description: "What sort of thing it is. Default fact.";
24
+ }, {
25
+ readonly name: "details";
26
+ readonly type: "json";
27
+ readonly required: false;
28
+ readonly description: string;
29
+ }, {
30
+ readonly name: "extends";
31
+ readonly type: "list";
32
+ readonly required: false;
33
+ readonly description: "Contexts this is a more specific detail of, as type:name.";
34
+ }, {
35
+ readonly name: "related_to";
36
+ readonly type: "list";
37
+ readonly required: false;
38
+ readonly description: "Contexts this sits alongside, as type:name.";
39
+ }, {
40
+ readonly name: "reinforces";
41
+ readonly type: "list";
42
+ readonly required: false;
43
+ readonly description: "Contexts this backs up with more evidence, as type:name.";
44
+ }, {
45
+ readonly name: "contradicts";
46
+ readonly type: "list";
47
+ readonly required: false;
48
+ readonly description: "Contexts this supersedes, as type:name.";
49
+ }]>>;
@@ -0,0 +1 @@
1
+ import{tool as h}from"../../tool/index.js";import{contextId as y,contextIds as a,fixedRoot as x,rootOf as w}from"../context.js";const g=["fact","decision","constraint","feedback","observation"],b="assistant",C=e=>typeof e=="object"&&e!==null&&!Array.isArray(e),q=(e,t)=>{if(t==null)return{data:e};if(!C(t))throw new Error("details must be a JSON object of named fields.");return{...t,data:e}},d=(e,t)=>t?.length?{[e]:t}:{};function v(e){const t=e.defaultContextType,u=e.rootContextId===void 0?"":` under ${x(e.rootContextId)??"your root"}`,m=(c,l)=>{const s=w(e.rootContextId),i=a(l,t)??[],r=s?[s,...i]:i;return[...new Set(r)].filter(n=>n!==c)};return h({name:"remember",effect:"idempotent",description:`Records one thing in your own memory${u} so a later run can recall it. File it under a context named type:name, like person:alice or decision:pricing_tier, reusing a name from explore_memory when one fits. One fact per call; the text should stand on its own without the conversation around it.`,args:[{name:"context",type:"string",required:!0,maxLength:200,description:"Where it is filed, as type:name."},{name:"text",type:"string",required:!0,maxLength:4e3,description:"The thing to remember, as a complete sentence or two."},{name:"kind",type:"enum",required:!1,options:g,description:"What sort of thing it is. Default fact."},{name:"details",type:"json",required:!1,description:"Structured fields worth keeping with the note, as a JSON object: amounts, dates, ids, URLs. The text is still what a recall shows; these ride along with it."},{name:"extends",type:"list",required:!1,description:"Contexts this is a more specific detail of, as type:name."},{name:"related_to",type:"list",required:!1,description:"Contexts this sits alongside, as type:name."},{name:"reinforces",type:"list",required:!1,description:"Contexts this backs up with more evidence, as type:name."},{name:"contradicts",type:"list",required:!1,description:"Contexts this supersedes, as type:name."}],run:async({context:c,text:l,kind:s="fact",details:i,...r})=>{const n=y(c,t),f={event_kind:s,context_id:n,source:e.source??b,payload:q(l,i),...d("extends",m(n,r.extends)),...d("related_to",a(r.related_to,t)),...d("reinforces",a(r.reinforces,t)),...d("contradicts",a(r.contradicts,t))},p=await e.client.storeEventsBatch([f],e.graphId),[o]=p.results;if(!o||o.status==="error")throw new Error(o?.error??"The memory was not stored, and the server did not say why.");return o.status==="filtered"?{stored:!1,context:n,reason:o.error??"judged not worth keeping"}:{stored:!0,context:n,relevance:o.relevance}}})}export{v as remember};
@@ -0,0 +1,5 @@
1
+ export type { Recency, SearchAsk, SearchDepth, Searched, Searcher, SearcherOptions, SearchUsage, Source, } from "./search.js";
2
+ export { DEPTHS, NO_TOKENS } from "./search.js";
3
+ export { type SearxngOptions, searxng, textOf } from "./searxng/index.js";
4
+ export { type TavilyOptions, tavily } from "./tavily/index.js";
5
+ export { readReply, type SonarOptions, sonar } from "./sonar/index.js";
@@ -0,0 +1 @@
1
+ import{DEPTHS as e,NO_TOKENS as t}from"./search.js";import{searxng as f,textOf as p}from"./searxng/index.js";import{tavily as m}from"./tavily/index.js";import{readReply as l,sonar as n}from"./sonar/index.js";export{e as DEPTHS,t as NO_TOKENS,l as readReply,f as searxng,n as sonar,m as tavily,p as textOf};
@@ -0,0 +1,6 @@
1
+ import type { SearcherOptions } from "./search.js";
2
+ /**
3
+ * Sends again while the answer is a 429 or a 5xx and tries remain, then hands
4
+ * back whatever came last, ok or not, for the caller to read or refuse.
5
+ */
6
+ export declare function retrying(opts: Pick<SearcherOptions, "attempts" | "sleep" | "random">): (send: () => Promise<Response>) => Promise<Response>;
@@ -0,0 +1 @@
1
+ const M=500,f=8e3;function m(t){const r=t.headers.get("retry-after");if(!r)return null;const e=Number(r);return Number.isFinite(e)&&e>=0?e*1e3:null}const u=t=>t===429||t>=500;function A(t){const r=Math.max(1,t.attempts??3),e=t.sleep??(o=>new Promise(n=>setTimeout(n,o))),c=t.random??Math.random;return async o=>{for(let n=1;;n++){const s=await o();if(s.ok||!u(s.status)||n===r)return s;const a=Math.min(8e3,500*2**(n-1)),i=m(s)??a;await e(i+a*c())}}}export{A as retrying};
@@ -0,0 +1,63 @@
1
+ /** How much searching one call is worth. */
2
+ export type SearchDepth = "quick" | "pro" | "deep";
3
+ export declare const DEPTHS: readonly SearchDepth[];
4
+ export type Recency = "day" | "week" | "month" | "year";
5
+ export interface Source {
6
+ url: string;
7
+ title?: string;
8
+ date?: string;
9
+ }
10
+ export interface SearchUsage {
11
+ inputTokens: number;
12
+ outputTokens: number;
13
+ citationTokens: number;
14
+ reasoningTokens: number;
15
+ searchQueries: number;
16
+ }
17
+ /** What every searcher hands back, whoever did the searching. */
18
+ export interface Searched {
19
+ answer: string;
20
+ results: Source[];
21
+ model: string;
22
+ truncated: boolean;
23
+ usage: SearchUsage;
24
+ }
25
+ export interface SearchAsk {
26
+ query: string;
27
+ depth: SearchDepth;
28
+ recency?: Recency;
29
+ }
30
+ export type Searcher = ((ask: SearchAsk) => Promise<Searched>) & {
31
+ domains?: readonly string[];
32
+ };
33
+ /** What every searcher is configured with; each adds only where its answers come from. */
34
+ export interface SearcherOptions {
35
+ /** Hosts results are restricted to. Set here, so the model cannot widen it. */
36
+ domains?: string[];
37
+ /** Give up on a request after this. */
38
+ timeoutMs?: number;
39
+ /** Longest answer handed back. Default 20000 characters. */
40
+ maxChars?: number;
41
+ /** Longest source list handed back. Default 20. */
42
+ maxResults?: number;
43
+ /**
44
+ * Tries for a rate limit or a bad minute on the other side. Default 3.
45
+ *
46
+ * A wave of sub-questions searches in parallel, so two calls landing together
47
+ * is ordinary and a 429 on the second is not a failure worth surfacing — it
48
+ * is a wait. `retry-after` is honoured where the server sends one, and the
49
+ * wait is spread by up to one backoff on top: a whole wave limited at the
50
+ * same instant computes the same delay, and retrying in step rebuilds the
51
+ * burst that caused it.
52
+ */
53
+ attempts?: number;
54
+ /** Injected for tests. */
55
+ fetch?: typeof fetch;
56
+ /** Injected for tests. */
57
+ sleep?: (ms: number) => Promise<void>;
58
+ /** Injected for tests. Returns 0 to 1, and spreads a retried wave apart. */
59
+ random?: () => number;
60
+ }
61
+ export declare const DEFAULT_MAX_CHARS = 20000;
62
+ export declare const DEFAULT_MAX_RESULTS = 20;
63
+ export declare const NO_TOKENS: SearchUsage;
@@ -0,0 +1 @@
1
+ const o=["quick","pro","deep"],e=2e4,t=20,n={inputTokens:0,outputTokens:0,citationTokens:0,reasoningTokens:0,searchQueries:1};export{e as DEFAULT_MAX_CHARS,t as DEFAULT_MAX_RESULTS,o as DEPTHS,n as NO_TOKENS};
@@ -0,0 +1,2 @@
1
+ export { type SearxngOptions, searxng } from "./searxng.js";
2
+ export { textOf } from "./text.js";
@@ -0,0 +1 @@
1
+ import{searxng as o}from"./searxng.js";import{textOf as x}from"./text.js";export{o as searxng,x as textOf};
@@ -0,0 +1,14 @@
1
+ import { type SearchDepth, type Searcher, type SearcherOptions } from "../search.js";
2
+ export interface SearxngOptions extends SearcherOptions {
3
+ /** The SearXNG instance, such as http://localhost:8080. Required. */
4
+ baseUrl: string;
5
+ /** Pin which engines answer. Default: whatever the instance runs. */
6
+ engines?: string[];
7
+ /** Pin the language. Default: the instance's own. */
8
+ language?: string;
9
+ /** Text kept from each page read. Default 6000 characters. */
10
+ pageChars?: number;
11
+ /** Pages read at each depth. Default 5, 8 and 10. */
12
+ pages?: Partial<Record<SearchDepth, number>>;
13
+ }
14
+ export declare function searxng(opts: SearxngOptions): Searcher;
@@ -0,0 +1,5 @@
1
+ import{checkUrl as S}from"../../builtin/http/allow.js";import{retrying as y}from"../retry.js";import{DEFAULT_MAX_CHARS as N,DEFAULT_MAX_RESULTS as F,NO_TOKENS as x}from"../search.js";import{textOf as A}from"./text.js";const T="searxng",U=["*"],G=2e4,X=6e3,L=4e5,H={quick:5,pro:8,deep:10},I=/https?:\/\/[^\s"'<>)\]]+/,Q=2;function W(t){return!t.url||!t.title?null:{url:t.url,title:t.title,...t.publishedDate?{date:t.publishedDate}:{},snippet:t.content??""}}function q({url:t,title:r,date:i}){return{url:t,title:r,...i?{date:i}:{}}}function K(t,r){return`## ${t.title}
2
+ ${t.url}
3
+ ${r}`}const Y=3,v=t=>t.status>=300&&t.status<400;function R(t,r){if(!r?.length)return!0;const i=new URL(t.url).hostname.toLowerCase();return r.some(u=>{const h=u.toLowerCase().replace(/^\./,"");return i===h||i.endsWith(`.${h}`)})}function z(t){const r=new Set;return t.flatMap(i=>i.hits).filter(i=>!r.has(i.url)&&r.add(i.url))}function tt(t){if(!t.baseUrl?.trim())throw new Error("searxng needs the baseUrl of a SearXNG instance, such as http://localhost:8080.");const r=t.fetch??globalThis.fetch.bind(globalThis),i=t.baseUrl.replace(/\/+$/,""),u=t.timeoutMs??G,h=Math.max(1,t.maxResults??F),g=t.maxChars??N,f=t.pageChars??X,b={...H,...t.pages},O=y(t);async function d(n,s){const e=new URL(`${i}/search`);e.searchParams.set("q",n),e.searchParams.set("format","json"),s&&e.searchParams.set("time_range",s),t.language&&e.searchParams.set("language",t.language),t.engines?.length&&e.searchParams.set("engines",t.engines.join(","));const a=await O(()=>r(e,{signal:AbortSignal.timeout(u),headers:{accept:"application/json"}}));if(!a.ok){const o=a.status===403?" SearXNG refuses the json format unless settings.yml lists it under search.formats and the limiter is off.":"";throw new Error(`SearXNG returned ${a.status}.${o}`)}const c=await a.json();return{hits:(c.results??[]).flatMap(o=>{const l=W(o);return l&&R(l,t.domains)?[l]:[]}),suggestions:c.suggestions??[]}}async function P(n,s){const e=n.suggestions.slice(0,Q);return[n,...await Promise.all(e.map(a=>d(a,s)))]}async function p(n){const s=AbortSignal.timeout(u);let e=S(n,U),a=await r(e,{signal:s,redirect:"manual"});for(let c=0;c<Y&&v(a);c++){const o=a.headers.get("location");if(!o)break;e=S(new URL(o,a.url||e).toString(),U),a=await r(e,{signal:s,redirect:"manual"})}return a}async function M(n){try{const s=await p(n.url);if(!s.ok)return n.snippet;const e=A((await s.text()).slice(0,L));return e?e.slice(0,f):n.snippet}catch{return n.snippet}}async function D(n){if(!R({url:n,snippet:""},t.domains))return null;try{const s=await p(n);return s.ok&&A((await s.text()).slice(0,L)).slice(0,f)||null}catch{return null}}const C=(n,s)=>({answer:s.slice(0,g),results:[{url:n}],model:T,truncated:s.length>g,usage:{...x,searchQueries:0}});return Object.assign(async({query:n,depth:s,recency:e})=>{const a=n.match(I)?.[0],c=a?await D(a):null;if(a&&c)return C(a,c);const o=await d(n,e),l=s==="deep"?await P(o,e):[o],w=z(l),m=w.slice(0,h),_=m.slice(0,b[s]),k=await Promise.all(_.map(M)),E=_.map(($,j)=>K($,k[j])).join(`
4
+
5
+ `);return{answer:E.slice(0,g),results:m.map(q),model:T,truncated:E.length>g||w.length>m.length,usage:{...x,searchQueries:l.length}}},t.domains?.length?{domains:[...t.domains]}:{})}export{tt as searxng};
@@ -0,0 +1,2 @@
1
+ /** A page's readable text: the main content where it is marked, with menus, scripts and markup gone. */
2
+ export declare function textOf(html: string): string;
@@ -0,0 +1,3 @@
1
+ const r=/<(nav|header|footer|aside|form)\b[\s\S]*?<\/\1>/gi,c=/<(main|article)\b[^>]*>([\s\S]*?)<\/\1>/i;function t(e){return e.match(c)?.[2]??e}function a(e){return t(e).replace(/<!--[\s\S]*?-->/g," ").replace(/<script[\s\S]*?<\/script>/gi," ").replace(/<style[\s\S]*?<\/style>/gi," ").replace(/<noscript[\s\S]*?<\/noscript>/gi," ").replace(r," ").replace(/<\/(p|div|li|h[1-6]|tr|br|section|article)>/gi,`
2
+ `).replace(/<[^>]+>/g," ").replace(/&nbsp;/g," ").replace(/&amp;/g,"&").replace(/&lt;/g,"<").replace(/&gt;/g,">").replace(/&quot;/g,'"').replace(/&#39;|&apos;/g,"'").replace(/[ \t]+/g," ").replace(/\s*\n\s*/g,`
3
+ `).trim()}export{a as textOf};
@@ -0,0 +1,2 @@
1
+ export { readReply } from "./reply.js";
2
+ export { type SonarOptions, sonar } from "./sonar.js";
@@ -0,0 +1 @@
1
+ import{readReply as e}from"./reply.js";import{sonar as a}from"./sonar.js";export{e as readReply,a as sonar};
@@ -0,0 +1,13 @@
1
+ import { type Searcher, type SearcherOptions } from "../search.js";
2
+ export interface SonarOptions extends SearcherOptions {
3
+ /** Perplexity key. Required, and read from here rather than the environment. */
4
+ apiKey: string;
5
+ }
6
+ /**
7
+ * The live web through Perplexity's Sonar models, one model per depth.
8
+ *
9
+ * Perplexity searches, reads and writes the answer itself, with citation
10
+ * marks, and bills citation and reasoning tokens on top of the usual two.
11
+ * The timeout defaults to a minute, or five on a deep search.
12
+ */
13
+ export declare function sonar(opts: SonarOptions): Searcher;
@@ -0,0 +1 @@
1
+ import{retrying as m}from"../retry.js";import{DEFAULT_MAX_CHARS as d,DEFAULT_MAX_RESULTS as h}from"../search.js";import{readReply as _}from"./reply.js";const E={quick:"sonar",pro:"sonar-pro",deep:"sonar-deep-research"},f="https://api.perplexity.ai/chat/completions",p=6e4,T=3e5,u=500;function A(e){if(!e.apiKey?.trim())throw new Error("sonar needs a Perplexity apiKey; it will not read one from the process.");const i=e.fetch??globalThis.fetch.bind(globalThis),a=m(e);return Object.assign(async({query:s,depth:o,recency:n})=>{const r=E[o],c=e.timeoutMs??(o==="deep"?T:p),t=await a(()=>i(f,{method:"POST",signal:AbortSignal.timeout(c),headers:{authorization:`Bearer ${e.apiKey}`,"content-type":"application/json"},body:JSON.stringify({model:r,messages:[{role:"user",content:s}],...n?{search_recency_filter:n}:{},...e.domains?.length?{search_domain_filter:e.domains}:{}})}));if(!t.ok){const l=(await t.text()).slice(0,u);throw new Error(`Perplexity returned ${t.status} for ${r}: ${l}`)}return _(await t.json(),r,e.maxChars??d,e.maxResults??h)},e.domains?.length?{domains:[...e.domains]}:{})}export{A as sonar};
@@ -0,0 +1,4 @@
1
+ export type { TavilyReply } from "./reply.js";
2
+ export { readReply } from "./reply.js";
3
+ export type { TavilyOptions } from "./tavily.js";
4
+ export { tavily } from "./tavily.js";
@@ -0,0 +1 @@
1
+ import{readReply as o}from"./reply.js";import{tavily as t}from"./tavily.js";export{o as readReply,t as tavily};
@@ -0,0 +1,22 @@
1
+ import { type Searched } from "../search.js";
2
+ interface Hit {
3
+ url?: string;
4
+ title?: string;
5
+ content?: string;
6
+ raw_content?: string | null;
7
+ published_date?: string;
8
+ }
9
+ export interface TavilyReply {
10
+ answer?: string | null;
11
+ results?: Hit[];
12
+ auto_parameters?: Record<string, unknown>;
13
+ }
14
+ /**
15
+ * Tavily's reply as a `Searched`.
16
+ *
17
+ * Its own `answer` leads where it gave one, because it is a summary written
18
+ * across every result; the per-result text follows so a later stage can quote
19
+ * a source rather than the summary of it.
20
+ */
21
+ export declare function readReply(raw: TavilyReply, model: string, maxChars: number, maxResults: number): Searched;
22
+ export {};
@@ -0,0 +1,5 @@
1
+ import{NO_TOKENS as c}from"../search.js";function i(e){return e.url?{url:e.url,...e.title?{title:e.title}:{},...e.published_date?{date:e.published_date}:{}}:null}function a(e){const t=(e.raw_content??e.content??"").trim();return`## ${e.title??e.url}
2
+ ${e.url}
3
+ ${t}`}function p(e,t,r,s){const u=(e.results??[]).filter(l=>!!l.url),n=u.slice(0,s),o=[e.answer?.trim(),...n.map(a)].filter(Boolean).join(`
4
+
5
+ `);return{answer:o.slice(0,r),results:n.map(i).filter(l=>l!==null),model:t,truncated:o.length>r||u.length>n.length,usage:{...c}}}export{p as readReply};
@@ -0,0 +1,14 @@
1
+ import { type Searcher, type SearcherOptions } from "../search.js";
2
+ export interface TavilyOptions extends SearcherOptions {
3
+ /** Tavily key. Required, and read from here rather than the environment. */
4
+ apiKey: string;
5
+ }
6
+ /**
7
+ * The live web through Tavily, which searches and extracts in one call.
8
+ *
9
+ * Built for retrieval rather than for a person reading a results page: it
10
+ * returns the text of each page alongside its url, so a finding keeps both
11
+ * what was said and where it was said. Depth buys a wider search and more
12
+ * pages read, not a different model.
13
+ */
14
+ export declare function tavily(opts: TavilyOptions): Searcher;
@@ -0,0 +1 @@
1
+ import{retrying as l}from"../retry.js";import{DEFAULT_MAX_CHARS as m,DEFAULT_MAX_RESULTS as n}from"../search.js";import{readReply as _}from"./reply.js";const u="https://api.tavily.com/search",y="tavily-search",g=6e4,p=500,T={quick:{search_depth:"basic",pages:3},pro:{search_depth:"advanced",pages:5},deep:{search_depth:"advanced",pages:10}},E={day:1,week:7,month:30,year:365};function S(e){if(!e.apiKey?.trim())throw new Error("tavily needs an apiKey; it will not read one from the process.");const i=e.fetch??globalThis.fetch.bind(globalThis),r=l(e);return Object.assign(async({query:s,depth:o,recency:t})=>{const{search_depth:c,pages:d}=T[o],a=await r(()=>i(u,{method:"POST",signal:AbortSignal.timeout(e.timeoutMs??g),headers:{authorization:`Bearer ${e.apiKey}`,"content-type":"application/json"},body:JSON.stringify({query:s,search_depth:c,include_answer:!0,include_raw_content:!0,max_results:Math.min(d,e.maxResults??n),...t?{days:E[t]}:{},...e.domains?.length?{include_domains:e.domains}:{}})}));if(!a.ok){const h=(await a.text()).slice(0,p);throw new Error(`Tavily returned ${a.status}: ${h}`)}return _(await a.json(),y,e.maxChars??m,e.maxResults??n)},e.domains?.length?{domains:[...e.domains]}:{})}export{S as tavily};
@@ -0,0 +1,2 @@
1
+ export { type Replay, replaying, type Unrecorded } from "./replay.js";
2
+ export { type MemorySeed, type MemoryStore, memoryStore, type ObservedMoment, type StoredMemory, } from "./store.js";
@@ -0,0 +1 @@
1
+ import{replaying as e}from"./replay.js";import{memoryStore as p}from"./store.js";export{p as memoryStore,e as replaying};
@@ -0,0 +1,55 @@
1
+ import type { ToolResult } from "../types/index.js";
2
+ /** A call the recording had no answer for. */
3
+ export interface Unrecorded {
4
+ name: string;
5
+ args: Record<string, unknown>;
6
+ }
7
+ export interface ReplayOptions {
8
+ /**
9
+ * Tools that must still run for real, by name.
10
+ *
11
+ * A replay stands in for the world, not for the agent's own bookkeeping.
12
+ * `finish` carries the answer — the thing actually under test — so replaying
13
+ * it would mean a reworded prompt could only ever produce the old wording,
14
+ * and the run stalls the first time it says anything differently. `think` and
15
+ * `notes` are the same: they change nothing outside the process, so there is
16
+ * nothing to stand in for.
17
+ *
18
+ * Their recordings are dropped rather than kept unused, because a result
19
+ * nothing was ever going to ask for is not a step the new run skipped.
20
+ */
21
+ live?: readonly string[];
22
+ /** What runs the tools named in `live`. Required with it, and refused without. */
23
+ on?: {
24
+ invoke(name: string, args: Record<string, unknown>): Promise<ToolResult>;
25
+ };
26
+ }
27
+ export interface Replay {
28
+ /** Hands back what the recording holds, or a failed result saying it does not. */
29
+ invoke(name: string, args: Record<string, unknown>): Promise<ToolResult>;
30
+ /** Calls this run made that the recording could not answer, in order. */
31
+ unrecorded: Unrecorded[];
32
+ /** Recorded results this run never asked for. */
33
+ unused(): ToolResult[];
34
+ }
35
+ /**
36
+ * The tool results a run already produced, handed back instead of running anything.
37
+ *
38
+ * What a recorded trace is for: re-run the reasoning against a new prompt or a
39
+ * new model while the tools answer exactly as they did, so the only thing that
40
+ * changed is the thing under test. Nothing reaches a database, an API or a
41
+ * customer, and the comparison is not confounded by the world having moved.
42
+ *
43
+ * Matched on the arguments as well as the name, which is the whole value and
44
+ * also the part that looks like a limitation. A replay that answered
45
+ * `track("ORD-9")` with the recording of `track("ORD-1")` would be a green run
46
+ * proving nothing. A call the recording cannot answer is the finding: the new
47
+ * prompt asked something the old one never did, and the recording is not
48
+ * evidence about it either way.
49
+ *
50
+ * It offers no `effectOf`, and that is not an omission. Nothing runs during a
51
+ * replay, so nothing is destructive — a rail that gates on effect has nothing
52
+ * to guard, and giving it the original effects would have it blocking calls
53
+ * that are already only paper.
54
+ */
55
+ export declare function replaying(recorded: readonly ToolResult[], opts?: ReplayOptions): Replay;
@@ -0,0 +1 @@
1
+ const u=(i,n,e)=>i.name===n&&JSON.stringify(i.args)===JSON.stringify(e),f=(i,n,e)=>({name:i,args:n,status:"failed",output:"",error:`Nothing recorded for ${i}(${JSON.stringify(n)}). The recording holds: ${[...new Set(e.map(o=>o.name))].join(", ")||"nothing"}`,latencyMs:0});function l(i,n={}){const e=new Set(n.live??[]);if(e.size>0&&!n.on)throw new Error(`replaying names ${[...e].join(", ")} as live, but nothing to run them on.`);const o=i.filter(t=>!e.has(t.name)),s=new Set,c=[];return{unrecorded:c,unused:()=>o.filter((t,r)=>!s.has(r)),invoke:async(t,r)=>{if(e.has(t))return n.on.invoke(t,r);const a=o.findIndex((d,h)=>!s.has(h)&&u(d,t,r));return a===-1?(c.push({name:t,args:r}),f(t,r,o)):(s.add(a),{...o[a]})}}}export{l as replaying};
@@ -0,0 +1,35 @@
1
+ import type { MemoryClient, MemorySource } from "../memories/client.js";
2
+ export interface StoredMemory {
3
+ id: string;
4
+ context: string;
5
+ kind: string;
6
+ source: string;
7
+ text: string;
8
+ details: Record<string, unknown>;
9
+ extends: string[];
10
+ related_to: string[];
11
+ reinforces: string[];
12
+ contradicts: string[];
13
+ }
14
+ export interface ObservedMoment {
15
+ source: string;
16
+ payload: string | Record<string, unknown>;
17
+ stream?: string;
18
+ }
19
+ export interface MemorySeed {
20
+ context: string;
21
+ text: string;
22
+ details?: Record<string, unknown>;
23
+ kind?: string;
24
+ source?: MemorySource;
25
+ extends?: string[];
26
+ related_to?: string[];
27
+ reinforces?: string[];
28
+ contradicts?: string[];
29
+ }
30
+ export interface MemoryStore extends MemoryClient {
31
+ memories: StoredMemory[];
32
+ observed: ObservedMoment[];
33
+ remember(seed: MemorySeed): StoredMemory;
34
+ }
35
+ export declare function memoryStore(seeds?: MemorySeed[]): MemoryStore;
@@ -0,0 +1,4 @@
1
+ const O={policy:.95,verified:.9,tool:.8,document:.75,user:.7,assistant:.6,derived:.55,system:.5},g=c=>new Set(c.toLowerCase().split(/[^a-z0-9]+/).filter(o=>o.length>2));function w(c,o){const r=g(c),i=g(`${o.text} ${o.context.replace(":"," ")}`);let d=0;for(const p of r)i.has(p)&&(d+=1);return r.size===0?0:d/r.size}const z=c=>O[c]??.5,$=(c,o)=>c.map(r=>({memory:r,score:w(o,r)*.75+z(r.source)*.25})).filter(r=>w(o,r.memory)>0).sort((r,i)=>i.score-r.score),k=c=>[...c.extends,...c.related_to,...c.reinforces,...c.contradicts],h=(c,o,r,i=new Set)=>o===r?!0:i.has(o)?!1:(i.add(o),c.filter(d=>d.context===o).flatMap(k).some(d=>h(c,d,r,i))),v=(c,o,r)=>!r?.length||r.some(i=>h(c,o.context,i)||k(o).some(d=>h(c,d,i)));function T(c=[]){const o=[],r=[];let i=1;const d=t=>{const s={id:`m${i++}`,context:t.context,kind:t.kind??"fact",source:t.source??"assistant",text:t.text,details:t.details??{},extends:t.extends??[],related_to:t.related_to??[],reinforces:t.reinforces??[],contradicts:t.contradicts??[]};return o.push(s),s},p=()=>{const t=new Map;for(const s of o)t.set(s.context,(t.get(s.context)??0)+1);return[...t].map(([s,e])=>{const n=s.indexOf(":");return{context_id:s,context_type:s.slice(0,n),context_name:s.slice(n+1),reference_count:e}})},S=t=>{const s=[];for(const e of o)e.context===t?(s.push(...e.extends.map(n=>`extends \u2192 ${n}`)),s.push(...e.related_to.map(n=>`related_to \u2192 ${n}`)),s.push(...e.reinforces.map(n=>`reinforces \u2192 ${n}`)),s.push(...e.contradicts.map(n=>`contradicts \u2192 ${n}`))):(e.extends.includes(t)&&s.push(`extended_by \u2190 ${e.context}`),e.related_to.includes(t)&&s.push(`related_to \u2190 ${e.context}`),e.reinforces.includes(t)&&s.push(`reinforced_by \u2190 ${e.context}`),e.contradicts.includes(t)&&s.push(`contradicted_by \u2190 ${e.context}`));return[...new Set(s)]},b=t=>t.map((s,e)=>` ${e===t.length-1?"\u2514\u2500":"\u251C\u2500"} ${s}`),y=t=>`${t.map(s=>[s.context_id,...b(S(s.context_id))].join(`
2
+ `)).join(`
3
+ `)}
4
+ `;for(const t of c)d(t);return{memories:o,observed:r,remember:d,async retrieveMemories(t){const e=$(o.filter(n=>v(o,n,t.contextIds)),t.query).filter(({memory:n})=>!t.sources?.length||t.sources.includes(n.source)).slice(0,t.limit??10).map(({memory:n,score:l})=>({locus_id:n.id,context_ids:[n.context,...n.extends,...n.related_to,...n.reinforces].sort(),text:n.text,score:l,source:n.source,kind:n.kind}));return{items_found:e.length,items:e}},async deepRecall(t){const e=$(o.filter(n=>v(o,n,t.contextIds)),t.question).map(({memory:n,score:l})=>({text:n.text,source:n.source,locus:n.id,contexts:[n.context],score:l}));return{facts:e,contexts_walked:[...new Set(e.flatMap(n=>n.contexts))],...e[0]?{summary:e[0].text}:{},coverage:e.length?"full":"partial",fallback:!1,usage:{cost_usd:0}}},async storeEventsBatch(t){const s=t.map(e=>{const{data:n,...l}=e.payload;return d({context:e.context_id??"topic:untitled",text:typeof n=="string"?n:JSON.stringify(e.payload),details:l,kind:e.event_kind,source:e.source,extends:e.extends,related_to:e.related_to,reinforces:e.reinforces,contradicts:e.contradicts}),{status:"recorded",event_id:`m${i-1}`,relevance:"high"}});return{stored:s.length,filtered:0,failed:0,results:s}},async searchContexts(t,s,e){const n=t.toLowerCase(),l=p().filter(x=>!e?.context_type||x.context_type===e.context_type).filter(x=>x.context_id.toLowerCase().includes(n)).slice(0,e?.page_size??20);return{contexts:l,total:l.length,...e?.include==="relationships"?{relationships_text:y(l)}:{}}},async listContextTypes(){const t=new Map;for(const e of p())t.set(e.context_type,(t.get(e.context_type)??0)+e.reference_count);const s=[...t].map(([e,n])=>({context_type:e,count:n}));return{context_types:s,total:s.length}},async getContextGraph(t){const s=`${t.contextType}:${t.contextName}`,e=t.depth??1,n=p(),l=o.flatMap(a=>[...a.extends.map(f=>({from:a.context,to:f,link_type:"extends"})),...a.related_to.map(f=>({from:a.context,to:f,link_type:"related_to"})),...a.reinforces.map(f=>({from:a.context,to:f,link_type:"reinforces"})),...a.contradicts.map(f=>({from:a.context,to:f,link_type:"contradicts"}))]),x=new Set([s]);let _=[s];for(let a=0;a<e&&_.length;a+=1){const f=l.filter(u=>_.includes(u.from)||_.includes(u.to)).flatMap(u=>[u.from,u.to]).filter(u=>!x.has(u));for(const u of f)x.add(u);_=[...new Set(f)]}const M=n.filter(a=>x.has(a.context_id)),C=[...new Set(l.filter(a=>x.has(a.from)&&x.has(a.to)))];return{nodes:M,edges:C}},async observe(t){return r.push({source:t.source,payload:t.payload,...t.stream?{stream:t.stream}:{}}),{observed:!0}}}}export{T as memoryStore};
@@ -1,4 +1,4 @@
1
- import type { ArgSpec, ArgsOf, Effect, Tool } from "../types/index.js";
1
+ import type { ArgSpec, ArgsOf, Effect, Tool, Trust } from "../types/index.js";
2
2
  export interface ToolSpec<T extends readonly ArgSpec[]> {
3
3
  name: string;
4
4
  description: string;
@@ -6,6 +6,9 @@ export interface ToolSpec<T extends readonly ArgSpec[]> {
6
6
  aliases?: readonly string[];
7
7
  pinned?: boolean;
8
8
  effect?: Effect;
9
+ trust?: Trust;
10
+ costOf?(value: unknown): number | undefined;
11
+ needs?: readonly string[];
9
12
  run(args: ArgsOf<T>): Promise<unknown> | unknown;
10
13
  }
11
14
  /**
package/dist/tool/tool.js CHANGED
@@ -1 +1 @@
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
+ const r=/^[A-Za-z_][A-Za-z0-9_]*$/,i=["readonly","idempotent","destructive"],a=["own","external"];function s(n,t){if(!t.options?.length)throw new Error(`Tool "${n}" argument "${t.name}" is an enum with no options, so nothing can satisfy it.`);const e=new Set;for(const o of t.options){if(!o.trim())throw new Error(`Tool "${n}" argument "${t.name}" has a blank option, which the model can pick and you cannot act on.`);if(e.has(o))throw new Error(`Tool "${n}" argument "${t.name}" lists "${o}" twice.`);e.add(o)}}function f(n,t){if(t){for(const e of t)if(!e.trim())throw new Error(`Tool "${n}" has a blank alias, which matches every query or none.`)}}function h(n,t){const e=new Set;for(const o of t){if(!r.test(o.name))throw new Error(`Tool "${n}" argument "${o.name}" is not a usable name.`);if(e.has(o.name))throw new Error(`Tool "${n}" declares "${o.name}" twice.`);e.add(o.name),o.type==="enum"&&s(n,o)}}function d(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(h(n.name,n.args),f(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(", ")}.`);if(n.trust!==void 0&&!a.includes(n.trust))throw new Error(`Tool "${n.name}" declares trust "${n.trust}", which is not one of ${a.join(", ")}.`);return{name:n.name,description:n.description,args:n.args.map(t=>({...t,options:t.options?[...t.options]:void 0})),...n.aliases?.length?{aliases:[...n.aliases]}:{},pinned:n.pinned,...n.effect?{effect:n.effect}:{},...n.trust?{trust:n.trust}:{},...n.costOf?{costOf:n.costOf}:{},...n.needs?.length?{needs:[...n.needs]}:{},run:n.run}}export{d as tool};
@@ -1,2 +1,2 @@
1
1
  export type { ToolResult } from "./result.js";
2
- export type { ArgSpec, ArgsOf, ArgValue, Effect, Tool, ToolDeclaration } from "./tool.js";
2
+ export type { ArgSpec, ArgsOf, ArgValue, Effect, Tool, ToolDeclaration, Trust } from "./tool.js";
@@ -1,3 +1,5 @@
1
+ import type { Source } from "../search/search.js";
2
+ import type { Trust } from "./tool.js";
1
3
  /** What happened when one was called. */
2
4
  export interface ToolResult {
3
5
  name: string;
@@ -7,4 +9,40 @@ export interface ToolResult {
7
9
  output: string;
8
10
  error?: string;
9
11
  latencyMs: number;
12
+ /**
13
+ * What the tool declared about where its content came from.
14
+ *
15
+ * Carried onto the result so a hook watching what has already run can tell
16
+ * that untrusted content entered the context, without matching on a name.
17
+ */
18
+ trust?: Trust;
19
+ /** What this call itself cost, when the tool declared how to read it. */
20
+ costMicros?: number;
21
+ /**
22
+ * Where the content came from, when the tool returned it apart from the prose.
23
+ *
24
+ * A search tool already knows each result's url; rendering the output flattens
25
+ * that into text nobody downstream can read back. Carried here so a citation
26
+ * survives as a link rather than as something to parse out of a paragraph.
27
+ */
28
+ sources?: Source[];
29
+ /**
30
+ * Set when the output was cut to fit, with what it was cut from.
31
+ *
32
+ * Present only when something was actually dropped, so its absence is the
33
+ * claim that the output is whole.
34
+ */
35
+ truncated?: {
36
+ kept: number;
37
+ of: number;
38
+ };
39
+ /**
40
+ * Set when this exact destructive call had already run and the earlier result
41
+ * was handed back instead of running it again.
42
+ *
43
+ * The output is the first call's, unchanged, because it is still true — the
44
+ * refund did happen. This is for whoever counts how often the agent asked
45
+ * twice, which is a fact about the agent and not about the answer.
46
+ */
47
+ repeated?: true;
10
48
  }
@@ -66,6 +66,22 @@ export type ArgsOf<T extends readonly ArgSpec[]> = {
66
66
  * the same as safe.
67
67
  */
68
68
  export type Effect = "readonly" | "idempotent" | "destructive";
69
+ /**
70
+ * Where what a tool returns came from.
71
+ *
72
+ * `own` is yours: a database you run, a file you wrote, a calculation. `external`
73
+ * is content from outside that somebody else can write — a fetched page, a
74
+ * search result, an inbox, a document a customer uploaded. What comes back from
75
+ * one of those is data, and anything in it that reads like an instruction was
76
+ * put there by whoever controls the source.
77
+ *
78
+ * Declared rather than inferred, and separate from `effect`, because the two are
79
+ * independent: reading a web page is `readonly` and `external`, reading your own
80
+ * ledger is `readonly` and `own`, and only the first can carry an instruction
81
+ * written by a stranger. Unset means nobody has said, which is not the same as
82
+ * safe.
83
+ */
84
+ export type Trust = "own" | "external";
69
85
  /**
70
86
  * A description the model reads and a function you wrote, in one object.
71
87
  *
@@ -97,6 +113,51 @@ export interface Tool<Args = Record<string, unknown>> {
97
113
  pinned?: boolean;
98
114
  /** What calling it does outside the process. Unset means unknown. */
99
115
  effect?: Effect;
116
+ /**
117
+ * Whether what it returns is content somebody outside wrote. Unset means unknown.
118
+ *
119
+ * A guard that wants to say "no destructive call after untrusted content came
120
+ * in" has to gate on something the tool declared. Without this the only thing
121
+ * left to gate on is the tool's name, which the hook documentation warns
122
+ * against for the reason it always does: a rename turns the rule off, silently
123
+ * and in the permissive direction.
124
+ */
125
+ trust?: Trust;
126
+ /**
127
+ * Reads what one call cost, in micro-USD, out of what `run` returned.
128
+ *
129
+ * Some tools spend money: a deep recall runs model calls of its own, a judge
130
+ * tool pays for a verdict, a sub-agent tool pays for a whole run. That spend
131
+ * is invisible to the loop that called them — token counts belong to the
132
+ * outer model call and a tool's own bill is not in them. Without this the
133
+ * number exists only inside the stringified result, where the model reads it
134
+ * and no code can add it up.
135
+ *
136
+ * A reader rather than a field on the result, because `run` returns whatever
137
+ * suits the tool and only the tool knows which part of it is the price.
138
+ */
139
+ costOf?(value: unknown): number | undefined;
140
+ /**
141
+ * Tools that have to be offered alongside this one, or not at all.
142
+ *
143
+ * Selection scores each tool on its own, which is the wrong unit when two of
144
+ * them are a cheap entry point and its expensive counterpart. `deep_recall`
145
+ * and `recall` overlap on almost every term, so a shortlist of three routinely
146
+ * kept the dear one and dropped the cheap one — while both descriptions tell
147
+ * the model to try the cheap one first. The model was then instructed to
148
+ * prefer a tool it had not been given.
149
+ *
150
+ * Enforced as a guarantee rather than a preference: where the shortlist has
151
+ * no room for the pair, the tool that declared the need is dropped instead.
152
+ * Offering the expensive half alone is the failure this exists to stop, and a
153
+ * best-effort version would still do it whenever the ceiling was tight.
154
+ *
155
+ * A name no tool on the bus carries is ignored rather than refused. The
156
+ * constraint is about what a shortlist offers, and a partner that is not on
157
+ * the bus cannot be dropped in favour of anything — a bus holding this tool
158
+ * alone has no selection problem to have.
159
+ */
160
+ needs?: readonly string[];
100
161
  run(args: Args): Promise<unknown> | unknown;
101
162
  }
102
163
  /** A tool as the model sees it: no implementation, no pin. */