@spendgraph/tools 0.8.0 → 0.8.1

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/docs/bogus.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "Bogus tools spendgraph docs",
2
+ title: "Bogus tools: spendgraph docs",
3
3
  description:
4
4
  "Twelve tools that answer the same way every time, built so the answer cannot be known without calling. Nothing reaches the network, so the noise in a run is the model's own.",
5
5
  };
@@ -22,13 +22,13 @@ Its own subpath, so nothing here reaches a production bundle by accident.
22
22
 
23
23
  ## For testing a pipeline, not a provider
24
24
 
25
- Nothing here touches the network. No key, no rate limit, no flake, no bill so whatever noise a run shows is the **model's own**, not the weather between you and a provider. Run the same question a hundred times and the tools are the one thing that did not move.
25
+ Nothing here touches the network. No key, no rate limit, no flake, no bill, so whatever noise a run shows is the **model's own**, not the weather between you and a provider. Run the same question a hundred times and the tools are the one thing that did not move.
26
26
 
27
27
  ## Every answer is unknowable without the call
28
28
 
29
29
  This is the part that makes them worth more than stubs. The Bogus League's clubs exist nowhere else. The dictionary's definitions are its own. The gold rate is not the market rate, and the prayer times match no published table. Each description says so.
30
30
 
31
- A model that answers from memory is therefore **caught**, not merely unhelpful there is no training data to be accidentally right from.
31
+ A model that answers from memory is therefore **caught**, not merely unhelpful: there is no training data to be accidentally right from.
32
32
 
33
33
  ## The receipt
34
34
 
@@ -44,13 +44,13 @@ import { receipt } from "@spendgraph/tools/bogus";
44
44
  receipt("my_notes", "wifi"); // "bogus-my_notes-9e01b3"
45
45
  ```
46
46
 
47
- Grep an answer for `bogus-` and you know which tools actually reached it. The model cannot invent a receipt it never saw, so provenance is **observed rather than claimed** which is a different and stronger thing than reading the step log, since the log records what your code ran and the receipt records what the answer was actually built from.
47
+ Grep an answer for `bogus-` and you know which tools actually reached it. The model cannot invent a receipt it never saw, so provenance is **observed rather than claimed**, which is a different and stronger thing than reading the step log, since the log records what your code ran and the receipt records what the answer was actually built from.
48
48
 
49
49
  ## The twelve
50
50
 
51
51
  | | | |
52
52
  | --- | --- | --- |
53
- | `my_notes` | the user's own notes passwords, plans, things written down | `readonly` |
53
+ | `my_notes` | the user's own notes: passwords, plans, things written down | `readonly` |
54
54
  | `my_calendar` | what is on for a given day | `readonly` |
55
55
  | `my_recipes` | only what they saved themselves, not a cookbook | `readonly` |
56
56
  | `dictionary` | definitions that are its own and not the ones in general use | `readonly` |
@@ -64,12 +64,12 @@ Grep an answer for `bogus-` and you know which tools actually reached it. The mo
64
64
  | `ask_mum` | always answers, rarely the question asked, forty-one minutes | `destructive` |
65
65
 
66
66
  <Callout tone="trap" title="They are fixtures, not fakes of anything real">
67
- Every answer is unknowable without the call that is what makes them useful for testing a loop and useless for testing a provider. Ship one by accident and the model will happily tell a customer what the gold price is.
67
+ Every answer is unknowable without the call: that is what makes them useful for testing a loop and useless for testing a provider. Ship one by accident and the model will happily tell a customer what the gold price is.
68
68
  </Callout>
69
69
 
70
70
  ## All three effects, on purpose
71
71
 
72
- Eight `readonly`, one `idempotent`, three `destructive`. That spread is why this is the fixture for a gate: you can assert a policy refuses `order_pizza`, retries `feed_cat`, and waves `my_notes` through without a bus that can spend real money.
72
+ Eight `readonly`, one `idempotent`, three `destructive`. That spread is why this is the fixture for a gate: you can assert a policy refuses `order_pizza`, retries `feed_cat`, and waves `my_notes` through, without a bus that can spend real money.
73
73
 
74
74
  ```ts
75
75
  const bus = toolbus(bogusTools());
@@ -82,4 +82,4 @@ bus.unannotated(); // []
82
82
 
83
83
  ## What they are not
84
84
 
85
- They are not a benchmark. A model doing well on the Bogus League says it can follow a description and call a tool, not that it is good the questions were written to be answerable only one way. Use them to prove the wiring, then measure quality on real work.
85
+ They are not a benchmark. A model doing well on the Bogus League says it can follow a description and call a tool, not that it is good: the questions were written to be answerable only one way. Use them to prove the wiring, then measure quality on real work.
package/docs/builtins.mdx CHANGED
@@ -1,12 +1,12 @@
1
1
  export const meta = {
2
- title: "Builtins spendgraph docs",
2
+ title: "Builtins: spendgraph docs",
3
3
  description:
4
- "Twenty-one factories, none registered for you this package's own advice is that more tools makes a model worse at picking, so they are opt-in one at a time.",
4
+ "Twenty-one factories, none registered for you: this package's own advice is that more tools makes a model worse at picking, so they are opt-in one at a time.",
5
5
  };
6
6
 
7
7
  # Builtins
8
8
 
