@spendgraph/tools 0.7.0 → 0.8.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.
- package/dist/builtin/index.d.ts +1 -0
- package/dist/builtin/index.js +1 -1
- package/dist/builtin/openalex/api.d.ts +8 -4
- package/dist/builtin/openalex/api.js +1 -1
- package/dist/builtin/openalex/index.d.ts +0 -1
- package/dist/builtin/openalex/index.js +1 -1
- package/dist/builtin/openalex/openalex.d.ts +30 -17
- package/dist/builtin/openalex/openalex.js +1 -1
- package/dist/builtin/paper-search/index.d.ts +1 -0
- package/dist/builtin/paper-search/index.js +1 -0
- package/dist/builtin/paper-search/paper-search.d.ts +38 -0
- package/dist/builtin/paper-search/paper-search.js +3 -0
- package/dist/builtin/polite.d.ts +1 -0
- package/dist/builtin/polite.js +1 -1
- package/dist/builtin/publish/publish.d.ts +1 -1
- package/dist/builtin/wikidata/wikidata.d.ts +3 -1
- package/dist/builtin/wikidata/wikidata.js +1 -1
- package/dist/bus/output.d.ts +0 -17
- package/dist/bus/output.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/papers/consensus/consensus.d.ts +12 -0
- package/dist/papers/consensus/consensus.js +1 -0
- package/dist/papers/consensus/index.d.ts +4 -0
- package/dist/papers/consensus/index.js +1 -0
- package/dist/papers/consensus/reply.d.ts +24 -0
- package/dist/papers/consensus/reply.js +1 -0
- package/dist/papers/every.d.ts +6 -0
- package/dist/papers/every.js +1 -0
- package/dist/papers/index.d.ts +11 -0
- package/dist/papers/index.js +1 -0
- package/dist/papers/openalex/index.d.ts +2 -0
- package/dist/papers/openalex/index.js +1 -0
- package/dist/papers/openalex/openalex.d.ts +8 -0
- package/dist/papers/openalex/openalex.js +1 -0
- package/dist/papers/papers.d.ts +54 -0
- package/dist/papers/papers.js +1 -0
- package/dist/{builtin/openalex → papers}/render.d.ts +1 -1
- package/dist/papers/render.js +4 -0
- package/dist/papers/semantic-scholar/index.d.ts +4 -0
- package/dist/papers/semantic-scholar/index.js +1 -0
- package/dist/papers/semantic-scholar/reply.d.ts +29 -0
- package/dist/papers/semantic-scholar/reply.js +1 -0
- package/dist/papers/semantic-scholar/semantic-scholar.d.ts +8 -0
- package/dist/papers/semantic-scholar/semantic-scholar.js +1 -0
- package/dist/search/every.d.ts +6 -0
- package/dist/search/every.js +4 -0
- package/dist/search/index.d.ts +4 -2
- package/dist/search/index.js +1 -1
- package/dist/search/search.d.ts +6 -0
- package/dist/search/searxng/searxng.js +3 -3
- package/dist/search/sonar/sonar.js +1 -1
- package/dist/search/tavily/tavily.js +1 -1
- package/dist/tool/tool.d.ts +4 -0
- package/dist/tool/tool.js +1 -1
- package/dist/types/tool.d.ts +13 -0
- package/docs/builtins.mdx +65 -4
- package/package.json +10 -4
- package/dist/agents/index.d.ts +0 -1
- package/dist/agents/index.js +0 -1
- package/dist/agents/moa/design.d.ts +0 -49
- package/dist/agents/moa/design.js +0 -5
- package/dist/agents/moa/http.d.ts +0 -34
- package/dist/agents/moa/http.js +0 -1
- package/dist/agents/moa/index.d.ts +0 -74
- package/dist/agents/moa/index.js +0 -1
- package/dist/agents/moa/moa.d.ts +0 -56
- package/dist/agents/moa/moa.js +0 -13
- package/dist/agents/moa/types.d.ts +0 -33
- package/dist/agents/moa/types.js +0 -0
- package/dist/builtin/openalex/render.js +0 -4
package/dist/search/search.d.ts
CHANGED
|
@@ -14,6 +14,10 @@ export interface SearchUsage {
|
|
|
14
14
|
reasoningTokens: number;
|
|
15
15
|
searchQueries: number;
|
|
16
16
|
}
|
|
17
|
+
export interface SearchFailure {
|
|
18
|
+
source: string;
|
|
19
|
+
reason: string;
|
|
20
|
+
}
|
|
17
21
|
/** What every searcher hands back, whoever did the searching. */
|
|
18
22
|
export interface Searched {
|
|
19
23
|
answer: string;
|
|
@@ -21,6 +25,7 @@ export interface Searched {
|
|
|
21
25
|
model: string;
|
|
22
26
|
truncated: boolean;
|
|
23
27
|
usage: SearchUsage;
|
|
28
|
+
failed?: SearchFailure[];
|
|
24
29
|
}
|
|
25
30
|
export interface SearchAsk {
|
|
26
31
|
query: string;
|
|
@@ -29,6 +34,7 @@ export interface SearchAsk {
|
|
|
29
34
|
}
|
|
30
35
|
export type Searcher = ((ask: SearchAsk) => Promise<Searched>) & {
|
|
31
36
|
domains?: readonly string[];
|
|
37
|
+
source?: string;
|
|
32
38
|
};
|
|
33
39
|
/** What every searcher is configured with; each adds only where its answers come from. */
|
|
34
40
|
export interface SearcherOptions {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{checkUrl as
|
|
1
|
+
import{checkUrl as x}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 A}from"../search.js";import{textOf as T}from"./text.js";const f="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
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,
|
|
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,d=t.pageChars??X,b={...H,...t.pages},O=y(t);async function p(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=>p(a,s)))]}async function w(n){const s=AbortSignal.timeout(u);let e=x(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=x(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 w(n.url);if(!s.ok)return n.snippet;const e=T((await s.text()).slice(0,L));return e?e.slice(0,d):n.snippet}catch{return n.snippet}}async function D(n){if(!R({url:n,snippet:""},t.domains))return null;try{const s=await w(n);return s.ok&&T((await s.text()).slice(0,L)).slice(0,d)||null}catch{return null}}const C=(n,s)=>({answer:s.slice(0,g),results:[{url:n}],model:f,truncated:s.length>g,usage:{...A,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 p(n,e),l=s==="deep"?await P(o,e):[o],_=z(l),m=_.slice(0,h),E=m.slice(0,b[s]),k=await Promise.all(E.map(M)),S=E.map(($,j)=>K($,k[j])).join(`
|
|
4
4
|
|
|
5
|
-
`);return{answer:
|
|
5
|
+
`);return{answer:S.slice(0,g),results:m.map(q),model:f,truncated:S.length>g||_.length>m.length,usage:{...A,searchQueries:l.length}}},{source:f,...t.domains?.length?{domains:[...t.domains]}:{}})}export{tt as searxng};
|
|
@@ -1 +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",
|
|
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="sonar",p="https://api.perplexity.ai/chat/completions",u=6e4,T=3e5,y=500;function O(e){if(!e.apiKey?.trim())throw new Error("sonar needs a Perplexity apiKey; it will not read one from the process.");const a=e.fetch??globalThis.fetch.bind(globalThis),i=m(e);return Object.assign(async({query:s,depth:o,recency:n})=>{const r=E[o],c=e.timeoutMs??(o==="deep"?T:u),t=await i(()=>a(p,{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,y);throw new Error(`Perplexity returned ${t.status} for ${r}: ${l}`)}return _(await t.json(),r,e.maxChars??d,e.maxResults??h)},{source:f,...e.domains?.length?{domains:[...e.domains]}:{}})}export{O as sonar};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
import{retrying as
|
|
1
|
+
import{retrying as m}from"../retry.js";import{DEFAULT_MAX_CHARS as u,DEFAULT_MAX_RESULTS as n}from"../search.js";import{readReply as _}from"./reply.js";const y="https://api.tavily.com/search",i="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 r=e.fetch??globalThis.fetch.bind(globalThis),s=m(e);return Object.assign(async({query:o,depth:c,recency:t})=>{const{search_depth:d,pages:h}=T[c],a=await s(()=>r(y,{method:"POST",signal:AbortSignal.timeout(e.timeoutMs??g),headers:{authorization:`Bearer ${e.apiKey}`,"content-type":"application/json"},body:JSON.stringify({query:o,search_depth:d,include_answer:!0,include_raw_content:!0,max_results:Math.min(h,e.maxResults??n),...t?{days:E[t]}:{},...e.domains?.length?{include_domains:e.domains}:{}})}));if(!a.ok){const l=(await a.text()).slice(0,p);throw new Error(`Tavily returned ${a.status}: ${l}`)}return _(await a.json(),i,e.maxChars??u,e.maxResults??n)},{source:i,...e.domains?.length?{domains:[...e.domains]}:{}})}export{S as tavily};
|
package/dist/tool/tool.d.ts
CHANGED
|
@@ -9,6 +9,10 @@ export interface ToolSpec<T extends readonly ArgSpec[]> {
|
|
|
9
9
|
trust?: Trust;
|
|
10
10
|
costOf?(value: unknown): number | undefined;
|
|
11
11
|
needs?: readonly string[];
|
|
12
|
+
wants?: {
|
|
13
|
+
name: string;
|
|
14
|
+
description: string;
|
|
15
|
+
};
|
|
12
16
|
run(args: ArgsOf<T>): Promise<unknown> | unknown;
|
|
13
17
|
}
|
|
14
18
|
/**
|
package/dist/tool/tool.js
CHANGED
|
@@ -1 +1 @@
|
|
|
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
|
+
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]}:{},...n.wants?{wants:{...n.wants}}:{},run:n.run}}export{d as tool};
|
package/dist/types/tool.d.ts
CHANGED
|
@@ -106,6 +106,19 @@ export interface Tool<Args = Record<string, unknown>> {
|
|
|
106
106
|
* where the words a caller actually types are declared.
|
|
107
107
|
*/
|
|
108
108
|
aliases?: string[];
|
|
109
|
+
/**
|
|
110
|
+
* A second, differently-shaped input the tool would rather be given, named.
|
|
111
|
+
*
|
|
112
|
+
* A stage hands every tool the one question it wrote. That suits a search
|
|
113
|
+
* engine and defeats a catalogue that matches on a title: measured against
|
|
114
|
+
* OpenAlex, a 280-character sub-question found nothing where the same ask as
|
|
115
|
+
* an 80-character phrase found ten papers. Declaring it here is what lets the
|
|
116
|
+
* stage be told to write one, without the stored wording naming any tool.
|
|
117
|
+
*/
|
|
118
|
+
wants?: {
|
|
119
|
+
name: string;
|
|
120
|
+
description: string;
|
|
121
|
+
};
|
|
109
122
|
/**
|
|
110
123
|
* Offered whatever selection decides — an escalation path, a refusal, where
|
|
111
124
|
* losing a similarity search is not an acceptable outcome.
|
package/docs/builtins.mdx
CHANGED
|
@@ -36,11 +36,10 @@ const bus = toolbus([
|
|
|
36
36
|
| `httpRequest()` | a GET or POST to hosts you named | local |
|
|
37
37
|
| `extract()` | the text of a file at a URL — pdf, docx, a page, md, txt | local |
|
|
38
38
|
| `codeExec()` | a short program, run wherever you say | sandbox |
|
|
39
|
-
| `writeDocument()` | a document agent that returns a rendered file | key |
|
|
40
39
|
| `publish()` | content out as a file at a public URL, through the app | key |
|
|
41
40
|
| `webSearch()` | a searched answer with its sources, from a searcher you pick | local or key |
|
|
42
41
|
| `wikidata()` | what Wikidata records about a thing, as text with every id named | local |
|
|
43
|
-
| `
|
|
42
|
+
| `paperSearch()` | published papers on a topic, as text with a DOI and a PDF where one is open, from a paper searcher you pick | local or key |
|
|
44
43
|
| `recall()` | what your memory holds about something, one note per row | key |
|
|
45
44
|
| `deepRecall()` | the same, walked across linked notes, with the evidence | key |
|
|
46
45
|
| `remember()` | one thing filed under a `type:name` context | key |
|
|
@@ -126,7 +125,66 @@ webSearch({ search: sonar({ apiKey, domains: ["gov.uk"] }), maxDepth: "pro" });
|
|
|
126
125
|
webSearch({ search: searxng({ baseUrl: "http://localhost:8080", language: "en" }), maxDepth: "deep" });
|
|
127
126
|
```
|
|
128
127
|
|
|
129
|
-
Both hand back the same shape — `answer`, `results`, `model`, `truncated`, `usage` — so whatever reads one reads the other; only where the answer came from and what it cost differ. The model chooses `query`, `depth` and `recency`; `domains`, engines and language are pinned on the searcher, so it cannot widen them. Pages the SearXNG searcher reads go through the same guard as `httpRequest`, so a private address is refused even when an engine offered it. A stock SearXNG answers the json format with a 403 until `settings.yml` lists it under `search.formats` and the limiter is off, and the error says so. `examples/search` runs both searchers directly.
|
|
128
|
+
Both hand back the same shape — `answer`, `results`, `model`, `truncated`, `usage` — so whatever reads one reads the other; only where the answer came from and what it cost differ. `everySearch([sonar, tavily, searxng])` is that shape used on itself: one searcher over several, asked at the same time. The model chooses `query`, `depth` and `recency`; `domains`, engines and language are pinned on the searcher, so it cannot widen them. Pages the SearXNG searcher reads go through the same guard as `httpRequest`, so a private address is refused even when an engine offered it. A stock SearXNG answers the json format with a 403 until `settings.yml` lists it under `search.formats` and the limiter is off, and the error says so. `examples/search` runs both searchers directly.
|
|
129
|
+
|
|
130
|
+
**`paperSearch`** is `webSearch` for the literature, and ships three searchers. `openAlexPapers` is [OpenAlex](https://openalex.org), free and keyless. `semanticScholar` is the [Semantic Scholar](https://www.semanticscholar.org/product/api) graph, and carries the `tldr` as each paper's takeaway. `consensus` is [Consensus](https://consensus.app), which needs a key and is the only one that says how a study was run — `rct`, `meta-analysis` — with its sample size and a one-line finding.
|
|
131
|
+
|
|
132
|
+
Keys, and what each is worth:
|
|
133
|
+
|
|
134
|
+
| | |
|
|
135
|
+
| --- | --- |
|
|
136
|
+
| `openAlexPapers` | optional, free, from [OpenAlex authentication](https://help.openalex.org/api/authentication/) — sent as a bearer token, never in the url. Basic use is keyless; a key is ten times the daily budget, and heavier use is pay as you go |
|
|
137
|
+
| `semanticScholar` | optional, free, from [the API key form](https://www.semanticscholar.org/product/api#api-key-form) — emailed after review, so not the same day. Keyless is 1000 req/s *shared with every unauthenticated caller on the internet*, which in practice means routine 429s; a key is 1 req/s that is yours |
|
|
138
|
+
| `consensus` | required, from a [Consensus](https://consensus.app) account |
|
|
139
|
+
|
|
140
|
+
A 429 from the keyless Semantic Scholar pool is not a failure to design around — under `everyPaper` it is one source that did not answer while the others did.
|
|
141
|
+
|
|
142
|
+
**What a search cost** rides back with it. OpenAlex prices every call in its own envelope — `meta.cost_usd` — and that becomes `costMicroUsd` on the `PapersFound`, in micro-USD like every other price here. A source that does not price its own calls takes `costMicroUsd` as an option instead, which is what your plan charges per search:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
everyPaper([openAlexPapers({ apiKey }), consensus({ apiKey, costMicroUsd: 2500 })]);
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
`everyPaper` sums what the sources that answered reported, and bills nothing for one that failed. `paper_search` declares `costOf`, so the number lands on the result as `costMicros` and `spentOn([...results])` adds a run up. Only sources that said are counted: a search with no price is unmeasured, not free, and those are different claims.
|
|
149
|
+
|
|
150
|
+
The tool returns `{ answer, results, costMicroUsd }` rather than a bare string. The model still reads only `answer` — `render()` has always unwrapped that shape — while `results` become the result's `sources`, one per paper, preferring an open PDF, then the source's own page, then `https://doi.org/…`.
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
import { paperSearch } from "@spendgraph/tools";
|
|
154
|
+
import { consensus, openAlexPapers, semanticScholar } from "@spendgraph/tools/papers";
|
|
155
|
+
|
|
156
|
+
paperSearch({ papers: openAlexPapers({ apiKey }) });
|
|
157
|
+
paperSearch({ papers: semanticScholar({ apiKey, fieldsOfStudy: ["Medicine"] }), maxResults: 5 });
|
|
158
|
+
paperSearch({ papers: consensus({ apiKey, studyTypes: ["rct"], excludePreprints: true }) });
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
All three hand back the same `Paper` — title, year, authors, venue, citations, DOI, an open PDF, a study type and sample size where the source knows them, a takeaway, and the abstract — so whatever reads one reads the others. The model chooses `query`, `year_min`, `year_max`, `min_citations` and `open_access`; the study filters, fields of study and journals are pinned on the searcher, so it cannot widen them. `openalex()` is the preset for the keyless case, and is `paperSearch({ papers: openAlexPapers(opts) })` with nothing added.
|
|
162
|
+
|
|
163
|
+
**Two or three sources at once** is `everySearch` in `@spendgraph/tools/search` and `everyPaper` in `@spendgraph/tools/papers`. Each takes a list, returns one searcher of the same type, and asks all of them at the same time.
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
import { paperSearch, webSearch } from "@spendgraph/tools";
|
|
167
|
+
import { consensus, everyPaper, openAlexPapers, semanticScholar } from "@spendgraph/tools/papers";
|
|
168
|
+
import { everySearch, searxng, sonar } from "@spendgraph/tools/search";
|
|
169
|
+
|
|
170
|
+
paperSearch({ papers: everyPaper([consensus({ apiKey }), semanticScholar(), openAlexPapers()]) });
|
|
171
|
+
webSearch({ search: everySearch([sonar({ apiKey }), searxng({ baseUrl })]) });
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
One tool, not three. `busgroup` is the other way to hold several providers — a bus each, `namespace: true`, and the model sees `consensus__paper_search` beside `s2__paper_search` — and it is the right shape when the buses do *different* things, because a name there has to say which one runs. These do the same thing: same ask in, same shape out. Interchangeable by construction is a fan-out, not a routing decision the model has any basis to make, and three namespaced copies spend three shortlist slots on one capability.
|
|
175
|
+
|
|
176
|
+
What the merge does with what comes back:
|
|
177
|
+
|
|
178
|
+
| | |
|
|
179
|
+
| --- | --- |
|
|
180
|
+
| order | round-robin — first from each source, then second from each, so no one corpus owns the top |
|
|
181
|
+
| the same paper twice | folded on DOI, else on the title with punctuation and case removed |
|
|
182
|
+
| a fold | fills gaps rather than dropping one: Consensus' `studyType` survives alongside Semantic Scholar's abstract, and the higher citation count wins |
|
|
183
|
+
| a source that fails | named in `failed`, and the rest still answer; `paper_search` prints `openalex did not answer: …` under the list |
|
|
184
|
+
| every source failing | the call fails, quoting what each one said |
|
|
185
|
+
| the bill | three providers per call, not one — a fan-out costs what its parts cost |
|
|
186
|
+
|
|
187
|
+
`everySearch` puts each answer under `# via <model>` so a quote stays attributable — `via` because a page's own text routinely carries an `#` heading of its own, and the marker has to be the one line that cannot be mistaken for content, unions the links, and sums all five token lines. It keeps `domains` only when *every* searcher is pinned, since one unpinned searcher means the answer is no longer restricted.
|
|
130
188
|
|
|
131
189
|
**`codeExec`** runs a short program and hands back what it printed — for work an arithmetic expression cannot state: sorting, filtering, joining, parsing. It takes a **required** `sandbox` and has no default, because a default would mean running model-written code in the process that called it. That is what `calculate` exists to avoid, and shipping one here would undo it.
|
|
132
190
|
|
|
@@ -151,6 +209,9 @@ codeExec({ sandbox, language: "python", timeoutMs: 10_000 });
|
|
|
151
209
|
|
|
152
210
|
Anything satisfying `execute` works — a hosted sandbox, a container, a WASM runtime. A failed run comes back with `ok: false` rather than throwing, so the model reads `stderr` and corrects its own code instead of getting an opaque tool failure.
|
|
153
211
|
|
|
154
|
-
|
|
212
|
+
Commissioning a document agent used to live here as `writeDocument`. It now lives in
|
|
213
|
+
`@spendgraph/vigil/agents` as `moaAgent()`, because the work takes minutes and a tool has to
|
|
214
|
+
return now: a run parks on the agent and picks itself back up, rather than holding a process
|
|
215
|
+
open for ten minutes.
|
|
155
216
|
|
|
156
217
|
**`httpRequest`** takes a **required** allowlist and has no default. It is https only, refuses private, loopback, link-local and `.internal`/`.local` hosts **even if you list them**, and sets headers itself so a credential is never something the model can redirect. If the guard feels heavy, that is the correct amount of heavy: this is the one builtin that can exfiltrate. `allow: ["*"]` opens it to every public host, for the one case where the tool is a reader of the open web rather than a client of your own APIs; the private-address and https refusals still hold.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Declare a tool once, offer the right few, and record what was called.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -37,6 +37,10 @@
|
|
|
37
37
|
"types": "./dist/wire/index.d.ts",
|
|
38
38
|
"import": "./dist/wire/index.js"
|
|
39
39
|
},
|
|
40
|
+
"./papers": {
|
|
41
|
+
"types": "./dist/papers/index.d.ts",
|
|
42
|
+
"import": "./dist/papers/index.js"
|
|
43
|
+
},
|
|
40
44
|
"./search": {
|
|
41
45
|
"types": "./dist/search/index.d.ts",
|
|
42
46
|
"import": "./dist/search/index.js"
|
|
@@ -57,13 +61,14 @@
|
|
|
57
61
|
"README.md"
|
|
58
62
|
],
|
|
59
63
|
"dependencies": {
|
|
60
|
-
"@spendgraph/sdk": "^0.
|
|
64
|
+
"@spendgraph/sdk": "^0.8.0",
|
|
61
65
|
"jszip": "^3.10.2",
|
|
62
66
|
"unpdf": "^1.8.1"
|
|
63
67
|
},
|
|
64
68
|
"devDependencies": {
|
|
65
69
|
"@locusgraph/client": "^0.8.1",
|
|
66
|
-
"@spendgraph/
|
|
70
|
+
"@spendgraph/config": "0.8.0",
|
|
71
|
+
"@spendgraph/prompt": "^0.8.0",
|
|
67
72
|
"pdf-lib": "^1.17.1",
|
|
68
73
|
"typescript": "^5"
|
|
69
74
|
},
|
|
@@ -75,7 +80,8 @@
|
|
|
75
80
|
},
|
|
76
81
|
"scripts": {
|
|
77
82
|
"build": "rm -rf dist && tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments && node ../../scripts/minify.mjs dist",
|
|
83
|
+
"examples": "pnpm run build && node examples/02-select/01-shortlist.mjs",
|
|
78
84
|
"test": "vitest run",
|
|
79
|
-
"
|
|
85
|
+
"typecheck": "tsc -p tsconfig.tests.json"
|
|
80
86
|
}
|
|
81
87
|
}
|
package/dist/agents/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { type ArtifactFormat, DEFAULT_DESIGN, type DesignGuide, MoaAbortedError, type MoaClient, MoaTimeoutError, mergeDesign, renderDesign, type WriteDocumentOptions, writeDocument, } from "./moa/index.js";
|
package/dist/agents/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{DEFAULT_DESIGN as o,MoaAbortedError as t,MoaTimeoutError as D,mergeDesign as i,renderDesign as m,writeDocument as n}from"./moa/index.js";export{o as DEFAULT_DESIGN,t as MoaAbortedError,D as MoaTimeoutError,i as mergeDesign,m as renderDesign,n as writeDocument};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import type { ArtifactFormat } from "./types.js";
|
|
2
|
-
/**
|
|
3
|
-
* The house style a commissioned document is built to.
|
|
4
|
-
*
|
|
5
|
-
* Every field is optional because a caller overrides only what its brand
|
|
6
|
-
* actually fixes; the rest falls back to {@link DEFAULT_DESIGN}.
|
|
7
|
-
*/
|
|
8
|
-
export interface DesignGuide {
|
|
9
|
-
/** What the style is called, so the document can say which one it followed. */
|
|
10
|
-
name?: string;
|
|
11
|
-
fonts?: {
|
|
12
|
-
heading?: string;
|
|
13
|
-
body?: string;
|
|
14
|
-
mono?: string;
|
|
15
|
-
};
|
|
16
|
-
palette?: {
|
|
17
|
-
primary?: string;
|
|
18
|
-
accent?: string;
|
|
19
|
-
text?: string;
|
|
20
|
-
muted?: string;
|
|
21
|
-
surface?: string;
|
|
22
|
-
};
|
|
23
|
-
/** Page size, margins, grid, density. */
|
|
24
|
-
layout?: string;
|
|
25
|
-
/** How a figure earns its place and what it may not do. */
|
|
26
|
-
charts?: string;
|
|
27
|
-
/** How the writing sounds. */
|
|
28
|
-
tone?: string;
|
|
29
|
-
/** Fetchable logo, dropped on the cover and nowhere else. */
|
|
30
|
-
logoUrl?: string;
|
|
31
|
-
/** Hard rules, appended to the defaults rather than replacing them. */
|
|
32
|
-
rules?: readonly string[];
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* What a document looks like when nobody has said.
|
|
36
|
-
*
|
|
37
|
-
* Chosen to be defensible rather than distinctive: one typeface, one accent,
|
|
38
|
-
* real whitespace, and figures that carry a single message.
|
|
39
|
-
*/
|
|
40
|
-
export declare const DEFAULT_DESIGN: DesignGuide;
|
|
41
|
-
/**
|
|
42
|
-
* A caller's guide over the defaults: named fields win, `rules` accumulate.
|
|
43
|
-
*
|
|
44
|
-
* Rules append because they are constraints — a brand adding "never use red"
|
|
45
|
-
* means it as well as the defaults, not instead of them.
|
|
46
|
-
*/
|
|
47
|
-
export declare function mergeDesign(base: DesignGuide, over?: DesignGuide): DesignGuide;
|
|
48
|
-
/** The guide as the block Moa reads, since its artifact pipeline is prompt-driven. */
|
|
49
|
-
export declare function renderDesign(guide: DesignGuide, format: ArtifactFormat, notes?: string): string;
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
const c={name:"Spendgraph house style",fonts:{heading:"Inter",body:"Inter",mono:"JetBrains Mono"},palette:{primary:"#0F172A",accent:"#2563EB",text:"#111827",muted:"#64748B",surface:"#FFFFFF"},layout:"A4 portrait, 20mm margins, a single text column at 60-75 characters, a 4pt spacing scale, and one heading scale used consistently (28/20/16/12). Whitespace is the design.",charts:"One message per figure, stated in its caption. Label series directly where there are three or fewer. No 3D, no gradients, no gridline heavier than the data, no pie chart past three slices, and axes that start at zero unless the caption says why not.",tone:"Plain and specific. Lead with the finding, then the evidence. No filler adjectives, no throat-clearing, no sentence that survives being deleted.",rules:["A cover carrying the title, date and author, and no other ornament.","Every number traceable to something in the source material; invent nothing.","Tables for anything being compared; prose for anything being argued.","No stock imagery, no clip art, no placeholder text in the finished file.","Page numbers on every page after the cover."]},d={pdf:"Cover, then contents if it runs past four pages, then the body. Figures captioned and referenced from the text. Nothing straddles a page break: no orphan heading, no split table row.",pptx:"16:9. One idea per slide, and the title is that idea as a sentence \u2014 a claim, not a label. At most six lines of body per slide, one figure per slide at most, and everything you cut goes into the speaker notes. Close on decisions and next steps, not on 'thank you'.",docx:"Real Word styles for headings rather than manual bold, a table-of-contents field rather than a typed list, a header row on every table, and captions on figures.",md:"A title heading, ATX headings throughout, tables for comparisons, fenced code with a language tag, and no HTML."};function i(e,t){return e?t?{...e,...t}:e:t}function h(e,t){return t?{...e,...t,fonts:i(e.fonts,t.fonts),palette:i(e.palette,t.palette),rules:[...e.rules??[],...t.rules??[]]}:e}const a=(e,t)=>t?[`${e}: ${t}`]:[];function p(e,t,o){const{fonts:r,palette:n}=e;return[...a("Style",e.name),...a("Typefaces",[r?.heading&&`headings ${r.heading}`,r?.body&&`body ${r.body}`,r?.mono&&`code ${r.mono}`].filter(Boolean).join(", ")||void 0),...a("Palette",[n?.primary&&`primary ${n.primary}`,n?.accent&&`accent ${n.accent}`,n?.text&&`text ${n.text}`,n?.muted&&`muted ${n.muted}`,n?.surface&&`surface ${n.surface}`].filter(Boolean).join(", ")||void 0),...a("Layout",e.layout),...a("Figures",e.charts),...a("Voice",e.tone),...a("Logo",e.logoUrl),`Format craft (.${t}): ${d[t]}`,...e.rules?.length?[`Rules:
|
|
2
|
-
${e.rules.map(s=>`- ${s}`).join(`
|
|
3
|
-
`)}`]:[],...o?.trim()?[`Overrides for this document, which win over everything above:
|
|
4
|
-
${o.trim()}`]:[]].join(`
|
|
5
|
-
`)}export{c as DEFAULT_DESIGN,h as mergeDesign,p as renderDesign};
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/** Anything shaped like an `AbortSignal`, so a Worker's own signal fits too. */
|
|
2
|
-
export interface AbortSignalLike {
|
|
3
|
-
readonly aborted: boolean;
|
|
4
|
-
addEventListener(type: "abort", listener: () => void, opts?: {
|
|
5
|
-
once?: boolean;
|
|
6
|
-
}): void;
|
|
7
|
-
removeEventListener(type: "abort", listener: () => void): void;
|
|
8
|
-
}
|
|
9
|
-
/** Where Moa lives, unless a config says otherwise. */
|
|
10
|
-
export declare const MOA_BASE_URL = "https://api.fnmoa.com";
|
|
11
|
-
export interface MoaClientConfig {
|
|
12
|
-
/** Read from here rather than the environment, as every other builtin does. */
|
|
13
|
-
apiKey?: string;
|
|
14
|
-
baseUrl?: string;
|
|
15
|
-
/** Ceiling on one HTTP call, not on the commission. Default 60000. */
|
|
16
|
-
timeoutMs?: number;
|
|
17
|
-
/** Injected for tests. */
|
|
18
|
-
fetch?: typeof fetch;
|
|
19
|
-
}
|
|
20
|
-
/** A non-2xx from Moa, carrying the status so a caller can tell 4xx from 5xx. */
|
|
21
|
-
export declare class MoaApiError extends Error {
|
|
22
|
-
readonly status: number;
|
|
23
|
-
readonly detail?: string;
|
|
24
|
-
constructor(message: string, status: number, detail?: string);
|
|
25
|
-
}
|
|
26
|
-
interface RequestOptions {
|
|
27
|
-
method?: string;
|
|
28
|
-
body?: unknown;
|
|
29
|
-
signal?: AbortSignalLike;
|
|
30
|
-
timeoutMs?: number;
|
|
31
|
-
}
|
|
32
|
-
/** One `request<T>(path, opts)` against Moa, with auth, timeout and error shape settled. */
|
|
33
|
-
export declare function createRequest(config?: MoaClientConfig): <T>(path: string, opts?: RequestOptions) => Promise<T>;
|
|
34
|
-
export {};
|
package/dist/agents/moa/http.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
const f="https://api.fnmoa.com",M=6e4,_=500;class c extends Error{status;detail;constructor(t,s,n){super(n?`${t}: ${n}`:t),this.name="MoaApiError",this.status=s,this.detail=n}}function T(e){const t=e.cause;return t?.code??t?.message??e.message}function m(e,t){const s=new AbortController,n=()=>s.abort();let r=!1;const o=setTimeout(()=>{r=!0,n()},e);return t?.addEventListener("abort",n,{once:!0}),t?.aborted&&n(),{signal:s.signal,expired:()=>r,release:()=>{clearTimeout(o),t?.removeEventListener("abort",n)}}}function $(e={}){const t=e.fetch??globalThis.fetch.bind(globalThis),s=(e.baseUrl??f).replace(/\/+$/,"");return async function(r,o={}){const u=o.timeoutMs??e.timeoutMs??6e4,i=o.method??"GET",d=`${s}${r}`,{signal:E,expired:h,release:b}=m(u,o.signal);try{const a=await t(d,{method:i,signal:E,headers:{...e.apiKey?{authorization:`Bearer ${e.apiKey}`}:{},...o.body===void 0?{}:{"content-type":"application/json"}},...o.body===void 0?{}:{body:JSON.stringify(o.body)}}).catch(l=>{throw o.signal?.aborted?l:h()?new c(`Moa did not answer ${i} ${r}`,504,`${u}ms`):new c(`Moa is unreachable at ${d}`,0,T(l))});if(!a.ok)throw new c(`Moa returned ${a.status} for ${i} ${r}`,a.status,(await a.text()).slice(0,500));return a.status===204?void 0:await a.json()}finally{b()}}}export{f as MOA_BASE_URL,c as MoaApiError,$ as createRequest};
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { type DesignGuide } from "./design.js";
|
|
2
|
-
import type { AbortSignalLike } from "./http.js";
|
|
3
|
-
import { type MoaClient } from "./moa.js";
|
|
4
|
-
import type { ArtifactFormat } from "./types.js";
|
|
5
|
-
export interface WriteDocumentOptions {
|
|
6
|
-
/** Moa key. Required, and read from here rather than the environment. */
|
|
7
|
-
apiKey: string;
|
|
8
|
-
/** Defaults to `https://api.fnmoa.com`. */
|
|
9
|
-
baseUrl?: string;
|
|
10
|
-
/**
|
|
11
|
-
* House style laid over {@link DEFAULT_DESIGN}: named fields win, `rules`
|
|
12
|
-
* accumulate. This is where a brand goes, so the model cannot dilute it.
|
|
13
|
-
*/
|
|
14
|
-
design?: DesignGuide;
|
|
15
|
-
/** Formats this deployment will produce. Default all four. */
|
|
16
|
-
formats?: readonly ArtifactFormat[];
|
|
17
|
-
/** Give up waiting after this. Default 600000 — rendering is minutes, not seconds. */
|
|
18
|
-
timeoutMs?: number;
|
|
19
|
-
/** Stops the wait. With `cancelOnAbort`, stops the task too. */
|
|
20
|
-
signal?: AbortSignalLike;
|
|
21
|
-
/** Cancel the task server-side when `signal` fires. Default true. */
|
|
22
|
-
cancelOnAbort?: boolean;
|
|
23
|
-
/** Return artifact bytes inline. Default false: base64 of a PDF is not context. */
|
|
24
|
-
includeData?: boolean;
|
|
25
|
-
/** Longest write-up handed back. Default 4000 characters. */
|
|
26
|
-
maxChars?: number;
|
|
27
|
-
/** Injected for tests, and for anyone fronting Moa with their own transport. */
|
|
28
|
-
client?: MoaClient;
|
|
29
|
-
/** Injected for tests. */
|
|
30
|
-
fetch?: typeof fetch;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* A document agent, briefed in your house style.
|
|
34
|
-
*
|
|
35
|
-
* Style is the deployment's business and content is the model's: the guide
|
|
36
|
-
* comes from `options.design`, and `design_notes` may bend it for one document
|
|
37
|
-
* rather than replace it. `effect` is `destructive` because a second call is a
|
|
38
|
-
* second bill and a second set of files — never something to retry blindly.
|
|
39
|
-
*/
|
|
40
|
-
export declare function writeDocument(opts: WriteDocumentOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
|
|
41
|
-
readonly name: "brief";
|
|
42
|
-
readonly type: "text";
|
|
43
|
-
readonly required: true;
|
|
44
|
-
readonly maxLength: 8000;
|
|
45
|
-
readonly description: string;
|
|
46
|
-
}, {
|
|
47
|
-
readonly name: "format";
|
|
48
|
-
readonly type: "enum";
|
|
49
|
-
readonly required: true;
|
|
50
|
-
readonly options: readonly ["pdf", "pptx", "docx", "md"];
|
|
51
|
-
readonly description: "pptx for anything presented, pdf for anything sent, docx for anything edited.";
|
|
52
|
-
}, {
|
|
53
|
-
readonly name: "source";
|
|
54
|
-
readonly type: "text";
|
|
55
|
-
readonly required: false;
|
|
56
|
-
readonly maxLength: 40000;
|
|
57
|
-
readonly description: string;
|
|
58
|
-
}, {
|
|
59
|
-
readonly name: "design_notes";
|
|
60
|
-
readonly type: "text";
|
|
61
|
-
readonly required: false;
|
|
62
|
-
readonly maxLength: 2000;
|
|
63
|
-
readonly description: string;
|
|
64
|
-
}, {
|
|
65
|
-
readonly name: "session_id";
|
|
66
|
-
readonly type: "string";
|
|
67
|
-
readonly required: false;
|
|
68
|
-
readonly description: string;
|
|
69
|
-
}]>>;
|
|
70
|
-
export { DEFAULT_DESIGN, type DesignGuide, mergeDesign, renderDesign } from "./design.js";
|
|
71
|
-
export { MOA_BASE_URL, MoaApiError, type MoaClientConfig } from "./http.js";
|
|
72
|
-
export type { MoaClient, PollOptions } from "./moa.js";
|
|
73
|
-
export { createMoaClient, INTERACTIVE_TIMEOUT_MS, MoaAbortedError, MoaTimeoutError, pollUntilDone, } from "./moa.js";
|
|
74
|
-
export type { ArtifactFormat, MoaArtifact, MoaInput, MoaResult, MoaStatus, } from "./types.js";
|
package/dist/agents/moa/index.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{tool as f}from"../../tool/index.js";import{DEFAULT_DESIGN as l,mergeDesign as h,renderDesign as g}from"./design.js";import{createMoaClient as p,MoaAbortedError as w,MoaTimeoutError as y,pollUntilDone as M}from"./moa.js";const _=["pdf","pptx","docx","md"],E=6e5,x=2e3,T=4e3;function A(e,r){return`Commissions a document agent to write and render a finished file \u2014 ${e.map(t=>`.${t}`).join(", ")} \u2014 and hands back where to download it. Use it when the answer has to be a document somebody opens: a report, a deck, a proposal, a one-pager. Not for text you can simply write in your reply. It runs for minutes and it costs money, so brief it once and brief it fully \u2014 the agent cannot see this conversation, so every fact, figure and quote it must use goes in \`source\`. Design is already settled by ${r.name??"the house style"}; use \`design_notes\` only for what this one document must do differently.`}function U(e,r,t,d){const i=(e.artifacts??[]).map(o=>({name:o.name,format:o.format,url:o.url,mime:o.mime,sizeBytes:o.sizeBytes,...d&&o.data?{data:o.data}:{}}));return{taskId:e.id,sessionId:e.sessionId,status:"done",style:r,documents:i,delivered:i.length>0,writeUp:e.output?.slice(0,t),truncated:(e.output?.length??0)>t,turns:e.numTurns,costMicroUsd:e.costUsd===void 0?void 0:Math.round(e.costUsd*1e6)}}function b(e,r,t){return{taskId:e,status:"running",style:r,documents:[],delivered:!1,note:`Still rendering after ${Math.round(t.timeoutMs/1e3)}s. The task is not lost \u2014 say so rather than commissioning it again.`}}function L(e){if(!e.client&&!e.apiKey?.trim())throw new Error("writeDocument needs a Moa apiKey; it will not read one from the process.");const r=e.formats?.length?e.formats:_,t=h(l,e.design),d=e.maxChars??T,i=e.client??p({apiKey:e.apiKey,baseUrl:e.baseUrl,...e.fetch?{fetch:e.fetch}:{}});return f({name:"write_document",description:A(r,t),effect:"destructive",args:[{name:"brief",type:"text",required:!0,maxLength:8e3,description:"What to produce, who reads it, what it must cover and how long it runs. Write it as you would brief a person who knows the craft and nothing about this account."},{name:"format",type:"enum",required:!0,options:["pdf","pptx","docx","md"],description:"pptx for anything presented, pdf for anything sent, docx for anything edited."},{name:"source",type:"text",required:!1,maxLength:4e4,description:"The material it must build from \u2014 figures, findings, quotes, an outline \u2014 verbatim. Anything missing here the agent will either omit or invent."},{name:"design_notes",type:"text",required:!1,maxLength:2e3,description:"How this one document departs from the house style, in plain words. Leave it empty unless the user asked for something specific."},{name:"session_id",type:"string",required:!1,description:"Continue an earlier write_document instead of starting over \u2014 take it from that result's sessionId. Use it to revise a document or to recast one into another format."}],run:async({brief:o,format:s,source:u,design_notes:m,session_id:c})=>{if(!r.includes(s))throw new Error(`write_document was asked for .${s}, which this deployment does not produce (${r.map(n=>`.${n}`).join(", ")}).`);const a=await i.create({task:o,context:u,design:g(t,s,m),artifacts:[s],...c?{sessionId:c}:{}});try{const n=await M(i,a,{timeoutMs:e.timeoutMs??E,settleMs:x,expectArtifacts:!0,...e.signal?{signal:e.signal}:{}});if(n.status==="failed")throw new Error(`write_document: Moa task ${a} failed: ${n.error??"no reason given"}`);return U(n,t.name,d,e.includeData===!0)}catch(n){if(n instanceof w&&e.cancelOnAbort!==!1&&await i.cancel?.(a).catch(()=>{}),n instanceof y)return b(a,t.name,n);throw n}}})}import{DEFAULT_DESIGN as $,mergeDesign as q,renderDesign as C}from"./design.js";import{MOA_BASE_URL as O,MoaApiError as R}from"./http.js";import{createMoaClient as N,INTERACTIVE_TIMEOUT_MS as B,MoaAbortedError as j,MoaTimeoutError as z,pollUntilDone as G}from"./moa.js";export{$ as DEFAULT_DESIGN,B as INTERACTIVE_TIMEOUT_MS,O as MOA_BASE_URL,j as MoaAbortedError,R as MoaApiError,z as MoaTimeoutError,N as createMoaClient,q as mergeDesign,G as pollUntilDone,C as renderDesign,L as writeDocument};
|
package/dist/agents/moa/moa.d.ts
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import { type AbortSignalLike, type MoaClientConfig } from "./http.js";
|
|
2
|
-
import type { MoaInput, MoaResult } from "./types.js";
|
|
3
|
-
/**
|
|
4
|
-
* Moa's task surface — fire, then await.
|
|
5
|
-
*
|
|
6
|
-
* `create()` hands back an id immediately and `get()` reads it; pass `waitMs`
|
|
7
|
-
* (or use {@link pollUntilDone}) to long-poll, so one call is awaited rather
|
|
8
|
-
* than a loop tight-polling. `cancel()` is optional so a test stub stays small.
|
|
9
|
-
*/
|
|
10
|
-
export interface MoaClient {
|
|
11
|
-
create(input: MoaInput): Promise<string>;
|
|
12
|
-
get(id: string, opts?: {
|
|
13
|
-
waitMs?: number;
|
|
14
|
-
signal?: AbortSignalLike;
|
|
15
|
-
}): Promise<MoaResult>;
|
|
16
|
-
cancel?(id: string): Promise<void>;
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* HTTP {@link MoaClient} over Moa's task API (`POST /tasks` → `GET /tasks/:id`).
|
|
20
|
-
*
|
|
21
|
-
* The request body carries the ask as prose — Moa's artifact pipeline is
|
|
22
|
-
* prompt-driven and `POST /tasks` strips unknown fields, so context, design and
|
|
23
|
-
* deliverables are blocks on the task rather than parameters beside it.
|
|
24
|
-
*/
|
|
25
|
-
export declare function createMoaClient(config?: MoaClientConfig): MoaClient;
|
|
26
|
-
/** The sane ceiling for a chat-bound call. A rendered document wants minutes, not this. */
|
|
27
|
-
export declare const INTERACTIVE_TIMEOUT_MS = 30000;
|
|
28
|
-
/** Thrown by {@link pollUntilDone} when the caller's abort signal fires. */
|
|
29
|
-
export declare class MoaAbortedError extends Error {
|
|
30
|
-
readonly taskId: string;
|
|
31
|
-
constructor(taskId: string);
|
|
32
|
-
}
|
|
33
|
-
/** Thrown when the wait ran out. The task itself is still running server-side. */
|
|
34
|
-
export declare class MoaTimeoutError extends Error {
|
|
35
|
-
readonly taskId: string;
|
|
36
|
-
readonly timeoutMs: number;
|
|
37
|
-
constructor(taskId: string, timeoutMs: number);
|
|
38
|
-
}
|
|
39
|
-
export interface PollOptions {
|
|
40
|
-
timeoutMs?: number;
|
|
41
|
-
pollIntervalMs?: number;
|
|
42
|
-
/**
|
|
43
|
-
* Stops the wait and throws {@link MoaAbortedError}, cutting the held
|
|
44
|
-
* long-poll with it. The task keeps running — cancelling it is the caller's call.
|
|
45
|
-
*/
|
|
46
|
-
signal?: AbortSignalLike;
|
|
47
|
-
/**
|
|
48
|
-
* How long to wait before re-reading a done task whose result has not landed
|
|
49
|
-
* yet. Moa flips the status slightly before it writes. Default 0.
|
|
50
|
-
*/
|
|
51
|
-
settleMs?: number;
|
|
52
|
-
/** Keep settling while a done task carries no artifacts. Set it when files were asked for. */
|
|
53
|
-
expectArtifacts?: boolean;
|
|
54
|
-
}
|
|
55
|
-
/** Wait for a Moa task to settle, long-polling rather than tight-polling. */
|
|
56
|
-
export declare function pollUntilDone(client: MoaClient, id: string, opts?: PollOptions): Promise<MoaResult>;
|
package/dist/agents/moa/moa.js
DELETED
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import{createRequest as w,MoaApiError as M}from"./http.js";const h={queued:"pending",running:"running",succeeded:"done",failed:"failed",canceled:"failed"},T=["md","pdf","docx","pptx"],g=n=>{const t=n?.split(".").pop()?.toLowerCase();return t&&T.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 p(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 M("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)}`:"",r=await t(`/tasks/${encodeURIComponent(e)}${c}`,{signal:s?.signal}),a=r.status?h[r.status]:void 0;return{id:r.id??e,status:a??"running",output:r.result,error:r.error,costUsd:r.costUsd,numTurns:r.numTurns,sessionId:r.sessionId,artifacts:E(r.artifacts)}},async cancel(e){await t(`/tasks/${encodeURIComponent(e)}`,{method:"DELETE"})}}}const k=3e4,b=2e3,A=3,y=25e3;class f extends Error{taskId;constructor(t){super(`Moa task ${t} was cancelled by the caller.`),this.name="MoaAbortedError",this.taskId=t}}class S extends Error{taskId;timeoutMs;constructor(t,e){super(`Moa task ${t} did not complete within ${e/1e3}s. The task may still be running in the background.`),this.name="MoaTimeoutError",this.taskId=t,this.timeoutMs=e}}const m=(n,t)=>new Promise(e=>{const s=()=>e();t?.addEventListener("abort",s,{once:!0}),setTimeout(()=>{t?.removeEventListener("abort",s),e()},n)});async function L(n,t,e={}){const s=e.timeoutMs??k,c=e.pollIntervalMs??b,r=e.settleMs??0,a=e.signal,l=Date.now()+s;for(;;){if(a?.aborted)throw new f(t);const u=l-Date.now();if(u<=0)throw new S(t,s);let o;try{o=await n.get(t,{waitMs:Math.min(u,y),signal:a})}catch(d){throw a?.aborted?new f(t):d}if(o.status==="failed")return o;if(o.status==="done"){const d=i=>!i.output?.trim()||e.expectArtifacts===!0&&!i.artifacts?.length;for(let i=0;r>0&&i<A&&d(o);i++)await m(r),o=await n.get(t);return o}await m(Math.min(c,Math.max(0,l-Date.now())),a)}}export{k as INTERACTIVE_TIMEOUT_MS,f as MoaAbortedError,S as MoaTimeoutError,p as createMoaClient,L as pollUntilDone};
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/** A document format Moa can leave behind in `artifacts/`. */
|
|
2
|
-
export type ArtifactFormat = "md" | "pdf" | "docx" | "pptx";
|
|
3
|
-
/** Where a Moa task has got to. `pending` is queued, `failed` is settled. */
|
|
4
|
-
export type MoaStatus = "pending" | "running" | "done" | "failed";
|
|
5
|
-
/** One finished file, either downloadable or carried inline as base64. */
|
|
6
|
-
export interface MoaArtifact {
|
|
7
|
-
name: string;
|
|
8
|
-
format: ArtifactFormat;
|
|
9
|
-
url?: string;
|
|
10
|
-
data?: string;
|
|
11
|
-
mime?: string;
|
|
12
|
-
sizeBytes?: number;
|
|
13
|
-
}
|
|
14
|
-
/** What to hand Moa: the ask, the material it rests on, and the house style. */
|
|
15
|
-
export interface MoaInput {
|
|
16
|
-
task: string;
|
|
17
|
-
context?: string;
|
|
18
|
-
design?: string;
|
|
19
|
-
artifacts?: readonly ArtifactFormat[];
|
|
20
|
-
sessionId?: string;
|
|
21
|
-
newSessionId?: string;
|
|
22
|
-
}
|
|
23
|
-
/** A Moa task as it stands right now. */
|
|
24
|
-
export interface MoaResult {
|
|
25
|
-
id: string;
|
|
26
|
-
status: MoaStatus;
|
|
27
|
-
output?: string;
|
|
28
|
-
error?: string;
|
|
29
|
-
costUsd?: number;
|
|
30
|
-
numTurns?: number;
|
|
31
|
-
sessionId?: string;
|
|
32
|
-
artifacts?: MoaArtifact[];
|
|
33
|
-
}
|
package/dist/agents/moa/types.js
DELETED
|
File without changes
|
|
@@ -1,4 +0,0 @@
|
|
|
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};
|