@spendgraph/tools 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +25 -258
  3. package/dist/bogus/ask-mum.d.ts +5 -0
  4. package/dist/bogus/ask-mum.js +1 -0
  5. package/dist/bogus/book-taxi.d.ts +9 -0
  6. package/dist/bogus/book-taxi.js +1 -0
  7. package/dist/bogus/bus.d.ts +8 -0
  8. package/dist/bogus/bus.js +1 -0
  9. package/dist/bogus/calculator.d.ts +14 -0
  10. package/dist/bogus/calculator.js +1 -0
  11. package/dist/bogus/dictionary.d.ts +6 -0
  12. package/dist/bogus/dictionary.js +1 -0
  13. package/dist/bogus/feed-cat.d.ts +8 -0
  14. package/dist/bogus/feed-cat.js +1 -0
  15. package/dist/bogus/football-score.d.ts +6 -0
  16. package/dist/bogus/football-score.js +1 -0
  17. package/dist/bogus/gold-price.d.ts +7 -0
  18. package/dist/bogus/gold-price.js +1 -0
  19. package/dist/bogus/index.d.ts +14 -0
  20. package/dist/bogus/index.js +1 -0
  21. package/dist/bogus/my-calendar.d.ts +6 -0
  22. package/dist/bogus/my-calendar.js +1 -0
  23. package/dist/bogus/my-notes.d.ts +6 -0
  24. package/dist/bogus/my-notes.js +1 -0
  25. package/dist/bogus/my-recipes.d.ts +6 -0
  26. package/dist/bogus/my-recipes.js +1 -0
  27. package/dist/bogus/order-pizza.d.ts +12 -0
  28. package/dist/bogus/order-pizza.js +1 -0
  29. package/dist/bogus/prayer-times.d.ts +6 -0
  30. package/dist/bogus/prayer-times.js +1 -0
  31. package/dist/bogus/receipt.d.ts +8 -0
  32. package/dist/bogus/receipt.js +1 -0
  33. package/dist/builtin/calculate/calculate.js +1 -21
  34. package/dist/builtin/calculate/exact.d.ts +16 -0
  35. package/dist/builtin/calculate/exact.js +1 -0
  36. package/dist/builtin/calculate/index.js +1 -2
  37. package/dist/builtin/calculate/parse.js +1 -137
  38. package/dist/builtin/clock/clock.js +1 -61
  39. package/dist/builtin/clock/index.js +1 -1
  40. package/dist/builtin/code-exec/code-exec.d.ts +28 -0
  41. package/dist/builtin/code-exec/code-exec.js +1 -0
  42. package/dist/builtin/code-exec/index.d.ts +1 -0
  43. package/dist/builtin/code-exec/index.js +1 -0
  44. package/dist/builtin/date/date.js +1 -41
  45. package/dist/builtin/date/index.js +1 -2
  46. package/dist/builtin/date/shift.js +1 -47
  47. package/dist/builtin/deep-recall/deep-recall.d.ts +39 -2
  48. package/dist/builtin/deep-recall/deep-recall.js +1 -45
  49. package/dist/builtin/deep-recall/index.d.ts +1 -1
  50. package/dist/builtin/deep-recall/index.js +1 -1
  51. package/dist/builtin/escalate/escalate.js +1 -20
  52. package/dist/builtin/escalate/index.js +1 -1
  53. package/dist/builtin/finish/finish.js +1 -26
  54. package/dist/builtin/finish/index.js +1 -1
  55. package/dist/builtin/http/allow.js +1 -37
  56. package/dist/builtin/http/http.js +1 -41
  57. package/dist/builtin/http/index.js +1 -2
  58. package/dist/builtin/index.d.ts +2 -1
  59. package/dist/builtin/index.js +1 -11
  60. package/dist/builtin/json/index.js +1 -2
  61. package/dist/builtin/json/json.js +1 -37
  62. package/dist/builtin/json/path.js +1 -56
  63. package/dist/builtin/think/index.js +1 -1
  64. package/dist/builtin/think/think.js +1 -12
  65. package/dist/builtin/web-search/index.js +1 -1
  66. package/dist/builtin/web-search/reply.js +1 -27
  67. package/dist/builtin/web-search/web-search.d.ts +6 -1
  68. package/dist/builtin/web-search/web-search.js +1 -103
  69. package/dist/builtin/write-document/commission.js +13 -152
  70. package/dist/builtin/write-document/design.js +5 -86
  71. package/dist/builtin/write-document/http.js +1 -71
  72. package/dist/builtin/write-document/index.js +1 -4
  73. package/dist/builtin/write-document/types.js +0 -1
  74. package/dist/builtin/write-document/write-document.js +1 -142
  75. package/dist/bus/args.d.ts +11 -0
  76. package/dist/bus/args.js +1 -0
  77. package/dist/bus/bus.js +1 -49
  78. package/dist/bus/group.d.ts +8 -0
  79. package/dist/bus/group.js +1 -0
  80. package/dist/bus/index.d.ts +1 -0
  81. package/dist/bus/index.js +1 -2
  82. package/dist/bus/invoke.js +1 -43
  83. package/dist/bus/options.js +0 -1
  84. package/dist/bus/output.d.ts +26 -0
  85. package/dist/bus/output.js +2 -0
  86. package/dist/bus/overlap.js +1 -31
  87. package/dist/bus/score.d.ts +11 -9
  88. package/dist/bus/score.js +1 -12
  89. package/dist/bus/select.d.ts +2 -1
  90. package/dist/bus/select.js +1 -44
  91. package/dist/bus/step.js +1 -11
  92. package/dist/bus/terms.d.ts +25 -0
  93. package/dist/bus/terms.js +1 -0
  94. package/dist/bus/trace.js +1 -22
  95. package/dist/index.d.ts +13 -3
  96. package/dist/index.js +1 -3
  97. package/dist/internals.d.ts +1 -1
  98. package/dist/internals.js +1 -10
  99. package/dist/tool/bind.js +1 -64
  100. package/dist/tool/index.js +1 -2
  101. package/dist/tool/tool.d.ts +1 -0
  102. package/dist/tool/tool.js +1 -51
  103. package/dist/types/index.js +0 -1
  104. package/dist/types/result.js +0 -1
  105. package/dist/types/tool.d.ts +13 -0
  106. package/dist/types/tool.js +0 -1
  107. package/dist/wire/index.js +1 -3
  108. package/dist/wire/json-schema.js +1 -65
  109. package/dist/wire/markdown.d.ts +5 -3
  110. package/dist/wire/markdown.js +9 -29
  111. package/dist/wire/providers.js +1 -18
  112. package/package.json +11 -7