9
- Twenty-one, each a factory, **none registered for you** this package's own advice is that more tools in front of a model makes it worse at picking, so they are opt-in one at a time. Six need a key, one needs a sandbox; the rest are local code.
9
+ Twenty-one, each a factory, **none registered for you**: this package's own advice is that more tools in front of a model makes it worse at picking, so they are opt-in one at a time. Six need a key, one needs a sandbox; the rest are local code.
10
10
 
11
11
  ```ts
12
12
  import { calculate, currentTime, escalate, finish, think, httpRequest } from "@spendgraph/tools";
@@ -34,7 +34,7 @@ const bus = toolbus([
34
34
  | `dates()` | the same, asked in words, for a caller that can only send a question | local |
35
35
  | `jsonQuery()` | one value out of a JSON document, by path | local |
36
36
  | `httpRequest()` | a GET or POST to hosts you named | local |
37
- | `extract()` | the text of a file at a URL pdf, docx, a page, md, txt | local |
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
39
  | `publish()` | content out as a file at a public URL, through the app | key |
40
40
  | `webSearch()` | a searched answer with its sources, from a searcher you pick | local or key |
@@ -94,12 +94,12 @@ const bus = toolbus(memories({ client: store }).tools);
94
94
  ```
95
95
 
96
96
  <Callout tone="trap" title="None of them are registered for you">
97
- Importing a builtin does nothing until it is in a bus. That is deliberate a package that silently added `http_request` to every agent would be a package that widened your blast radius on `npm update` but it does mean a tool you "added" and never see called is usually a tool you never put in the bus.
97
+ Importing a builtin does nothing until it is in a bus. That is deliberate, a package that silently added `http_request` to every agent would be a package that widened your blast radius on `npm update`, but it does mean a tool you "added" and never see called is usually a tool you never put in the bus.
98
98
  </Callout>
99
99
 
100
100
  ## A few worth reading about
101
101
 
102
- **`calculate`** parses the expression numbers, `+ - * / % ^`, brackets, and `sqrt abs round floor ceil ln log` and refuses everything else. `eval` on model output is arbitrary code execution with extra steps.
102
+ **`calculate`** parses the expression, numbers, `+ - * / % ^`, brackets, and `sqrt abs round floor ceil ln log`, and refuses everything else. `eval` on model output is arbitrary code execution with extra steps.
103
103
 
104
104
  It answers with the digits a double actually carries, so `0.1 + 0.2` is `0.3` and not `0.30000000000000004`. A whole number past 2^53 is refused rather than silently rounded, because the answer would then be confidently wrong about a figure the model can see in its own request.
105
105
 
@@ -107,13 +107,13 @@ It answers with the digits a double actually carries, so `0.1 + 0.2` is `0.3` an
107
107
 
108
108
  **`finish`** gives a loop a real stop condition. Without it a run ends when the model goes quiet, which is indistinguishable from it losing the thread. Read the result with `isFinished(result)` and `answerOf(result)`.
109
109
 
110
- **`decline`** is the other real ending, and the one usually left to prose. A model that cannot answer will otherwise write an apology, which reads as an answer to everything downstream it is scored, it is stored, and the rate of it is invisible. The tool takes a fixed `because` `unknowable`, `not_covered`, `out_of_scope`, `unsafe` so the rate of each is countable, and the two commonest want opposite fixes: one is the world, the other is your records. `isDeclined(result)` and `declinedFor(result)` read it back, and a loop that ends this way reports `stoppedBy: "declined"` with `declinedBecause` beside it.
110
+ **`decline`** is the other real ending, and the one usually left to prose. A model that cannot answer will otherwise write an apology, which reads as an answer to everything downstream, it is scored, it is stored, and the rate of it is invisible. The tool takes a fixed `because`, `unknowable`, `not_covered`, `out_of_scope`, `unsafe`, so the rate of each is countable, and the two commonest want opposite fixes: one is the world, the other is your records. `isDeclined(result)` and `declinedFor(result)` read it back, and a loop that ends this way reports `stoppedBy: "declined"` with `declinedBecause` beside it.
111
111
 
112
112
  **`current_time`** is the clock a model does not have. Asked for today's date it will otherwise produce its training cutoff, confidently. A wrong IANA zone throws rather than falling back, because a confidently wrong time is the failure this exists to stop.
113
113
 
114
- **`dateMath`** is what `current_time` leads to. Calendar months clamp rather than overflow a month after 31 January is 28 February, not 3 March, which is what `setMonth` gives and what every billing system disagrees with. `difference` counts boundaries crossed rather than dividing elapsed milliseconds, because a month is not 30 days. `dates()` is the same tool taking one `question` in one of three forms `days from 2026-03-01 to 2026-09-09`, `30 days after 2026-03-01`, `2 months before 2026-03-01` for a pipeline that hands every tool a question and nothing else.
114
+ **`dateMath`** is what `current_time` leads to. Calendar months clamp rather than overflow, a month after 31 January is 28 February, not 3 March, which is what `setMonth` gives and what every billing system disagrees with. `difference` counts boundaries crossed rather than dividing elapsed milliseconds, because a month is not 30 days. `dates()` is the same tool taking one `question` in one of three forms, `days from 2026-03-01 to 2026-09-09`, `30 days after 2026-03-01`, `2 months before 2026-03-01`, for a pipeline that hands every tool a question and nothing else.
115
115
 
