@spendgraph/tools 0.2.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/README.md +283 -0
- package/dist/builtin/calculate/calculate.d.ts +13 -0
- package/dist/builtin/calculate/calculate.js +27 -0
- package/dist/builtin/calculate/index.d.ts +2 -0
- package/dist/builtin/calculate/index.js +2 -0
- package/dist/builtin/calculate/parse.d.ts +8 -0
- package/dist/builtin/calculate/parse.js +144 -0
- package/dist/builtin/clock/clock.d.ts +27 -0
- package/dist/builtin/clock/clock.js +76 -0
- package/dist/builtin/clock/index.d.ts +1 -0
- package/dist/builtin/clock/index.js +1 -0
- package/dist/builtin/date/date.d.ts +32 -0
- package/dist/builtin/date/date.js +47 -0
- package/dist/builtin/date/index.d.ts +2 -0
- package/dist/builtin/date/index.js +2 -0
- package/dist/builtin/date/shift.d.ts +12 -0
- package/dist/builtin/date/shift.js +62 -0
- package/dist/builtin/deep-recall/deep-recall.d.ts +27 -0
- package/dist/builtin/deep-recall/deep-recall.js +51 -0
- package/dist/builtin/deep-recall/index.d.ts +1 -0
- package/dist/builtin/deep-recall/index.js +1 -0
- package/dist/builtin/escalate/escalate.d.ts +46 -0
- package/dist/builtin/escalate/escalate.js +29 -0
- package/dist/builtin/escalate/index.d.ts +1 -0
- package/dist/builtin/escalate/index.js +1 -0
- package/dist/builtin/finish/finish.d.ts +27 -0
- package/dist/builtin/finish/finish.js +39 -0
- package/dist/builtin/finish/index.d.ts +1 -0
- package/dist/builtin/finish/index.js +1 -0
- package/dist/builtin/http/allow.d.ts +8 -0
- package/dist/builtin/http/allow.js +45 -0
- package/dist/builtin/http/http.d.ts +43 -0
- package/dist/builtin/http/http.js +51 -0
- package/dist/builtin/http/index.d.ts +2 -0
- package/dist/builtin/http/index.js +2 -0
- package/dist/builtin/index.d.ts +11 -0
- package/dist/builtin/index.js +11 -0
- package/dist/builtin/json/index.d.ts +2 -0
- package/dist/builtin/json/index.js +2 -0
- package/dist/builtin/json/json.d.ts +22 -0
- package/dist/builtin/json/json.js +44 -0
- package/dist/builtin/json/path.d.ts +8 -0
- package/dist/builtin/json/path.js +63 -0
- package/dist/builtin/think/index.d.ts +1 -0
- package/dist/builtin/think/index.js +1 -0
- package/dist/builtin/think/think.d.ts +13 -0
- package/dist/builtin/think/think.js +19 -0
- package/dist/builtin/web-search/index.d.ts +1 -0
- package/dist/builtin/web-search/index.js +1 -0
- package/dist/builtin/web-search/reply.d.ts +23 -0
- package/dist/builtin/web-search/reply.js +35 -0
- package/dist/builtin/web-search/web-search.d.ts +50 -0
- package/dist/builtin/web-search/web-search.js +93 -0
- package/dist/builtin/write-document/commission.d.ts +56 -0
- package/dist/builtin/write-document/commission.js +163 -0
- package/dist/builtin/write-document/design.d.ts +49 -0
- package/dist/builtin/write-document/design.js +99 -0
- package/dist/builtin/write-document/http.d.ts +34 -0
- package/dist/builtin/write-document/http.js +74 -0
- package/dist/builtin/write-document/index.d.ts +6 -0
- package/dist/builtin/write-document/index.js +4 -0
- package/dist/builtin/write-document/types.d.ts +33 -0
- package/dist/builtin/write-document/types.js +1 -0
- package/dist/builtin/write-document/write-document.d.ts +69 -0
- package/dist/builtin/write-document/write-document.js +150 -0
- package/dist/bus/bus.d.ts +76 -0
- package/dist/bus/bus.js +103 -0
- package/dist/bus/index.d.ts +3 -0
- package/dist/bus/index.js +2 -0
- package/dist/bus/invoke.d.ts +9 -0
- package/dist/bus/invoke.js +50 -0
- package/dist/bus/options.d.ts +22 -0
- package/dist/bus/options.js +1 -0
- package/dist/bus/overlap.d.ts +12 -0
- package/dist/bus/overlap.js +43 -0
- package/dist/bus/score.d.ts +9 -0
- package/dist/bus/score.js +19 -0
- package/dist/bus/select.d.ts +18 -0
- package/dist/bus/select.js +71 -0
- package/dist/bus/step.d.ts +4 -0
- package/dist/bus/step.js +12 -0
- package/dist/bus/trace.d.ts +42 -0
- package/dist/bus/trace.js +34 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +3 -0
- package/dist/internals.d.ts +16 -0
- package/dist/internals.js +16 -0
- package/dist/tool/bind.d.ts +42 -0
- package/dist/tool/bind.js +89 -0
- package/dist/tool/index.d.ts +4 -0
- package/dist/tool/index.js +2 -0
- package/dist/tool/tool.d.ts +20 -0
- package/dist/tool/tool.js +62 -0
- package/dist/types/index.d.ts +2 -0
- package/dist/types/index.js +1 -0
- package/dist/types/result.d.ts +10 -0
- package/dist/types/result.js +1 -0
- package/dist/types/tool.d.ts +105 -0
- package/dist/types/tool.js +1 -0
- package/dist/wire/index.d.ts +3 -0
- package/dist/wire/index.js +3 -0
- package/dist/wire/json-schema.d.ts +26 -0
- package/dist/wire/json-schema.js +81 -0
- package/dist/wire/markdown.d.ts +9 -0
- package/dist/wire/markdown.js +42 -0
- package/dist/wire/providers.d.ts +26 -0
- package/dist/wire/providers.js +20 -0
- package/package.json +64 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @spendgraph/tools
|
|
2
|
+
|
|
3
|
+
Declare a tool once, offer the right few, and record what was called.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npm install @spendgraph/tools
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Three things are exported. Everything else hangs off what they return.
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { tool, toolbus, currentTime } from "@spendgraph/tools";
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Declaring one
|
|
16
|
+
|
|
17
|
+
A description the model reads and a function you wrote, in one object. Splitting
|
|
18
|
+
them — a schema here, a handler there, matched by string — is how a rename
|
|
19
|
+
becomes a runtime failure mid-loop.
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
const lookup = tool({
|
|
23
|
+
name: "lookup_contract",
|
|
24
|
+
description: "Finds a contract clause by its reference, such as MSA 2.4.",
|
|
25
|
+
args: [
|
|
26
|
+
{ name: "ref", type: "string", required: true },
|
|
27
|
+
{ name: "limit", type: "number", required: false },
|
|
28
|
+
{ name: "mode", type: "enum", required: false, options: ["fast", "thorough"] },
|
|
29
|
+
] as const,
|
|
30
|
+
run: ({ ref, limit, mode }) => search(ref, limit ?? 5, mode ?? "fast"),
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
**Write `as const` and the handler types itself.** `ref` arrives a string,
|
|
35
|
+
`limit` a number, `mode` narrowed to its two options, and the optional ones
|
|
36
|
+
optional. Rename an argument and the handler stops compiling — rather than being
|
|
37
|
+
handed `undefined` halfway through an agent loop.
|
|
38
|
+
|
|
39
|
+
The description is the biggest lever on whether the model calls it correctly, and
|
|
40
|
+
worth iterating on more than the code is.
|
|
41
|
+
|
|
42
|
+
Every check `tool` makes is something a provider either rejects opaquely or
|
|
43
|
+
accepts and behaves oddly about — a name a provider will not take, an enum with
|
|
44
|
+
no options, a blank option the model can legally pick and you cannot act on.
|
|
45
|
+
They fire at import, not on the first loop in production.
|
|
46
|
+
|
|
47
|
+
## What a tool does
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
tool({
|
|
51
|
+
name: "issue_refund",
|
|
52
|
+
description: "Refund a charge to the original card.",
|
|
53
|
+
effect: "destructive",
|
|
54
|
+
args,
|
|
55
|
+
run,
|
|
56
|
+
});
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Without it `issue_refund` and `lookup_clause` are the same thing to a bus: two
|
|
60
|
+
names with schemas. Nothing can tell what is safe to retry, safe to run twice,
|
|
61
|
+
or worth asking a person about first.
|
|
62
|
+
|
|
63
|
+
| | |
|
|
64
|
+
| --- | --- |
|
|
65
|
+
| `readonly` | no effect outside the process |
|
|
66
|
+
| `idempotent` | has an effect, but twice is the same as once |
|
|
67
|
+
| `destructive` | neither, so never retry it on its own |
|
|
68
|
+
|
|
69
|
+
Three values, not a taxonomy — anything finer does not get filled in
|
|
70
|
+
accurately.
|
|
71
|
+
|
|
72
|
+
**There is no default, and unset is not `readonly`.** A default would quietly
|
|
73
|
+
label every tool written before this existed as safe, which is the one mistake
|
|
74
|
+
that matters here. Unset means nobody has said.
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
bus.effectOf("issue_refund") // "destructive", or undefined if it never said
|
|
78
|
+
bus.destructive() // ["issue_refund"]
|
|
79
|
+
bus.unannotated() // ["send_email"]
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`destructive()` returns only the tools that said so, so a gate built on it alone
|
|
83
|
+
lets an unlabelled one through. `unannotated()` is how you find those — assert
|
|
84
|
+
it is empty in a test and a new tool cannot join the bus unlabelled.
|
|
85
|
+
|
|
86
|
+
It rides along on `declarations()` and survives `select()`, which is where a
|
|
87
|
+
gate reads it. Every provider shape drops it: no API has a field for it, and an
|
|
88
|
+
unknown key is rejected or ignored.
|
|
89
|
+
|
|
90
|
+
Two builtins leave it unset on purpose. `http_request` is a GET or a POST
|
|
91
|
+
depending on an argument, and one label cannot be true of both; `escalate` runs
|
|
92
|
+
your `onEscalate`, and paging someone twice is not the same as paging them once.
|
|
93
|
+
|
|
94
|
+
## The bus
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const bus = toolbus([lookup, currentTime()], { limit: 6 });
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Holding them in one place is what makes selection possible. Fifty tools at
|
|
101
|
+
roughly 150 tokens of declaration each is 7.5k tokens on every call before the
|
|
102
|
+
user has said anything, so past a point you stop sending all of them.
|
|
103
|
+
|
|
104
|
+
```ts
|
|
105
|
+
bus.select(query) // the working set for one request
|
|
106
|
+
bus.anthropic(decls?) // declarations, per provider
|
|
107
|
+
bus.openai(decls?)
|
|
108
|
+
bus.markdown(decls?) // for a model with no tools API
|
|
109
|
+
await bus.invoke(name, args)
|
|
110
|
+
bus.missing(["refund"]) // names something expects that this bus lacks
|
|
111
|
+
bus.usage()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`limit` is an accuracy budget, not a token one: more tools in front of a model
|
|
115
|
+
makes it worse at picking the right one regardless of context length. Measured
|
|
116
|
+
shortlists cluster around five to eight. Pinned tools count against it.
|
|
117
|
+
|
|
118
|
+
Selection is keyword and usage, deliberately — no embedding provider, no extra
|
|
119
|
+
call, no latency. It is also the baseline that says what embeddings would buy.
|
|
120
|
+
|
|
121
|
+
`missing` catches a silent failure: a prompt edited to expect a `refund` tool,
|
|
122
|
+
deployed against a bus without one, does not error. The agent is never offered it
|
|
123
|
+
and answers anyway.
|
|
124
|
+
|
|
125
|
+
## One turn
|
|
126
|
+
|
|
127
|
+
```ts
|
|
128
|
+
const turn = bus.trace(query);
|
|
129
|
+
|
|
130
|
+
const reply = await llm.call(messages, { tools: turn });
|
|
131
|
+
await turn.invoke(call.name, call.args);
|
|
132
|
+
|
|
133
|
+
turn.record(); // { offeredTools, steps }
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
A turn is selected once and remembers what it offered. Which tools were offered
|
|
137
|
+
is recorded as well as which were called: without it a record cannot tell "chose
|
|
138
|
+
not to" from "was never offered", which are opposite bugs.
|
|
139
|
+
|
|
140
|
+
It plugs straight into `@spendgraph/prompt` — `call` and `invoke` take anything
|
|
141
|
+
that hands out a turn, and neither package imports the other.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
await prompt.call(values, ({ messages, turn }) => llm.call(messages, { tools: turn }), {
|
|
145
|
+
tools: bus,
|
|
146
|
+
});
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
## Builtins
|
|
150
|
+
|
|
151
|
+
Eight, each a factory, none registered for you — this package's own advice is
|
|
152
|
+
that more tools in front of a model makes it worse at picking, so they are opt-in
|
|
153
|
+
one at a time.
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
import { calculate, currentTime, escalate, finish, think, httpRequest } from "@spendgraph/tools";
|
|
157
|
+
|
|
158
|
+
const bus = toolbus([
|
|
159
|
+
calculate(),
|
|
160
|
+
think(),
|
|
161
|
+
finish(),
|
|
162
|
+
currentTime({ defaultTimeZone: "Europe/London" }),
|
|
163
|
+
escalate({ onEscalate: (r) => pager.page(r) }),
|
|
164
|
+
httpRequest({ allow: [".example.com"], headers: { authorization: `Bearer ${key}` } }),
|
|
165
|
+
]);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
| | |
|
|
169
|
+
| --- | --- |
|
|
170
|
+
| `calculate()` | arithmetic, **parsed not evaluated** |
|
|
171
|
+
| `think()` | a scratchpad that does nothing |
|
|
172
|
+
| `finish()` | the model says it is done, and what the answer is |
|
|
173
|
+
| `currentTime()` | the clock a model does not have |
|
|
174
|
+
| `escalate()` | hand back to a person; `pinned` by default |
|
|
175
|
+
| `httpRequest()` | a GET or POST to hosts you named |
|
|
176
|
+
| `dateMath()` | add, subtract and difference, in whole units |
|
|
177
|
+
| `jsonQuery()` | one value out of a JSON document, by path |
|
|
178
|
+
| `writeDocument()` | a document agent that returns a rendered file |
|
|
179
|
+
|
|
180
|
+
**`calculate`** parses the expression — numbers, `+ - * / % ^`, brackets, and
|
|
181
|
+
`sqrt abs round floor ceil ln log` — and refuses everything else. `eval` on model
|
|
182
|
+
output is arbitrary code execution with extra steps.
|
|
183
|
+
|
|
184
|
+
**`think`** returns `"noted"` and changes nothing. That is the point: the thought
|
|
185
|
+
lands in the steps instead of in the reply, and a model given room to plan before
|
|
186
|
+
a multi-step sequence uses the tools better.
|
|
187
|
+
|
|
188
|
+
**`finish`** gives a loop a real stop condition. Without it a run ends when the
|
|
189
|
+
model goes quiet, which is indistinguishable from it losing the thread. Read the
|
|
190
|
+
result with `isFinished(result)` and `answerOf(result)`.
|
|
191
|
+
|
|
192
|
+
**`escalate`** is `pinned: true`, because a refusal route lost to a similarity
|
|
193
|
+
score is exactly the failure `pinned` exists for — the one turn that needs it is
|
|
194
|
+
the turn where nothing else fit. Without an `onEscalate` it records the request
|
|
195
|
+
and reports `delivered: false`, rather than letting the model believe a person
|
|
196
|
+
was reached.
|
|
197
|
+
|
|
198
|
+
**`dateMath`** is what `current_time` leads to: the clock says when now is, this
|
|
199
|
+
works out what follows. Calendar months clamp rather than overflow — a month
|
|
200
|
+
after 31 January is 28 February, not 3 March, which is what `setMonth` gives and
|
|
201
|
+
what every billing system disagrees with. `difference` counts boundaries crossed
|
|
202
|
+
rather than dividing elapsed milliseconds, because a month is not 30 days and a
|
|
203
|
+
year is not 365.
|
|
204
|
+
|
|
205
|
+
**`jsonQuery`** reads one value out of a document by path — `invoices[0].total`,
|
|
206
|
+
or `invoices[*].total` for every element. After a wildcard the rest of the path
|
|
207
|
+
keeps applying to each element, so `invoices[*].lines[0].sku` is the first line
|
|
208
|
+
of every invoice, not the lines of the first. A path that matches nothing comes
|
|
209
|
+
back `found: false` rather than `null`, which a model would otherwise read as the
|
|
210
|
+
answer.
|
|
211
|
+
|
|
212
|
+
**`httpRequest`** takes a **required** allowlist and has no default. It is https
|
|
213
|
+
only, refuses private, loopback, link-local and `.internal`/`.local` hosts even
|
|
214
|
+
if you list them, and sets headers itself so a credential is never something the
|
|
215
|
+
model can redirect. `"api.example.com"` matches that host; `".example.com"`
|
|
216
|
+
matches it and any subdomain. If the guard feels heavy, that is the correct
|
|
217
|
+
amount of heavy: this is the one builtin that can exfiltrate.
|
|
218
|
+
|
|
219
|
+
## `current_time`
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
toolbus([currentTime({ defaultTimeZone: "Europe/London" })]);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
A model has no clock, and asked for today's date it will confidently produce its
|
|
226
|
+
training cutoff. A wrong IANA zone throws rather than falling back, because a
|
|
227
|
+
confidently wrong time is the failure this exists to stop.
|
|
228
|
+
|
|
229
|
+
## `write_document`
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
toolbus([
|
|
233
|
+
writeDocument({
|
|
234
|
+
apiKey: env.MOA_API_KEY,
|
|
235
|
+
design: {
|
|
236
|
+
name: "Northwind",
|
|
237
|
+
fonts: { heading: "Söhne" },
|
|
238
|
+
palette: { accent: "#E11D48" },
|
|
239
|
+
rules: ["Never set body text below 11pt."],
|
|
240
|
+
},
|
|
241
|
+
}),
|
|
242
|
+
]);
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
Commissions a Moa agent to write and render a real file — `.pdf`, `.pptx`,
|
|
246
|
+
`.docx`, `.md` — and hands back where to download it. Everything else in this
|
|
247
|
+
package answers a question; this one produces the document somebody opens.
|
|
248
|
+
|
|
249
|
+
Style is the deployment's business and content is the model's. `design` is laid
|
|
250
|
+
over `DEFAULT_DESIGN`: named fields win, `rules` accumulate, and the merged
|
|
251
|
+
guide is rendered into the task as a `<design>` block along with the craft that
|
|
252
|
+
belongs to the format asked for — a deck gets "one idea per slide, the title is
|
|
253
|
+
the claim", a PDF gets "nothing straddles a page break". The model may pass
|
|
254
|
+
`design_notes` to bend the guide for one document; it cannot replace it.
|
|
255
|
+
|
|
256
|
+
A rendered document is minutes, so the wait defaults to ten of them and
|
|
257
|
+
long-polls rather than tight-polls. When even that runs out the result comes
|
|
258
|
+
back `status: "running"` with the `taskId`, because the money is already spent
|
|
259
|
+
and a lost id makes the model commission it twice. Stop the turn and the task is
|
|
260
|
+
cancelled server-side.
|
|
261
|
+
|
|
262
|
+
It is the one builtin marked `destructive`, and not because it breaks anything:
|
|
263
|
+
a second call is a second bill and a second set of files, so it is never safe to
|
|
264
|
+
retry on its own.
|
|
265
|
+
|
|
266
|
+
## Examples
|
|
267
|
+
|
|
268
|
+
Eight of them under [`examples/`](./examples), all runnable with no API key and
|
|
269
|
+
no network — tools are local code; the model only decides which to call.
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
npm run build
|
|
273
|
+
node examples/02-select/01-shortlist.mjs
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Subpaths
|
|
277
|
+
|
|
278
|
+
| | |
|
|
279
|
+
| --- | --- |
|
|
280
|
+
| `@spendgraph/tools/wire` | `toAnthropic` · `toOpenAI` · `toJsonSchema` · `toMarkdown` |
|
|
281
|
+
| `@spendgraph/tools/internals` | `selectTools` · `score` · `invokeTool` · `newTrace` · `toStep` · `warnOnOverlap` |
|
|
282
|
+
|
|
283
|
+
A bus already wires all of `internals`; reach for it to build your own.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic, because a model does it from memory and is confidently wrong.
|
|
3
|
+
*
|
|
4
|
+
* The expression is parsed, never evaluated — `eval` on model output is
|
|
5
|
+
* arbitrary code execution with extra steps.
|
|
6
|
+
*/
|
|
7
|
+
export declare function calculate(): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
|
|
8
|
+
readonly name: "expression";
|
|
9
|
+
readonly type: "string";
|
|
10
|
+
readonly required: true;
|
|
11
|
+
readonly maxLength: 500;
|
|
12
|
+
readonly description: "For example (1250 * 0.2) + 99, or sqrt(144).";
|
|
13
|
+
}]>>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { tool } from "../../tool/index.js";
|
|
2
|
+
import { evaluate } from "./parse.js";
|
|
3
|
+
/**
|
|
4
|
+
* Arithmetic, because a model does it from memory and is confidently wrong.
|
|
5
|
+
*
|
|
6
|
+
* The expression is parsed, never evaluated — `eval` on model output is
|
|
7
|
+
* arbitrary code execution with extra steps.
|
|
8
|
+
*/
|
|
9
|
+
export function calculate() {
|
|
10
|
+
return tool({
|
|
11
|
+
name: "calculate",
|
|
12
|
+
effect: "readonly",
|
|
13
|
+
description: "Works out an arithmetic expression exactly. Use it for any sum that matters — " +
|
|
14
|
+
"totals, percentages, differences, rates. Supports + - * / % ^, brackets, and " +
|
|
15
|
+
"sqrt, abs, round, floor, ceil, ln, log.",
|
|
16
|
+
args: [
|
|
17
|
+
{
|
|
18
|
+
name: "expression",
|
|
19
|
+
type: "string",
|
|
20
|
+
required: true,
|
|
21
|
+
maxLength: 500,
|
|
22
|
+
description: "For example (1250 * 0.2) + 99, or sqrt(144).",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
run: ({ expression }) => evaluate(expression),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arithmetic, parsed rather than evaluated.
|
|
3
|
+
*
|
|
4
|
+
* `eval` on model output is arbitrary code execution with extra steps. This
|
|
5
|
+
* reads numbers, the five operators, brackets and a short list of functions,
|
|
6
|
+
* and refuses everything else.
|
|
7
|
+
*/
|
|
8
|
+
export declare function evaluate(expression: string): number;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const FUNCTIONS = {
|
|
2
|
+
sqrt: Math.sqrt,
|
|
3
|
+
abs: Math.abs,
|
|
4
|
+
round: Math.round,
|
|
5
|
+
floor: Math.floor,
|
|
6
|
+
ceil: Math.ceil,
|
|
7
|
+
ln: Math.log,
|
|
8
|
+
log: Math.log10,
|
|
9
|
+
};
|
|
10
|
+
const OPERATORS = new Set(["+", "-", "*", "/", "%", "^", "(", ")", ","]);
|
|
11
|
+
function tokenize(input) {
|
|
12
|
+
const tokens = [];
|
|
13
|
+
let i = 0;
|
|
14
|
+
while (i < input.length) {
|
|
15
|
+
const ch = input[i];
|
|
16
|
+
if (ch === " " || ch === "\t" || ch === "_") {
|
|
17
|
+
i++;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (ch >= "0" && ch <= "9") {
|
|
21
|
+
let j = i;
|
|
22
|
+
while (j < input.length && /[0-9._]/.test(input[j]))
|
|
23
|
+
j++;
|
|
24
|
+
const value = Number(input.slice(i, j).replace(/_/g, ""));
|
|
25
|
+
if (!Number.isFinite(value))
|
|
26
|
+
throw new Error(`"${input.slice(i, j)}" is not a number.`);
|
|
27
|
+
tokens.push({ kind: "number", value });
|
|
28
|
+
i = j;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (/[a-z]/i.test(ch)) {
|
|
32
|
+
let j = i;
|
|
33
|
+
while (j < input.length && /[a-z]/i.test(input[j]))
|
|
34
|
+
j++;
|
|
35
|
+
tokens.push({ kind: "op", value: input.slice(i, j).toLowerCase() });
|
|
36
|
+
i = j;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (OPERATORS.has(ch)) {
|
|
40
|
+
tokens.push({ kind: "op", value: ch });
|
|
41
|
+
i++;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
throw new Error(`"${ch}" is not something this calculator understands.`);
|
|
45
|
+
}
|
|
46
|
+
return tokens;
|
|
47
|
+
}
|
|
48
|
+
class Reader {
|
|
49
|
+
tokens;
|
|
50
|
+
at = 0;
|
|
51
|
+
constructor(tokens) {
|
|
52
|
+
this.tokens = tokens;
|
|
53
|
+
}
|
|
54
|
+
peek() {
|
|
55
|
+
return this.tokens[this.at];
|
|
56
|
+
}
|
|
57
|
+
take() {
|
|
58
|
+
return this.tokens[this.at++];
|
|
59
|
+
}
|
|
60
|
+
eat(value) {
|
|
61
|
+
const next = this.peek();
|
|
62
|
+
if (next?.kind === "op" && next.value === value) {
|
|
63
|
+
this.at++;
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
get done() {
|
|
69
|
+
return this.at >= this.tokens.length;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function primary(r) {
|
|
73
|
+
const token = r.take();
|
|
74
|
+
if (!token)
|
|
75
|
+
throw new Error("The expression ends where a number was expected.");
|
|
76
|
+
if (token.kind === "number")
|
|
77
|
+
return token.value;
|
|
78
|
+
if (token.value === "-")
|
|
79
|
+
return -primary(r);
|
|
80
|
+
if (token.value === "+")
|
|
81
|
+
return primary(r);
|
|
82
|
+
if (token.value === "(") {
|
|
83
|
+
const value = additive(r);
|
|
84
|
+
if (!r.eat(")"))
|
|
85
|
+
throw new Error("A bracket was opened and never closed.");
|
|
86
|
+
return value;
|
|
87
|
+
}
|
|
88
|
+
const fn = FUNCTIONS[token.value];
|
|
89
|
+
if (!fn)
|
|
90
|
+
throw new Error(`"${token.value}" is not a function this calculator has.`);
|
|
91
|
+
if (!r.eat("("))
|
|
92
|
+
throw new Error(`${token.value} needs brackets, as in ${token.value}(9).`);
|
|
93
|
+
const arg = additive(r);
|
|
94
|
+
if (!r.eat(")"))
|
|
95
|
+
throw new Error("A bracket was opened and never closed.");
|
|
96
|
+
return fn(arg);
|
|
97
|
+
}
|
|
98
|
+
function power(r) {
|
|
99
|
+
const base = primary(r);
|
|
100
|
+
if (r.eat("^"))
|
|
101
|
+
return base ** power(r);
|
|
102
|
+
return base;
|
|
103
|
+
}
|
|
104
|
+
function multiplicative(r) {
|
|
105
|
+
let value = power(r);
|
|
106
|
+
for (;;) {
|
|
107
|
+
if (r.eat("*"))
|
|
108
|
+
value *= power(r);
|
|
109
|
+
else if (r.eat("/"))
|
|
110
|
+
value /= power(r);
|
|
111
|
+
else if (r.eat("%"))
|
|
112
|
+
value %= power(r);
|
|
113
|
+
else
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function additive(r) {
|
|
118
|
+
let value = multiplicative(r);
|
|
119
|
+
for (;;) {
|
|
120
|
+
if (r.eat("+"))
|
|
121
|
+
value += multiplicative(r);
|
|
122
|
+
else if (r.eat("-"))
|
|
123
|
+
value -= multiplicative(r);
|
|
124
|
+
else
|
|
125
|
+
return value;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Arithmetic, parsed rather than evaluated.
|
|
130
|
+
*
|
|
131
|
+
* `eval` on model output is arbitrary code execution with extra steps. This
|
|
132
|
+
* reads numbers, the five operators, brackets and a short list of functions,
|
|
133
|
+
* and refuses everything else.
|
|
134
|
+
*/
|
|
135
|
+
export function evaluate(expression) {
|
|
136
|
+
const reader = new Reader(tokenize(expression));
|
|
137
|
+
const value = additive(reader);
|
|
138
|
+
if (!reader.done)
|
|
139
|
+
throw new Error("There is something left over after the expression.");
|
|
140
|
+
if (!Number.isFinite(value)) {
|
|
141
|
+
throw new Error(value === Infinity || value === -Infinity ? "That divides by zero." : "That is not a number.");
|
|
142
|
+
}
|
|
143
|
+
return value;
|
|
144
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface ClockOptions {
|
|
2
|
+
/** Injectable clock, so a test does not depend on the day it runs. */
|
|
3
|
+
now?: () => Date;
|
|
4
|
+
/** Used when the caller names none. Defaults to the host's zone. */
|
|
5
|
+
defaultTimeZone?: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* What time it is.
|
|
9
|
+
*
|
|
10
|
+
* The one thing every model is confidently wrong about: weights are frozen at
|
|
11
|
+
* training time, so "today" is answered fluently from a stale date.
|
|
12
|
+
*
|
|
13
|
+
* Built in because every agent needs it and the common mistake — a formatted
|
|
14
|
+
* string with no zone — is a different instant depending on where it ran.
|
|
15
|
+
*/
|
|
16
|
+
export declare function currentTime(opts?: ClockOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
|
|
17
|
+
readonly name: "timezone";
|
|
18
|
+
readonly type: "string";
|
|
19
|
+
readonly required: false;
|
|
20
|
+
readonly description: "IANA zone such as Europe/London or Asia/Kolkata. Defaults to the server's zone.";
|
|
21
|
+
}, {
|
|
22
|
+
readonly name: "format";
|
|
23
|
+
readonly type: "enum";
|
|
24
|
+
readonly required: false;
|
|
25
|
+
readonly options: readonly ["iso", "human", "date", "time"];
|
|
26
|
+
readonly description: "iso is unambiguous and the right default for anything machine-read.";
|
|
27
|
+
}]>>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { tool } from "../../tool/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* What time it is.
|
|
4
|
+
*
|
|
5
|
+
* The one thing every model is confidently wrong about: weights are frozen at
|
|
6
|
+
* training time, so "today" is answered fluently from a stale date.
|
|
7
|
+
*
|
|
8
|
+
* Built in because every agent needs it and the common mistake — a formatted
|
|
9
|
+
* string with no zone — is a different instant depending on where it ran.
|
|
10
|
+
*/
|
|
11
|
+
export function currentTime(opts = {}) {
|
|
12
|
+
const now = opts.now ?? (() => new Date());
|
|
13
|
+
return tool({
|
|
14
|
+
name: "current_time",
|
|
15
|
+
effect: "readonly",
|
|
16
|
+
description: "The current date and time. Call this before answering anything that " +
|
|
17
|
+
"depends on today's date — ages, deadlines, 'how long ago', 'this year'. " +
|
|
18
|
+
"Do not rely on your own sense of the date; it is the date you were " +
|
|
19
|
+
"trained, not today.",
|
|
20
|
+
args: [
|
|
21
|
+
{
|
|
22
|
+
name: "timezone",
|
|
23
|
+
type: "string",
|
|
24
|
+
required: false,
|
|
25
|
+
description: "IANA zone such as Europe/London or Asia/Kolkata. Defaults to the server's zone.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "format",
|
|
29
|
+
type: "enum",
|
|
30
|
+
required: false,
|
|
31
|
+
options: ["iso", "human", "date", "time"],
|
|
32
|
+
description: "iso is unambiguous and the right default for anything machine-read.",
|
|
33
|
+
},
|
|
34
|
+
],
|
|
35
|
+
run: ({ timezone, format = "iso" }) => {
|
|
36
|
+
const at = now();
|
|
37
|
+
const zone = timezone ?? opts.defaultTimeZone ?? Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
38
|
+
// Thrown rather than silently falling back: a wrong zone gives a
|
|
39
|
+
// confidently wrong time, which is the failure this tool exists to stop.
|
|
40
|
+
try {
|
|
41
|
+
new Intl.DateTimeFormat("en-GB", { timeZone: zone });
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`"${zone}" is not a known IANA time zone.`);
|
|
45
|
+
}
|
|
46
|
+
// `iso` is a UTC instant, so the whole answer is UTC. Formatting the
|
|
47
|
+
// weekday in the requested zone while reporting UTC put Saturday and
|
|
48
|
+
// Sunday in one object near midnight.
|
|
49
|
+
const answerZone = format === "iso" ? "UTC" : zone;
|
|
50
|
+
const parts = (options) => new Intl.DateTimeFormat("en-GB", {
|
|
51
|
+
timeZone: answerZone,
|
|
52
|
+
...options,
|
|
53
|
+
}).format(at);
|
|
54
|
+
const formatted = format === "human"
|
|
55
|
+
? parts({ dateStyle: "full", timeStyle: "short" })
|
|
56
|
+
: format === "date"
|
|
57
|
+
? parts({ year: "numeric", month: "2-digit", day: "2-digit" })
|
|
58
|
+
: format === "time"
|
|
59
|
+
? parts({
|
|
60
|
+
hour: "2-digit",
|
|
61
|
+
minute: "2-digit",
|
|
62
|
+
second: "2-digit",
|
|
63
|
+
hour12: false,
|
|
64
|
+
})
|
|
65
|
+
: at.toISOString();
|
|
66
|
+
// The zone travels with the answer, and every field uses the one named.
|
|
67
|
+
return {
|
|
68
|
+
now: formatted,
|
|
69
|
+
iso: at.toISOString(),
|
|
70
|
+
timezone: answerZone,
|
|
71
|
+
unix: Math.floor(at.getTime() / 1000),
|
|
72
|
+
weekday: parts({ weekday: "long" }),
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { type ClockOptions, currentTime } from "./clock.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { currentTime } from "./clock.js";
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Date arithmetic, because a model does it in its head and drops a leap year.
|
|
3
|
+
*
|
|
4
|
+
* `current_time` says when now is; this works out what follows from it —
|
|
5
|
+
* deadlines, ages, notice periods, how long ago something was.
|
|
6
|
+
*/
|
|
7
|
+
export declare function dateMath(): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
|
|
8
|
+
readonly name: "operation";
|
|
9
|
+
readonly type: "enum";
|
|
10
|
+
readonly required: true;
|
|
11
|
+
readonly options: readonly ["add", "subtract", "difference"];
|
|
12
|
+
}, {
|
|
13
|
+
readonly name: "date";
|
|
14
|
+
readonly type: "string";
|
|
15
|
+
readonly required: true;
|
|
16
|
+
readonly description: "ISO, as in 2026-03-31.";
|
|
17
|
+
}, {
|
|
18
|
+
readonly name: "unit";
|
|
19
|
+
readonly type: "enum";
|
|
20
|
+
readonly required: true;
|
|
21
|
+
readonly options: readonly ["seconds", "minutes", "hours", "days", "weeks", "months", "years"];
|
|
22
|
+
}, {
|
|
23
|
+
readonly name: "amount";
|
|
24
|
+
readonly type: "number";
|
|
25
|
+
readonly required: false;
|
|
26
|
+
readonly description: "For add and subtract.";
|
|
27
|
+
}, {
|
|
28
|
+
readonly name: "to";
|
|
29
|
+
readonly type: "string";
|
|
30
|
+
readonly required: false;
|
|
31
|
+
readonly description: "The other date, for difference.";
|
|
32
|
+
}]>>;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { tool } from "../../tool/index.js";
|
|
2
|
+
import { between, parseDate, shift } from "./shift.js";
|
|
3
|
+
const UNITS = ["seconds", "minutes", "hours", "days", "weeks", "months", "years"];
|
|
4
|
+
/**
|
|
5
|
+
* Date arithmetic, because a model does it in its head and drops a leap year.
|
|
6
|
+
*
|
|
7
|
+
* `current_time` says when now is; this works out what follows from it —
|
|
8
|
+
* deadlines, ages, notice periods, how long ago something was.
|
|
9
|
+
*/
|
|
10
|
+
export function dateMath() {
|
|
11
|
+
return tool({
|
|
12
|
+
name: "date_math",
|
|
13
|
+
effect: "readonly",
|
|
14
|
+
description: "Works out a date exactly. `add` and `subtract` move a date by an amount of a " +
|
|
15
|
+
"unit; `difference` says how far apart two dates are in whole units. Use it for " +
|
|
16
|
+
"deadlines, notice periods, ages and 'how long ago' — never work a date out yourself.",
|
|
17
|
+
args: [
|
|
18
|
+
{
|
|
19
|
+
name: "operation",
|
|
20
|
+
type: "enum",
|
|
21
|
+
required: true,
|
|
22
|
+
options: ["add", "subtract", "difference"],
|
|
23
|
+
},
|
|
24
|
+
{ name: "date", type: "string", required: true, description: "ISO, as in 2026-03-31." },
|
|
25
|
+
{ name: "unit", type: "enum", required: true, options: [...UNITS] },
|
|
26
|
+
{ name: "amount", type: "number", required: false, description: "For add and subtract." },
|
|
27
|
+
{
|
|
28
|
+
name: "to",
|
|
29
|
+
type: "string",
|
|
30
|
+
required: false,
|
|
31
|
+
description: "The other date, for difference.",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
run: ({ operation, date, unit, amount, to }) => {
|
|
35
|
+
const from = parseDate(date, "date");
|
|
36
|
+
if (operation === "difference") {
|
|
37
|
+
if (!to)
|
|
38
|
+
throw new Error("difference needs a second date in `to`.");
|
|
39
|
+
return { unit, difference: between(from, parseDate(to, "to"), unit) };
|
|
40
|
+
}
|
|
41
|
+
if (amount === undefined)
|
|
42
|
+
throw new Error(`${operation} needs an \`amount\`.`);
|
|
43
|
+
const moved = shift(from, operation === "subtract" ? -amount : amount, unit);
|
|
44
|
+
return { iso: moved.toISOString(), date: moved.toISOString().slice(0, 10) };
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
}
|