@@ -1 +1 @@
1
- export { answerOf, finish, isFinished } from "./finish.js";
1
+ import{answerOf as e,finish as f,isFinished as n}from"./finish.js";export{e as answerOf,f as finish,n as isFinished};
@@ -1,37 +1 @@
1
- const PRIVATE_V4 = /^(10\.|127\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/;
2
- const LOCAL_NAMES = new Set(["localhost", "metadata.google.internal"]);
3
- function isPrivateHost(host) {
4
- const name = host.toLowerCase();
5
- if (LOCAL_NAMES.has(name))
6
- return true;
7
- if (name.endsWith(".local") || name.endsWith(".internal") || name.endsWith(".localhost")) {
8
- return true;
9
- }
10
- if (name.startsWith("["))
11
- return true;
12
- return PRIVATE_V4.test(name);
13
- }
14
- function matches(host, rule) {
15
- const h = host.toLowerCase();
16
- const r = rule.toLowerCase();
17
- return r.startsWith(".") ? h === r.slice(1) || h.endsWith(r) : h === r;
18
- }
19
- export function checkUrl(raw, allow) {
20
- let url;
21
- try {
22
- url = new URL(raw);
23
- }
24
- catch {
25
- throw new Error(`"${raw}" is not a URL.`);
26
- }
27
- if (url.protocol !== "https:") {
28
- throw new Error(`Only https is allowed, and this is ${url.protocol.replace(":", "")}.`);
29
- }
30
- if (isPrivateHost(url.hostname)) {
31
- throw new Error(`${url.hostname} is a private or link-local address.`);
32
- }
33
- if (!allow.some((rule) => matches(url.hostname, rule))) {
34
- throw new Error(`${url.hostname} is not in the allowlist: ${allow.join(", ")}.`);
35
- }
36
- return url;
37
- }
1
+ const n=/^(10\.|127\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/,s=new Set(["localhost","metadata.google.internal"]);function i(r){const e=r.toLowerCase();return s.has(e)||e.endsWith(".local")||e.endsWith(".internal")||e.endsWith(".localhost")||e.startsWith("[")?!0:n.test(e)}function a(r,e){const t=r.toLowerCase(),o=e.toLowerCase();return o.startsWith(".")?t===o.slice(1)||t.endsWith(o):t===o}function h(r,e){let t;try{t=new URL(r)}catch{throw new Error(`"${r}" is not a URL.`)}if(t.protocol!=="https:")throw new Error(`Only https is allowed, and this is ${t.protocol.replace(":","")}.`);if(i(t.hostname))throw new Error(`${t.hostname} is a private or link-local address.`);if(!e.some(o=>a(t.hostname,o)))throw new Error(`${t.hostname} is not in the allowlist: ${e.join(", ")}.`);return t}export{h as checkUrl};
@@ -1,41 +1 @@
1
- import { tool } from "../../tool/index.js";
2
- import { checkUrl } from "./allow.js";
3
- const DEFAULT_TIMEOUT_MS = 10_000;
4
- const DEFAULT_MAX_CHARS = 100_000;
5
- export function httpRequest(opts) {
6
- if (!opts.allow?.length) {
7
- throw new Error("httpRequest needs an allow list of hosts; there is no safe default.");
8
- }
9
- const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
10
- const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
11
- return tool({
12
- name: "http_request",
13
- description: `Fetches a URL and returns its status and body. Only these hosts are reachable: ` +
14
- `${opts.allow.join(", ")}. Use it to read an API or a page you have been pointed at.`,
15
- args: [
16
- { name: "url", type: "string", required: true, maxLength: 2000 },
17
- { name: "method", type: "enum", required: false, options: ["GET", "POST"] },
18
- { name: "body", type: "text", required: false, maxLength: 20_000 },
19
- ],
20
- run: async ({ url, method = "GET", body }) => {
21
- const target = checkUrl(url, opts.allow);
22
- const signal = AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
23
- const res = await doFetch(target, {
24
- method,
25
- signal,
26
- headers: {
27
- ...opts.headers,
28
- ...(body ? { "content-type": "application/json" } : {}),
29
- },
30
- ...(body && method === "POST" ? { body } : {}),
31
- });
32
- const text = await res.text();
33
- return {
34
- status: res.status,
35
- ok: res.ok,
36
- body: text.slice(0, maxChars),
37
- truncated: text.length > maxChars,
38
- };
39
- },
40
- });
41
- }
1
+ import{tool as w}from"../../tool/index.js";import{checkUrl as h}from"./allow.js";const p=1e4,T=1e5,u=3,d=t=>t.status>=300&&t.status<400;function _(t){if(!t.allow?.length)throw new Error("httpRequest needs an allow list of hosts; there is no safe default.");const m=t.fetch??globalThis.fetch.bind(globalThis),o=t.maxChars??T;return w({name:"http_request",description:`Fetches a URL and returns its status and body. Only these hosts are reachable: ${t.allow.join(", ")}. Use it to read an API or a page you have been pointed at.`,args:[{name:"url",type:"string",required:!0,maxLength:2e3},{name:"method",type:"enum",required:!1,options:["GET","POST"]},{name:"body",type:"text",required:!1,maxLength:2e4}],run:async({url:f,method:s="GET",body:n})=>{const r=h(f,t.allow),g=AbortSignal.timeout(t.timeoutMs??p),i=a=>m(a,{method:s,signal:g,redirect:"manual",headers:{...t.headers,...n?{"content-type":"application/json"}:{}},...n&&s==="POST"?{body:n}:{}});let e=await i(r);for(let a=0;a<u&&d(e);a++){const c=e.headers.get("location");if(!c)break;e=await i(h(new URL(c,e.url||r).toString(),t.allow))}if(d(e))throw new Error(`Gave up after ${u} redirects from ${r.hostname}.`);const l=await e.text();return{status:e.status,ok:e.ok,body:l.slice(0,o),truncated:l.length>o}}})}export{_ as httpRequest};
@@ -1,2 +1 @@
1
- export { checkUrl } from "./allow.js";
2
- export { httpRequest } from "./http.js";
1
+ import{checkUrl as t}from"./allow.js";import{httpRequest as p}from"./http.js";export{t as checkUrl,p as httpRequest};
@@ -1,7 +1,8 @@
1
1
  export { calculate, evaluate } from "./calculate/index.js";
2
2
  export { type ClockOptions, currentTime } from "./clock/index.js";
3
+ export { type CodeExecOptions, codeExec, type Sandbox, type SandboxRequest, type SandboxResult, } from "./code-exec/index.js";
3
4
  export { dateMath } from "./date/index.js";
4
- export { type DeepRecallOptions, deepRecall } from "./deep-recall/index.js";
5
+ export { type DeepRecallOptions, deepRecall, type RecalledFact, type Recaller, type RecallResult, } from "./deep-recall/index.js";
5
6
  export { type EscalateOptions, type EscalationRequest, escalate } from "./escalate/index.js";
6
7
  export { answerOf, type Finished, finish, isFinished } from "./finish/index.js";
7
8
  export { checkUrl, type HttpOptions, httpRequest } from "./http/index.js";
@@ -1,11 +1 @@
1
- export { calculate, evaluate } from "./calculate/index.js";
2
- export { currentTime } from "./clock/index.js";
3
- export { dateMath } from "./date/index.js";
4
- export { deepRecall } from "./deep-recall/index.js";
5
- export { escalate } from "./escalate/index.js";
6
- export { answerOf, finish, isFinished } from "./finish/index.js";
7
- export { checkUrl, httpRequest } from "./http/index.js";
8
- export { jsonQuery } from "./json/index.js";
9
- export { think } from "./think/index.js";
10
- export { webSearch } from "./web-search/index.js";
11
- export { CommissionAbortedError, CommissionTimeoutError, DEFAULT_DESIGN, mergeDesign, renderDesign, writeDocument, } from "./write-document/index.js";
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,2 +1 @@
1
- export { jsonQuery } from "./json.js";
2
- export { readPath } from "./path.js";
1
+ import{jsonQuery as e}from"./json.js";import{readPath as a}from"./path.js";export{e as jsonQuery,a as readPath};
@@ -1,37 +1 @@
1
- import { tool } from "../../tool/index.js";
2
- import { readPath } from "./path.js";
3
- const DEFAULT_MAX_CHARS = 20_000;
4
- export function jsonQuery(opts = {}) {
5
- const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
6
- return tool({
7
- name: "json_query",
8
- effect: "readonly",
9
- description: "Reads one value out of a JSON document by path. Paths look like " +
10
- "`invoices[0].total`, or `invoices[*].total` for every element. Use it instead " +
11
- "of reading a long document yourself.",
12
- args: [
13
- { name: "json", type: "text", required: true, maxLength: 200_000 },
14
- { name: "path", type: "string", required: true, maxLength: 500 },
15
- ],
16
- run: ({ json, path }) => {
17
- let document;
18
- try {
19
- document = JSON.parse(json);
20
- }
21
- catch (err) {
22
- throw new Error(`json is not valid JSON: ${err.message}`);
23
- }
24
- const value = readPath(document, path);
25
- if (value === undefined)
26
- return { found: false, path, value: null };
27
- const rendered = JSON.stringify(value) ?? "null";
28
- return {
29
- found: true,
30
- path,
31
- value,
32
- truncated: rendered.length > maxChars,
33
- ...(rendered.length > maxChars ? { value: `${rendered.slice(0, maxChars)}…` } : {}),
34
- };
35
- },
36
- });
37
- }
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,56 +1 @@
1
- function parsePath(path) {
2
- const segments = [];
3
- for (const part of path.split(".")) {
4
- if (!part)
5
- continue;
6
- const [, name, brackets] = /^([^[\]]*)((?:\[[^\]]*\])*)$/.exec(part) ?? [];
7
- if (name === undefined)
8
- throw new Error(`"${part}" is not a path segment.`);
9
- if (name)
10
- segments.push({ kind: "key", name });
11
- for (const [, inner] of (brackets ?? "").matchAll(/\[([^\]]*)\]/g)) {
12
- if (inner === "*") {
13
- segments.push({ kind: "all" });
14
- continue;
15
- }
16
- const at = Number(inner);
17
- if (!Number.isInteger(at))
18
- throw new Error(`"[${inner}]" is not an index or a *.`);
19
- segments.push({ kind: "index", at });
20
- }
21
- }
22
- return segments;
23
- }
24
- function step(value, segment) {
25
- if (value === null || value === undefined)
26
- return undefined;
27
- if (segment.kind === "key") {
28
- if (typeof value !== "object" || Array.isArray(value))
29
- return undefined;
30
- return value[segment.name];
31
- }
32
- if (!Array.isArray(value))
33
- return undefined;
34
- if (segment.kind === "index") {
35
- return value.at(segment.at);
36
- }
37
- return value;
38
- }
39
- export function readPath(document, path) {
40
- let cursor = document;
41
- let spread = false;
42
- for (const segment of parsePath(path)) {
43
- if (segment.kind === "all") {
44
- if (!Array.isArray(cursor))
45
- return undefined;
46
- cursor = spread ? cursor.flat() : [...cursor];
47
- spread = true;
48
- continue;
49
- }
50
- cursor =
51
- spread && Array.isArray(cursor)
52
- ? cursor.map((item) => step(item, segment))
53
- : step(cursor, segment);
54
- }
55
- return cursor;
56
- }
1
+ function a(r){const e=[];for(const n of r.split(".")){if(!n)continue;const[,t,i]=/^([^[\]]*)((?:\[[^\]]*\])*)$/.exec(n)??[];if(t===void 0)throw new Error(`"${n}" is not a path segment.`);t&&e.push({kind:"key",name:t});for(const[,f]of(i??"").matchAll(/\[([^\]]*)\]/g)){if(f==="*"){e.push({kind:"all"});continue}const o=Number(f);if(!Number.isInteger(o))throw new Error(`"[${f}]" is not an index or a *.`);e.push({kind:"index",at:o})}}return e}function s(r,e){if(r!=null){if(e.kind==="key")return typeof r!="object"||Array.isArray(r)?void 0:r[e.name];if(Array.isArray(r))return e.kind==="index"?r.at(e.at):r}}function d(r,e){let n=r,t=!1;for(const i of a(e)){if(i.kind==="all"){if(!Array.isArray(n))return;n=t?n.flat():[...n],t=!0;continue}n=t&&Array.isArray(n)?n.map(f=>s(f,i)):s(n,i)}return n}export{d as readPath};
@@ -1 +1 @@
1
- export { think } from "./think.js";
1
+ import{think as t}from"./think.js";export{t as think};
@@ -1,12 +1 @@
1
- import { tool } from "../../tool/index.js";
2
- export function think() {
3
- return tool({
4
- name: "think",
5
- effect: "readonly",
6
- description: "Think a step through before acting. Use it to check a plan against the rules, " +
7
- "to work out what is still missing, or to decide between two tools. It changes " +
8
- "nothing and returns nothing — it is scratch space, not an answer.",
9
- args: [{ name: "thought", type: "text", required: true, maxLength: 4000 }],
10
- run: () => "noted",
11
- });
12
- }
1
+ import{tool as t}from"../../tool/index.js";function n(){return t({name:"think",effect:"readonly",description:"Think a step through before acting. Use it to check a plan against the rules, to work out what is still missing, or to decide between two tools. It changes nothing and returns nothing \u2014 it is scratch space, not an answer.",args:[{name:"thought",type:"text",required:!0,maxLength:4e3}],run:()=>"noted"})}export{n as think};
@@ -1 +1 @@
1
- export { webSearch } from "./web-search.js";
1
+ import{webSearch as o}from"./web-search.js";export{o as webSearch};
@@ -1,27 +1 @@
1
- function sources(raw) {
2
- const listed = raw.search_results ?? [];
3
- if (listed.length) {
4
- return listed.flatMap((r) => (r.url ? [{ url: r.url, title: r.title, date: r.date }] : []));
5
- }
6
- return (raw.citations ?? []).map((url) => ({ url }));
7
- }
8
- export function readReply(body, fallbackModel, maxChars, maxResults) {
9
- const raw = body;
10
- const answer = raw.choices?.[0]?.message?.content ?? "";
11
- const found = sources(raw);
12
- const results = found.slice(0, maxResults);
13
- const usage = raw.usage ?? {};
14
- return {
15
- answer: answer.slice(0, maxChars),
16
- results,
17
- model: raw.model ?? fallbackModel,
18
- truncated: answer.length > maxChars || found.length > results.length,
19
- usage: {
20
- inputTokens: usage.prompt_tokens ?? 0,
21
- outputTokens: usage.completion_tokens ?? 0,
22
- citationTokens: usage.citation_tokens ?? 0,
23
- reasoningTokens: usage.reasoning_tokens ?? 0,
24
- searchQueries: usage.num_search_queries ?? 0,
25
- },
26
- };
27
- }
1
+ function a(n){const s=n.search_results??[];return s.length?s.flatMap(e=>e.url?[{url:e.url,title:e.title,date:e.date}]:[]):(n.citations??[]).map(e=>({url:e}))}function l(n,s,e,r){const o=n,c=o.choices?.[0]?.message?.content??"",u=a(o),i=u.slice(0,r),t=o.usage??{};return{answer:c.slice(0,e),results:i,model:o.model??s,truncated:c.length>e||u.length>i.length,usage:{inputTokens:t.prompt_tokens??0,outputTokens:t.completion_tokens??0,citationTokens:t.citation_tokens??0,reasoningTokens:t.reasoning_tokens??0,searchQueries:t.num_search_queries??0}}}export{l as readReply};
@@ -23,13 +23,18 @@ export interface WebSearchOptions {
23
23
  *
24
24
  * A wave of sub-questions searches in parallel, so two calls landing together
25
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.
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.
27
30
  */
28
31
  attempts?: number;
29
32
  /** Injected for tests. */
30
33
  fetch?: typeof fetch;
31
34
  /** Injected for tests. */
32
35
  sleep?: (ms: number) => Promise<void>;
36
+ /** Injected for tests. Returns 0 to 1, and spreads a retried wave apart. */
37
+ random?: () => number;
33
38
  }