116
- **`jsonQuery`** reads one value by path `invoices[0].total`, or `invoices[*].total` for every element. A path that matches nothing comes back `found: false` rather than `null`, which a model would otherwise read as the answer.
116
+ **`jsonQuery`** reads one value by path: `invoices[0].total`, or `invoices[*].total` for every element. A path that matches nothing comes back `found: false` rather than `null`, which a model would otherwise read as the answer.
117
117
 
118
118
  **`webSearch`** takes a searcher, and ships two. `sonar` is Perplexity, which searches, reads and writes the answer itself, for a key. `searxng` is a [SearXNG](https://docs.searxng.org) you run, which costs nothing and needs no key: it searches, then reads the first hits as text, five on `quick`, eight on `pro`, ten on `deep`. Handed a query that names a link, it reads that page instead.
119
119
 
@@ -125,21 +125,21 @@ webSearch({ search: sonar({ apiKey, domains: ["gov.uk"] }), maxDepth: "pro" });
125
125
  webSearch({ search: searxng({ baseUrl: "http://localhost:8080", language: "en" }), maxDepth: "deep" });
126
126
  ```
127
127
 
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.
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
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.
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
131
 
132
132
  Keys, and what each is worth:
133
133
 
134
134
  | | |
135
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 |
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
138
  | `consensus` | required, from a [Consensus](https://consensus.app) account |
139
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.
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
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:
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
143
 
144
144
  ```ts
145
145
  everyPaper([openAlexPapers({ apiKey }), consensus({ apiKey, costMicroUsd: 2500 })]);
@@ -147,7 +147,7 @@ everyPaper([openAlexPapers({ apiKey }), consensus({ apiKey, costMicroUsd: 2500 }
147
147
 
148
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
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/…`.
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
151
 
152
152
  ```ts
153
153
  import { paperSearch } from "@spendgraph/tools";
@@ -158,7 +158,7 @@ paperSearch({ papers: semanticScholar({ apiKey, fieldsOfStudy: ["Medicine"] }),
158
158
  paperSearch({ papers: consensus({ apiKey, studyTypes: ["rct"], excludePreprints: true }) });
159
159
  ```
160
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.
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
162
 
163
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
164
 
@@ -171,22 +171,22 @@ paperSearch({ papers: everyPaper([consensus({ apiKey }), semanticScholar(), open
171
171
  webSearch({ search: everySearch([sonar({ apiKey }), searxng({ baseUrl })]) });
172
172
  ```
173
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.
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
175
 
176
176
  What the merge does with what comes back:
177
177
 
178
178
  | | |
179
179
  | --- | --- |
180
- | order | round-robin first from each source, then second from each, so no one corpus owns the top |
180
+ | order | round-robin: first from each source, then second from each, so no one corpus owns the top |
181
181
  | the same paper twice | folded on DOI, else on the title with punctuation and case removed |
182
182
  | a fold | fills gaps rather than dropping one: Consensus' `studyType` survives alongside Semantic Scholar's abstract, and the higher citation count wins |
183
183
  | a source that fails | named in `failed`, and the rest still answer; `paper_search` prints `openalex did not answer: …` under the list |
184
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 |
185
+ | the bill | three providers per call, not one, a fan-out costs what its parts cost |
186
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.
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.
188
188
 
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.
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.
190
190
 
191
191
  Its `effect` is left unset on purpose: `readonly` would be a lie about arbitrary code and `destructive` a lie about sorting a list.
192
192
 
@@ -207,7 +207,7 @@ const sandbox: Sandbox = {
207
207
  codeExec({ sandbox, language: "python", timeoutMs: 10_000 });
208
208
  ```
209
209
 
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.
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.
211
211
 
212
212
  Commissioning a document agent used to live here as `writeDocument`. It now lives in
213
213
  `@spendgraph/vigil/agents` as `moaAgent()`, because the work takes minutes and a tool has to
package/docs/bus.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "The bus spendgraph docs",
2
+ title: "The bus: spendgraph docs",
3
3
  description:
4
4
  "toolbus holds them, picks the few worth sending, converts them per provider, and runs them without throwing. Selection is never applied for you, because hiding it would hide a cache invalidation.",
5
5
  };
@@ -20,7 +20,7 @@ toolbus([lookup, lookup]); // Error: Two tools are called "lookup_contract".
20
20
 
21
21
  Registering the second under the same key would leave `invoke` a coin flip. It fails at import instead.
22
22
 
23
- The bus also warns once, however many pairs when two tools describe themselves almost identically. Nothing fails at runtime when they do; the model just picks wrong more often, which is exactly why it is worth saying out loud. Switch it off with `{ warnOnOverlap: false }` once a catalogue is large enough that the warning is noise rather than news.
23
+ The bus also warns, once, however many pairs, when two tools describe themselves almost identically. Nothing fails at runtime when they do; the model just picks wrong more often, which is exactly why it is worth saying out loud. Switch it off with `{ warnOnOverlap: false }` once a catalogue is large enough that the warning is noise rather than news.
24
24
 
25
25
  ## Selection is never applied for you
26
26
 
@@ -31,7 +31,7 @@ bus.anthropic(bus.select(query)) // the shortlist
31
31
 
32
32
  The provider adapters send **everything** unless you narrow them, and that is deliberate. The `tools` array sits ahead of the messages in the cached prefix, so a list that changes every turn invalidates the system prompt along with it. Hiding a `select()` call inside `anthropic()` would hide that cost, so the shorter call is the expensive one on purpose.
33
33
 
34
- In practice you want [`trace()`](/docs/tools/turns), which selects once and records what it offered.
34
+ In practice you want [`trace()`](/spendgraph/tools/turns), which selects once and records what it offered.
35
35
 
36
36
  ## What it answers
37
37
 
@@ -59,16 +59,16 @@ result.status; // "completed" | "failed"
59
59
  ```
60
60
 
61
61
  <Callout tone="trap" title="`invoke` never throws, so `status` is the only thing that says it failed">
62
- A tool that throws comes back as a failed result the model reads and can act on, which is the point but code that only looks at `output` sees an empty string and carries on. Check `status === "failed"` before believing a result.
62
+ A tool that throws comes back as a failed result the model reads and can act on, which is the point, but code that only looks at `output` sees an empty string and carries on. Check `status === "failed"` before believing a result.
63
63
  </Callout>
64
64
 
65
65
  ## Gating on effect
66
66
 
67
- `destructive()` lists only the tools that **said** they are. Pair it with `unannotated()` before treating the rest as safe see [Effects](/docs/tools/effects).
67
+ `destructive()` lists only the tools that **said** they are. Pair it with `unannotated()` before treating the rest as safe. See [Effects](/spendgraph/tools/effects).
68
68
 
69
69
  ## Usage is the tiebreaker
70
70
 
71
- `usage()` counts what has actually run, and selection falls back on it when a query singles nothing out. A call that never reached the tool a bad name, arguments that did not validate is not counted, because it was not a use.
71
+ `usage()` counts what has actually run, and selection falls back on it when a query singles nothing out. A call that never reached the tool, a bad name, arguments that did not validate, is not counted, because it was not a use.
72
72
 
73
73
  ## Nesting: one bus of buses
74
74
 
@@ -83,7 +83,7 @@ const bus = busgroup([
83
83
  ]);
84
84
  ```
85
85
 
86
- **It returns a bus.** Everything above works on it unchanged, a member's `bus` may itself be a group, and the model still sees a flat list of tools grouping is a selection-time concern, never a wire-format one.
86
+ **It returns a bus.** Everything above works on it unchanged, a member's `bus` may itself be a group, and the model still sees a flat list of tools: grouping is a selection-time concern, never a wire-format one.
87
87
 
88
88
  Routing runs in three steps. A query that clearly names a domain consults only that domain. A query that names none consults every member and re-ranks the merged offers, so nesting is never worse than flat. A query that matches nothing anywhere spreads across members rather than letting array order decide.
89
89
 
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "Declaring a tool spendgraph docs",
2
+ title: "Declaring a tool: spendgraph docs",
3
3
  description:
4
4
  "Write `as const` and the handler types itself. Every check `tool` makes is something a provider either rejects opaquely or accepts and behaves oddly about.",
5
5
  };
@@ -21,7 +21,7 @@ const lookup = tool({
21
21
 
22
22
  ## Write `as const` and the handler types itself
23
23
 
24
- `ref` arrives a string, `limit` a number, `mode` narrowed to its two options, and the optional ones optional. Rename an argument and the handler stops compiling rather than being handed `undefined` halfway through an agent loop.
24
+ `ref` arrives a string, `limit` a number, `mode` narrowed to its two options, and the optional ones optional. Rename an argument and the handler stops compiling: rather than being handed `undefined` halfway through an agent loop.
25
25
 
26
26
  Without `as const`, inference falls back to nothing **silently**. That is the failure the package's own typecheck test exists to catch.
27
27
 
@@ -32,11 +32,11 @@ Arguments are validated, then filled in and coerced, before `run` sees them:
32
32
  - a **declared default** applies when the argument is absent;
33
33
  - a value arrives at the **type its declaration promises**.
34
34
 
35
- Providers hand tool arguments over as JSON a model wrote, so `"limit": "7"` is ordinary. It validates the checker reads it through the same coercion and it reaches `run` as the number `7`. `"loud": "false"` reaches it as `false`, not as a non-empty string that would make `if (args.loud)` run the branch the model asked it not to.
35
+ Providers hand tool arguments over as JSON a model wrote, so `"limit": "7"` is ordinary. It validates, the checker reads it through the same coercion, and it reaches `run` as the number `7`. `"loud": "false"` reaches it as `false`, not as a non-empty string that would make `if (args.loud)` run the branch the model asked it not to.
36
36
 
37
37
  ## `aliases` are matched, never sent
38
38
 
39
- The description is what the model reads. `aliases` are what [selection](/docs/tools/selecting) matches on, and they never leave the process:
39
+ The description is what the model reads. `aliases` are what [selection](/spendgraph/tools/selecting) matches on, and they never leave the process:
40
40
 
41
41
  ```ts
42
42
  tool({
@@ -68,7 +68,7 @@ Every check `tool` makes is something a provider either rejects opaquely or acce
68
68
  They fire at import, not on the first loop in production.
69
69
 
70
70
  <Callout tone="trap" title="Without `as const`, inference gives up quietly">
71
- Drop it and the handler's argument type falls back to `Record<string, unknown>` no error, no red squiggle, just every field typed `unknown` from then on. It is checked by a typecheck test in the package for exactly that reason.
71
+ Drop it and the handler's argument type falls back to `Record<string, unknown>`, no error, no red squiggle, just every field typed `unknown` from then on. It is checked by a typecheck test in the package for exactly that reason.
72
72
  </Callout>
73
73
 
74
74
  ## What a result carries
@@ -80,6 +80,6 @@ result.error; // why, when it failed
80
80
  result.latencyMs;
81
81
  ```
82
82
 
83
- A tool whose **return value** cannot be serialised a circular object, a `BigInt` is still a tool that **ran**. It comes back completed, carrying `[not recordable: …]`, rather than failed: reporting a completed action as a failure invites the agent to do it again.
83
+ A tool whose **return value** cannot be serialised, a circular object, a `BigInt`, is still a tool that **ran**. It comes back completed, carrying `[not recordable: …]`, rather than failed: reporting a completed action as a failure invites the agent to do it again.
84
84
 
85
85
  A thrown string keeps its message. Anything can be thrown, and reading only `Error.message` discards the one thing the tool said about itself.
package/docs/effects.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "Effects spendgraph docs",
2
+ title: "Effects: spendgraph docs",
3
3
  description:
4
4
  "Three values, not a taxonomy. There is no default, and unset is not readonly: a default would quietly label every tool written before this existed as safe.",
5
5
  };
@@ -20,7 +20,7 @@ Without it, `issue_refund` and `lookup_clause` are the same thing to a bus: two
20
20
  | `idempotent` | has an effect, but twice is the same as once |
21
21
  | `destructive` | neither, so never retry it on its own |
22
22
 
23
- Three values, not a taxonomy anything finer does not get filled in accurately.
23
+ Three values, not a taxonomy: anything finer does not get filled in accurately.
24
24
 
25
25
  ## There is no default, and unset is not `readonly`
26
26
 
@@ -32,18 +32,18 @@ bus.destructive() // ["issue_refund"]
32
32
  bus.unannotated() // ["send_email"]
33
33
  ```
34
34
 
35
- `destructive()` returns only the tools that said so, so a gate built on it alone lets an unlabelled one through. **`unannotated()` is how you find those** assert it is empty in a test and a new tool cannot join the bus unlabelled.
35
+ `destructive()` returns only the tools that said so, so a gate built on it alone lets an unlabelled one through. **`unannotated()` is how you find those**: assert it is empty in a test and a new tool cannot join the bus unlabelled.
36
36
 
37
37
  <Callout tone="trap" title="`readonly` is not the same as safe">
38
- Effect says what a call does outside the process; `trust` says whether what it returns was written by somebody outside. A web fetch is `readonly` and is also how an injected instruction arrives. A guard that wants "no destructive call after untrusted content came in" has to gate on `trust: "external"` the alternative is gating on the tool's *name*, which a rename turns off silently, and in the permissive direction.
38
+ Effect says what a call does outside the process; `trust` says whether what it returns was written by somebody outside. A web fetch is `readonly` and is also how an injected instruction arrives. A guard that wants "no destructive call after untrusted content came in" has to gate on `trust: "external"`: the alternative is gating on the tool's *name*, which a rename turns off silently, and in the permissive direction.
39
39
  </Callout>
40
40
 
41
41
  ## It survives selection
42
42
 
43
43
  The effect rides along on `declarations()` and survives `select()`, which is where a gate reads it. Every provider shape drops it: no API has a field for it, and an unknown key is either rejected or ignored.
44
44
 
45
- A turn answers about the tools **it** was offered `effectOf` is `undefined` both for a tool that never said and for one this turn never saw. A gate reading it decides what unknown means for it.
45
+ A turn answers about the tools **it** was offered: `effectOf` is `undefined` both for a tool that never said and for one this turn never saw. A gate reading it decides what unknown means for it.
46
46
 
47
47
  ## Three builtins leave it unset on purpose
48
48
 
49
- `http_request` is a GET or a POST depending on an argument, and one label cannot be true of both. `escalate` runs your `onEscalate`, and paging someone twice is not the same as paging them once. `code_exec` runs whatever the model wrote, which is neither safe nor known and `undefined` says exactly that, where either label would lie.
49
+ `http_request` is a GET or a POST depending on an argument, and one label cannot be true of both. `escalate` runs your `onEscalate`, and paging someone twice is not the same as paging them once. `code_exec` runs whatever the model wrote, which is neither safe nor known, and `undefined` says exactly that, where either label would lie.
package/docs/overview.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "Tools spendgraph docs",
2
+ title: "Tools: spendgraph docs",
3
3
  description:
4
4
  "Declare a tool once, offer the right few, and record what was called. A description and a function in one object, so a rename cannot become a runtime failure.",
5
5
  };
@@ -8,12 +8,14 @@ export const meta = {
8
8
 
9
9
  Fifty tools is 7,500 tokens spent before the user has typed anything, and a model given fifty picks worse than a model given five.
10
10
 
11
- Declare a tool once, offer the right few, and record what was called because a model that says it checked the ledger and did not is a claim only the steps can disprove.
11
+ Declare a tool once, offer the right few, and record what was called: because a model that says it checked the ledger and did not is a claim only the steps can disprove.
12
12
 
13
13
  ```sh
14
14
  npm install @spendgraph/tools
15
15
  ```
16
16
 
17
+ <ToolShortlist />
18
+
17
19
  Two things to learn, and twenty-two builtins you can ignore until you want one.
18
20
 
19
21
  ```ts
@@ -44,7 +46,7 @@ turn.record(); // { offeredTools, steps }
44
46
 
45
47
  ## Why one object
46
48
 
47
- A description the model reads and a function you wrote, in **one object**. Splitting them a schema here, a handler there, matched by string is how a rename becomes a runtime failure mid-loop.
49
+ A description the model reads and a function you wrote, in **one object**. Splitting them, a schema here, a handler there, matched by string, is how a rename becomes a runtime failure mid-loop.
48
50
 
49
51
  The description is the biggest lever on whether the model calls it correctly, and worth iterating on more than the code is.
50
52
 
@@ -59,18 +61,18 @@ await prompt.call(values, ({ messages, turn }) => llm.call(messages, { tools: tu
59
61
  ```
60
62
 
61
63
  <Callout tone="trap" title="Declaring a tool is not offering it">
62
- `toolbus()` holds everything your app can do; what the model sees each turn is a shortlist. Selection is never applied for you hand the model the whole bus and you are paying for every description on every turn, and making it worse at choosing.
64
+ `toolbus()` holds everything your app can do; what the model sees each turn is a shortlist. Selection is never applied for you: hand the model the whole bus and you are paying for every description on every turn, and making it worse at choosing.
63
65
  </Callout>
64
66
 
65
67
  ## Where to go next
66
68
 
67
69
  | | |
68
70
  | --- | --- |
69
- | [Declaring a tool](/docs/tools/declaring) | args, `as const`, and what is checked at import |
70
- | [A stored tool](/docs/tools/stored) | wording in the dashboard, handler in code |
71
- | [Effects](/docs/tools/effects) | what running one does to the world outside |
72
- | [The bus](/docs/tools/bus) | holds, picks, converts, runs and nests |
73
- | [Selecting](/docs/tools/selecting) | the shortlist, and why it is a shortlist |
74
- | [A turn](/docs/tools/turns) | offered, called, and what the rollout keeps |
75
- | [Builtins](/docs/tools/builtins) | thirteen factories, none registered for you |
76
- | [Bogus tools](/docs/tools/bogus) | twelve mocks for testing the pipeline, not the provider |
71
+ | [Declaring a tool](/spendgraph/tools/declaring) | args, `as const`, and what is checked at import |
72
+ | [A stored tool](/spendgraph/tools/stored) | wording in the dashboard, handler in code |
73
+ | [Effects](/spendgraph/tools/effects) | what running one does to the world outside |
74
+ | [The bus](/spendgraph/tools/bus) | holds, picks, converts, runs, and nests |
75
+ | [Selecting](/spendgraph/tools/selecting) | the shortlist, and why it is a shortlist |
76
+ | [A turn](/spendgraph/tools/turns) | offered, called, and what the rollout keeps |
77
+ | [Builtins](/spendgraph/tools/builtins) | thirteen factories, none registered for you |
78
+ | [Bogus tools](/spendgraph/tools/bogus) | twelve mocks for testing the pipeline, not the provider |
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "Selecting tools spendgraph docs",
2
+ title: "Selecting tools: spendgraph docs",
3
3
  description:
4
4
  "Fifty tools is 7.5k tokens before the user has said anything. `limit` is an accuracy budget, not a token one, and selection is keyword and usage on purpose.",
5
5
  };
@@ -22,23 +22,23 @@ Holding tools in one place is what makes selection possible. Fifty tools at roug
22
22
 
23
23
  ## `limit` is an accuracy budget
24
24
 
25
- More tools in front of a model makes it worse at picking the right one **regardless of context length**. Measured shortlists cluster around five to eight. Pinned tools count against it what is in front of the model is what costs accuracy, whoever put it there.
25
+ More tools in front of a model makes it worse at picking the right one **regardless of context length**. Measured shortlists cluster around five to eight. Pinned tools count against it: what is in front of the model is what costs accuracy, whoever put it there.
26
26
 
27
- A limit that is `NaN` is refused rather than clamped: `Math.max(1, NaN)` is `NaN`, and every length compares false against it, so the model would be offered **nothing** the one outcome selection exists to avoid.
27
+ A limit that is `NaN` is refused rather than clamped: `Math.max(1, NaN)` is `NaN`, and every length compares false against it, so the model would be offered **nothing**, the one outcome selection exists to avoid.
28
28
 
29
29
  ## Keyword and usage, deliberately
30
30
 
31
31
  No embedding provider, no extra call, no latency. It is also the baseline that says what embeddings would buy.
32
32
 
33
- Relevance comes before usage: at a shortlist of six, three previously-used tools take half the slots and push out the tool the query needs. Usage earns its place as the tiebreaker among equal matches, and as the fallback when nothing matches the case where an agent would otherwise be handed nothing.
33
+ Relevance comes before usage: at a shortlist of six, three previously-used tools take half the slots and push out the tool the query needs. Usage earns its place as the tiebreaker among equal matches, and as the fallback when nothing matches, the case where an agent would otherwise be handed nothing.
34
34
 
35
- Words are found in **any script**. A query with no ASCII letters used to produce no terms at all, so every request fell through to "the first six declared" the same six for a Japanese question as for a Russian one.
35
+ Words are found in **any script**. A query with no ASCII letters used to produce no terms at all, so every request fell through to "the first six declared", the same six for a Japanese question as for a Russian one.
36
36
 
37
37
  ## How a tool scores
38
38
 
39
39
  Whole words, never substrings. A term matching inside a longer word is noise, not signal: `me` sits inside `statement`, and at a few hundred tools that alone puts half the catalogue in every shortlist.
40
40
 
41
- Where the word appears decides its weight **name beats alias beats description**. A tool named `refund` is a stronger answer to a question about refunds than one that merely mentions the word in a paragraph.
41
+ Where the word appears decides its weight: **name beats alias beats description**. A tool named `refund` is a stronger answer to a question about refunds than one that merely mentions the word in a paragraph.
42
42
 
43
43
  Then each term is weighted by how **rare** it is across the bus. A word one tool in two thousand uses says far more than one four hundred of them share, and without that every term counts the same and common vocabulary decides the shortlist.
44
44
 
@@ -56,15 +56,15 @@ tool({
56
56
  });
57
57
  ```
58
58
 
59
- They are matched, never sent every provider shape drops them, exactly as it drops `effect`. Unlike an embedding, a bad match is fixed by editing one line.
59
+ They are matched, never sent: every provider shape drops them, exactly as it drops `effect`. Unlike an embedding, a bad match is fixed by editing one line.
60
60
 
61
61
  <Callout tone="trap" title="Pinned tools spend the budget too">
62
- `limit` counts pinned tools, so a shortlist of 5 with `finish` and `escalate` pinned leaves three slots for the query. A tool that needs a partner is dropped rather than offered alone an agent told to prefer `recall` and handed only `deep_recall` will reach for the one it does not have.
62
+ `limit` counts pinned tools, so a shortlist of 5 with `finish` and `escalate` pinned leaves three slots for the query. A tool that needs a partner is dropped rather than offered alone, an agent told to prefer `recall` and handed only `deep_recall` will reach for the one it does not have.
63
63
  </Callout>
64
64
 
65
65
  ## Past a few hundred tools
66
66
 
67
- Ranking, not speed, is what gives out first: selection stays near a millisecond at two thousand tools, but a coincidental match in an unrelated domain starts taking slots. [`busgroup`](/docs/tools/bus) gives each domain its own bus and routes between them.
67
+ Ranking, not speed, is what gives out first: selection stays near a millisecond at two thousand tools, but a coincidental match in an unrelated domain starts taking slots. [`busgroup`](/spendgraph/tools/bus) gives each domain its own bus and routes between them.
68
68
 
69
69
  ## `missing` catches a silent failure
70
70
 
@@ -76,4 +76,4 @@ bus.missing(["refund", "escalate"]); // the names this bus does not have
76
76
 
77
77
  ## Two tools that describe themselves the same way
78
78
 
79
- Two near-identical descriptions are worse than one tool: the model cannot tell them apart and picks wrong more often, with nothing failing. The bus says so at construction, once, however many pairs there are a line each buries whatever else the process said at startup and teaches people to switch the check off.
79
+ Two near-identical descriptions are worse than one tool: the model cannot tell them apart and picks wrong more often, with nothing failing. The bus says so at construction, once, however many pairs there are, a line each buries whatever else the process said at startup and teaches people to switch the check off.
package/docs/stored.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "A stored tool spendgraph docs",
2
+ title: "A stored tool: spendgraph docs",
3
3
  description:
4
4
  "The dashboard owns the description, your code owns the arguments and the handler, and bindTool checks the two against each other before the model ever sees it.",
5
5
  };
@@ -19,7 +19,7 @@ const lookup = await bindTool(sg, {
19
19
  });
20
20
  ```
21
21
 
22
- No `description` in that call. That is the point one source for the string the model reads, and it is the one you can edit without a release.
22
+ No `description` in that call. That is the point, one source for the string the model reads, and it is the one you can edit without a release.
23
23
 
24
24
  ## Who owns what
25
25
 
@@ -31,13 +31,13 @@ No `description` in that call. That is the point — one source for the string t
31
31
  pinned a deploy
32
32
  ```
33
33
 
34
- Arguments stay in code because the handler's parameter types are inferred from them. Pull those from the server and every `run` is handed a `Record<string, unknown>`, which gives up the one guarantee [declaring a tool](/docs/tools/declaring) exists to provide.
34
+ Arguments stay in code because the handler's parameter types are inferred from them. Pull those from the server and every `run` is handed a `Record<string, unknown>`, which gives up the one guarantee [declaring a tool](/spendgraph/tools/declaring) exists to provide.
35
35
 
36
36
  Write `as const` here exactly as you would for `tool()`. Without it inference falls back to nothing, and the drift check below becomes the only thing still catching a rename.
37
37
 
38
38
  ## Drift fails at bind, not mid-loop
39
39
 
40
- The stored arguments and the declared ones are compared on the way through, by **name, type and requiredness** the three a handler is written against.
40
+ The stored arguments and the declared ones are compared on the way through, by **name, type and requiredness**: the three a handler is written against.
41
41
 
42
42
  ```ts
43
43
  import { ToolDriftError, ToolNotStoredError } from "@spendgraph/tools";
@@ -58,21 +58,21 @@ try {
58
58
  "mode" is stored but not declared here
59
59
  ```
60
60
 
61
- A description or a bound that differs is **not** drift that is the dashboard being edited, which is the whole feature. A renamed argument is a handler about to be passed `undefined` halfway through an agent loop, so it throws.
61
+ A description or a bound that differs is **not** drift: that is the dashboard being edited, which is the whole feature. A renamed argument is a handler about to be passed `undefined` halfway through an agent loop, so it throws.
62
62
 
63
63
  <Callout tone="warn" title="Bind at startup, not per request">
64
- Every `bindTool` is a round trip to the API. Bind once when the process starts and hold the result, the same as you would a database connection a bind inside a request handler puts a network hop in front of every model call.
64
+ Every `bindTool` is a round trip to the API. Bind once when the process starts and hold the result, the same as you would a database connection, a bind inside a request handler puts a network hop in front of every model call.
65
65
  </Callout>
66
66
 
67
67
  <Callout tone="trap" title="A rename in the dashboard is a handler passed `undefined`">
68
- The wording is editable without a deploy; the argument names are not. `bindTool` compares them at bind time and refuses a deploy that fails is recoverable, and the same rename noticed mid-loop is a handler halfway through somebody's refund reading an argument that is not there.
68
+ The wording is editable without a deploy; the argument names are not. `bindTool` compares them at bind time and refuses: a deploy that fails is recoverable, and the same rename noticed mid-loop is a handler halfway through somebody's refund reading an argument that is not there.
69
69
  </Callout>
70
70
 
71
71
  ## When nothing is stored yet
72
72
 
73
73
  `ToolNotStoredError` is the ordinary case of "you have not written this one in the dashboard yet", not an exotic failure. It exists so a 404 from three layers down arrives as a sentence naming the tool.
74
74
 
75
- Create the row first `sg.tools.create` on the [client](/docs/sdk/client), or the Tools page in the dashboard then bind to it.
75
+ Create the row first, `sg.tools.create` on the [client](/spendgraph/sdk/client), or the Tools page in the dashboard, then bind to it.
76
76
 
77
77
  ## It is an ordinary tool afterwards
78
78
 
package/docs/turns.mdx CHANGED
@@ -1,5 +1,5 @@
1
1
  export const meta = {
2
- title: "A turn spendgraph docs",
2
+ title: "A turn: spendgraph docs",
3
3
  description:
4
4
  "A turn is selected once and remembers what it offered. Without that a record cannot tell 'chose not to' from 'was never offered', which are opposite bugs.",
5
5
  };
@@ -19,7 +19,7 @@ A turn is **selected once** and remembers what it offered. A trace cannot disagr
19
19
 
20
20
  ## Offered is recorded, not just called
21
21
 
22
- Which tools were offered is recorded as well as which were called. Without it a record cannot tell **"chose not to"** from **"was never offered"** which are opposite bugs, and look identical in a log that only lists calls.
22
+ Which tools were offered is recorded as well as which were called. Without it a record cannot tell **"chose not to"** from **"was never offered"**, which are opposite bugs, and look identical in a log that only lists calls.
23
23
 
24
24
  ## What a turn answers
25
25
 
@@ -34,17 +34,17 @@ Which tools were offered is recorded as well as which were called. Without it a
34
34
 
35
35
  ## Never throwing is the point
36
36
 
37
- A tool that fails is an **outcome the agent can react to** and a step worth recording, not an exception that ends the run the same argument `status` and `error` exist on a rollout for.
37
+ A tool that fails is an **outcome the agent can react to** and a step worth recording, not an exception that ends the run, the same argument `status` and `error` exist on a rollout for.
38
38
 
39
39
  A tool the bus does not have is a failure that says what was offered, since the usual cause is a hallucinated name.
40
40
 
41
41
  <Callout tone="trap" title="`undefined` means two different things">
42
- A tool that was offered and stayed quiet, and a tool this turn never offered, both answer `undefined`. `offeredTools` is what tells them apart which is the difference between a model that declined to look something up and a model that could not.
42
+ A tool that was offered and stayed quiet, and a tool this turn never offered, both answer `undefined`. `offeredTools` is what tells them apart, which is the difference between a model that declined to look something up and a model that could not.
43
43
  </Callout>
44
44
 
45
45
  ## Usage drives the tiebreaker
46
46
 
47
- `bus.usage()` counts what has actually been called, which is what selection falls back on when a query singles out nothing. A call that never reached the tool a name that does not exist, arguments that did not validate is not counted, because it was not a use.
47
+ `bus.usage()` counts what has actually been called, which is what selection falls back on when a query singles out nothing. A call that never reached the tool: a name that does not exist, arguments that did not validate, is not counted, because it was not a use.
48
48
 
49
49
  ## With a prompt
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spendgraph/tools",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
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": {
@@ -61,14 +61,14 @@
61
61
  "README.md"
62
62
  ],
63
63
  "dependencies": {
64
- "@spendgraph/sdk": "^0.8.0",
64
+ "@spendgraph/sdk": "^0.8.1",
65
65
  "jszip": "^3.10.2",
66
66
  "unpdf": "^1.8.1"
67
67
  },
68
68
  "devDependencies": {
69
69
  "@locusgraph/client": "^0.8.1",
70
- "@spendgraph/config": "0.8.0",
71
- "@spendgraph/prompt": "^0.8.0",
70
+ "@spendgraph/config": "0.8.1",
71
+ "@spendgraph/prompt": "^0.8.1",
72
72
  "pdf-lib": "^1.17.1",
73
73
  "typescript": "^5"
74
74
  },