34
39
  /**
35
40
  * The live web, through Perplexity's Sonar models.
@@ -1,103 +1 @@
1
- import { tool } from "../../tool/index.js";
2
- import { readReply } from "./reply.js";
3
- const DEPTHS = ["quick", "pro", "deep"];
4
- const MODELS = {
5
- quick: "sonar",
6
- pro: "sonar-pro",
7
- deep: "sonar-deep-research",
8
- };
9
- const ENDPOINT = "https://api.perplexity.ai/chat/completions";
10
- const DEFAULT_TIMEOUT_MS = 60_000;
11
- const DEEP_TIMEOUT_MS = 300_000;
12
- const DEFAULT_MAX_CHARS = 20_000;
13
- const DEFAULT_MAX_RESULTS = 20;
14
- const ERROR_DETAIL_CHARS = 500;
15
- const BACKOFF_BASE_MS = 500;
16
- const MAX_BACKOFF_MS = 8_000;
17
- function retryAfterMs(res) {
18
- const raw = res.headers.get("retry-after");
19
- if (!raw)
20
- return null;
21
- const seconds = Number(raw);
22
- return Number.isFinite(seconds) && seconds >= 0 ? seconds * 1000 : null;
23
- }
24
- const isWorthRetrying = (status) => status === 429 || status >= 500;
25
- function describe(maxDepth, domains) {
26
- return ("Searches the live web and returns an answer with the sources it rests on. Use it for " +
27
- "anything that turns on current facts — prices, releases, who holds a post, what " +
28
- "changed. quick is one pass and is right for most lookups; pro searches wider and cites " +
29
- "more." +
30
- (maxDepth === "deep"
31
- ? " deep is an exhaustive multi-step review: minutes, and many times the cost. Ask for it only when a wrong answer is expensive."
32
- : " deep is not enabled here.") +
33
- (domains?.length ? ` Results come only from ${domains.join(", ")}.` : ""));
34
- }
35
- export function webSearch(opts) {
36
- if (!opts.apiKey?.trim()) {
37
- throw new Error("webSearch needs a Perplexity apiKey; it will not read one from the process.");
38
- }
39
- const doFetch = opts.fetch ?? globalThis.fetch.bind(globalThis);
40
- const maxDepth = opts.maxDepth ?? "pro";
41
- const attempts = Math.max(1, opts.attempts ?? 3);
42
- const pause = opts.sleep ?? ((ms) => new Promise((done) => setTimeout(done, ms)));
43
- return tool({
44
- name: "web_search",
45
- description: describe(maxDepth, opts.domains),
46
- effect: "readonly",
47
- args: [
48
- {
49
- name: "query",
50
- type: "string",
51
- required: true,
52
- maxLength: 1000,
53
- description: "What to find out, as a question. One thing, not several.",
54
- },
55
- {
56
- name: "depth",
57
- type: "enum",
58
- required: false,
59
- options: ["quick", "pro", "deep"],
60
- description: "Default quick. Raise it only when a first pass would not settle it.",
61
- },
62
- {
63
- name: "recency",
64
- type: "enum",
65
- required: false,
66
- options: ["day", "week", "month", "year"],
67
- description: "Only consider pages published within this. Leave unset for anything.",
68
- },
69
- ],
70
- run: async ({ query, depth = "quick", recency }) => {
71
- if (DEPTHS.indexOf(depth) > DEPTHS.indexOf(maxDepth)) {
72
- throw new Error(`web_search was asked for "${depth}", above the "${maxDepth}" this deployment allows.`);
73
- }
74
- const model = MODELS[depth];
75
- const fallback = depth === "deep" ? DEEP_TIMEOUT_MS : DEFAULT_TIMEOUT_MS;
76
- for (let attempt = 1;; attempt++) {
77
- const res = await doFetch(ENDPOINT, {
78
- method: "POST",
79
- signal: AbortSignal.timeout(opts.timeoutMs ?? fallback),
80
- headers: {
81
- authorization: `Bearer ${opts.apiKey}`,
82
- "content-type": "application/json",
83
- },
84
- body: JSON.stringify({
85
- model,
86
- messages: [{ role: "user", content: query }],
87
- ...(recency ? { search_recency_filter: recency } : {}),
88
- ...(opts.domains?.length ? { search_domain_filter: opts.domains } : {}),
89
- }),
90
- });
91
- if (res.ok) {
92
- return readReply(await res.json(), model, opts.maxChars ?? DEFAULT_MAX_CHARS, opts.maxResults ?? DEFAULT_MAX_RESULTS);
93
- }
94
- const detail = (await res.text()).slice(0, ERROR_DETAIL_CHARS);
95
- if (!isWorthRetrying(res.status) || attempt === attempts) {
96
- throw new Error(`Perplexity returned ${res.status} for ${model}: ${detail}`);
97
- }
98
- const wait = retryAfterMs(res) ?? BACKOFF_BASE_MS * 2 ** (attempt - 1);
99
- await pause(Math.min(MAX_BACKOFF_MS, wait));
100
- }
101
- },
102
- });
103
- }
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,152 +1,13 @@
1
- import { createRequest, MoaApiError } from "./http.js";
2
- const TASK_STATUS = {
3
- queued: "pending",
4
- running: "running",
5
- succeeded: "done",
6
- failed: "failed",
7
- canceled: "failed",
8
- };
9
- const ARTIFACT_FORMATS = ["md", "pdf", "docx", "pptx"];
10
- const formatOf = (name) => {
11
- const ext = name?.split(".").pop()?.toLowerCase();
12
- return ext && ARTIFACT_FORMATS.includes(ext) ? ext : undefined;
13
- };
14
- function toArtifacts(raw) {
15
- if (!raw?.length)
16
- return undefined;
17
- const artifacts = raw.flatMap((a) => {
18
- const format = formatOf(a.name);
19
- if (!format || !(a.url || a.data))
20
- return [];
21
- return [
22
- {
23
- name: a.name ?? `document.${format}`,
24
- format,
25
- url: a.url,
26
- data: a.data,
27
- mime: a.contentType,
28
- sizeBytes: a.sizeBytes,
29
- },
30
- ];
31
- });
32
- return artifacts.length ? artifacts : undefined;
33
- }
34
- function deliverablesBlock(formats) {
35
- return (`\n\n<deliverables>\nBesides the text answer, save the finished deliverable ` +
36
- `into the artifacts/ directory of your working dir as: ` +
37
- `${formats.map((f) => `one .${f} file`).join(", ")}.\n</deliverables>`);
38
- }
39
- function taskText(input) {
40
- let task = input.task;
41
- if (input.context)
42
- task += `\n\n<context>\n${input.context}\n</context>`;
43
- if (input.design)
44
- task += `\n\n<design>\n${input.design}\n</design>`;
45
- if (input.artifacts?.length)
46
- task += deliverablesBlock(input.artifacts);
47
- return task;
48
- }
49
- export function createCommissionClient(config = {}) {
50
- const request = createRequest(config);
51
- return {
52
- async create(input) {
53
- const created = await request("/tasks", {
54
- method: "POST",
55
- body: {
56
- task: taskText(input),
57
- mode: "striker",
58
- ...(input.sessionId ? { sessionId: input.sessionId } : {}),
59
- ...(input.newSessionId ? { newSessionId: input.newSessionId } : {}),
60
- },
61
- });
62
- if (!created.id)
63
- throw new MoaApiError("Moa did not return a task id", 502);
64
- return created.id;
65
- },
66
- async get(id, opts) {
67
- const qs = opts?.waitMs && opts.waitMs > 0 ? `?waitMs=${Math.floor(opts.waitMs)}` : "";
68
- const t = await request(`/tasks/${encodeURIComponent(id)}${qs}`, {
69
- signal: opts?.signal,
70
- });
71
- const status = t.status ? TASK_STATUS[t.status] : undefined;
72
- return {
73
- id: t.id ?? id,
74
- status: status ?? "running",
75
- output: t.result,
76
- error: t.error,
77
- costUsd: t.costUsd,
78
- numTurns: t.numTurns,
79
- sessionId: t.sessionId,
80
- artifacts: toArtifacts(t.artifacts),
81
- };
82
- },
83
- async cancel(id) {
84
- await request(`/tasks/${encodeURIComponent(id)}`, { method: "DELETE" });
85
- },
86
- };
87
- }
88
- export const INTERACTIVE_TIMEOUT_MS = 30_000;
89
- const DEFAULT_POLL_INTERVAL_MS = 2_000;
90
- const SETTLE_ATTEMPTS = 3;
91
- const LONGPOLL_WINDOW_MS = 25_000;
92
- export class CommissionAbortedError extends Error {
93
- commissionId;
94
- constructor(commissionId) {
95
- super(`Commission ${commissionId} was cancelled by the caller.`);
96
- this.name = "CommissionAbortedError";
97
- this.commissionId = commissionId;
98
- }
99
- }
100
- export class CommissionTimeoutError extends Error {
101
- commissionId;
102
- timeoutMs;
103
- constructor(commissionId, timeoutMs) {
104
- super(`Commission ${commissionId} did not complete within ${timeoutMs / 1000}s. ` +
105
- `The task may still be running in the background.`);
106
- this.name = "CommissionTimeoutError";
107
- this.commissionId = commissionId;
108
- this.timeoutMs = timeoutMs;
109
- }
110
- }
111
- const sleep = (ms, signal) => new Promise((resolve) => {
112
- const onAbort = () => resolve();
113
- signal?.addEventListener("abort", onAbort, { once: true });
114
- setTimeout(() => {
115
- signal?.removeEventListener("abort", onAbort);
116
- resolve();
117
- }, ms);
118
- });
119
- export async function pollUntilDone(client, id, opts = {}) {
120
- const timeoutMs = opts.timeoutMs ?? INTERACTIVE_TIMEOUT_MS;
121
- const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
122
- const settleMs = opts.settleMs ?? 0;
123
- const signal = opts.signal;
124
- const deadline = Date.now() + timeoutMs;
125
- while (true) {
126
- if (signal?.aborted)
127
- throw new CommissionAbortedError(id);
128
- const remaining = deadline - Date.now();
129
- if (remaining <= 0)
130
- throw new CommissionTimeoutError(id, timeoutMs);
131
- let result;
132
- try {
133
- result = await client.get(id, { waitMs: Math.min(remaining, LONGPOLL_WINDOW_MS), signal });
134
- }
135
- catch (err) {
136
- if (signal?.aborted)
137
- throw new CommissionAbortedError(id);
138
- throw err;
139
- }
140
- if (result.status === "failed")
141
- return result;
142
- if (result.status === "done") {
143
- const unsettled = (r) => !r.output?.trim() || (opts.expectArtifacts === true && !r.artifacts?.length);
144
- for (let i = 0; settleMs > 0 && i < SETTLE_ATTEMPTS && unsettled(result); i++) {
145
- await sleep(settleMs);
146
- result = await client.get(id);
147
- }
148
- return result;
149
- }
150
- await sleep(Math.min(pollIntervalMs, Math.max(0, deadline - Date.now())), signal);
151
- }
152
- }
1
+ import{createRequest as w,MoaApiError as h}from"./http.js";const T={queued:"pending",running:"running",succeeded:"done",failed:"failed",canceled:"failed"},M=["md","pdf","docx","pptx"],g=n=>{const t=n?.split(".").pop()?.toLowerCase();return t&&M.includes(t)?t:void 0};function E(n){if(!n?.length)return;const t=n.flatMap(e=>{const s=g(e.name);return!s||!(e.url||e.data)?[]:[{name:e.name??`document.${s}`,format:s,url:e.url,data:e.data,mime:e.contentType,sizeBytes:e.sizeBytes}]});return t.length?t:void 0}function I(n){return`
2
+
3
+ <deliverables>
4
+ Besides the text answer, save the finished deliverable into the artifacts/ directory of your working dir as: ${n.map(t=>`one .${t} file`).join(", ")}.
5
+ </deliverables>`}function x(n){let t=n.task;return n.context&&(t+=`
6
+
7
+ <context>
8
+ ${n.context}
9
+ </context>`),n.design&&(t+=`
10
+
11
+ <design>
12
+ ${n.design}
13
+ </design>`),n.artifacts?.length&&(t+=I(n.artifacts)),t}function _(n={}){const t=w(n);return{async create(e){const s=await t("/tasks",{method:"POST",body:{task:x(e),mode:"striker",...e.sessionId?{sessionId:e.sessionId}:{},...e.newSessionId?{newSessionId:e.newSessionId}:{}}});if(!s.id)throw new h("Moa did not return a task id",502);return s.id},async get(e,s){const c=s?.waitMs&&s.waitMs>0?`?waitMs=${Math.floor(s.waitMs)}`:"",o=await t(`/tasks/${encodeURIComponent(e)}${c}`,{signal:s?.signal}),i=o.status?T[o.status]:void 0;return{id:o.id??e,status:i??"running",output:o.result,error:o.error,costUsd:o.costUsd,numTurns:o.numTurns,sessionId:o.sessionId,artifacts:E(o.artifacts)}},async cancel(e){await t(`/tasks/${encodeURIComponent(e)}`,{method:"DELETE"})}}}const b=3e4,A=2e3,k=3,y=25e3;class m extends Error{commissionId;constructor(t){super(`Commission ${t} was cancelled by the caller.`),this.name="CommissionAbortedError",this.commissionId=t}}class C extends Error{commissionId;timeoutMs;constructor(t,e){super(`Commission ${t} did not complete within ${e/1e3}s. The task may still be running in the background.`),this.name="CommissionTimeoutError",this.commissionId=t,this.timeoutMs=e}}const f=(n,t)=>new Promise(e=>{const s=()=>e();t?.addEventListener("abort",s,{once:!0}),setTimeout(()=>{t?.removeEventListener("abort",s),e()},n)});async function p(n,t,e={}){const s=e.timeoutMs??b,c=e.pollIntervalMs??A,o=e.settleMs??0,i=e.signal,l=Date.now()+s;for(;;){if(i?.aborted)throw new m(t);const u=l-Date.now();if(u<=0)throw new C(t,s);let r;try{r=await n.get(t,{waitMs:Math.min(u,y),signal:i})}catch(d){throw i?.aborted?new m(t):d}if(r.status==="failed")return r;if(r.status==="done"){const d=a=>!a.output?.trim()||e.expectArtifacts===!0&&!a.artifacts?.length;for(let a=0;o>0&&a<k&&d(r);a++)await f(o),r=await n.get(t);return r}await f(Math.min(c,Math.max(0,l-Date.now())),i)}}export{m as CommissionAbortedError,C as CommissionTimeoutError,b as INTERACTIVE_TIMEOUT_MS,_ as createCommissionClient,p as pollUntilDone};