@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.
Files changed (108) hide show
  1. package/README.md +283 -0
  2. package/dist/builtin/calculate/calculate.d.ts +13 -0
  3. package/dist/builtin/calculate/calculate.js +27 -0
  4. package/dist/builtin/calculate/index.d.ts +2 -0
  5. package/dist/builtin/calculate/index.js +2 -0
  6. package/dist/builtin/calculate/parse.d.ts +8 -0
  7. package/dist/builtin/calculate/parse.js +144 -0
  8. package/dist/builtin/clock/clock.d.ts +27 -0
  9. package/dist/builtin/clock/clock.js +76 -0
  10. package/dist/builtin/clock/index.d.ts +1 -0
  11. package/dist/builtin/clock/index.js +1 -0
  12. package/dist/builtin/date/date.d.ts +32 -0
  13. package/dist/builtin/date/date.js +47 -0
  14. package/dist/builtin/date/index.d.ts +2 -0
  15. package/dist/builtin/date/index.js +2 -0
  16. package/dist/builtin/date/shift.d.ts +12 -0
  17. package/dist/builtin/date/shift.js +62 -0
  18. package/dist/builtin/deep-recall/deep-recall.d.ts +27 -0
  19. package/dist/builtin/deep-recall/deep-recall.js +51 -0
  20. package/dist/builtin/deep-recall/index.d.ts +1 -0
  21. package/dist/builtin/deep-recall/index.js +1 -0
  22. package/dist/builtin/escalate/escalate.d.ts +46 -0
  23. package/dist/builtin/escalate/escalate.js +29 -0
  24. package/dist/builtin/escalate/index.d.ts +1 -0
  25. package/dist/builtin/escalate/index.js +1 -0
  26. package/dist/builtin/finish/finish.d.ts +27 -0
  27. package/dist/builtin/finish/finish.js +39 -0
  28. package/dist/builtin/finish/index.d.ts +1 -0
  29. package/dist/builtin/finish/index.js +1 -0
  30. package/dist/builtin/http/allow.d.ts +8 -0
  31. package/dist/builtin/http/allow.js +45 -0
  32. package/dist/builtin/http/http.d.ts +43 -0
  33. package/dist/builtin/http/http.js +51 -0
  34. package/dist/builtin/http/index.d.ts +2 -0
  35. package/dist/builtin/http/index.js +2 -0
  36. package/dist/builtin/index.d.ts +11 -0
  37. package/dist/builtin/index.js +11 -0
  38. package/dist/builtin/json/index.d.ts +2 -0
  39. package/dist/builtin/json/index.js +2 -0
  40. package/dist/builtin/json/json.d.ts +22 -0
  41. package/dist/builtin/json/json.js +44 -0
  42. package/dist/builtin/json/path.d.ts +8 -0
  43. package/dist/builtin/json/path.js +63 -0
  44. package/dist/builtin/think/index.d.ts +1 -0
  45. package/dist/builtin/think/index.js +1 -0
  46. package/dist/builtin/think/think.d.ts +13 -0
  47. package/dist/builtin/think/think.js +19 -0
  48. package/dist/builtin/web-search/index.d.ts +1 -0
  49. package/dist/builtin/web-search/index.js +1 -0
  50. package/dist/builtin/web-search/reply.d.ts +23 -0
  51. package/dist/builtin/web-search/reply.js +35 -0
  52. package/dist/builtin/web-search/web-search.d.ts +50 -0
  53. package/dist/builtin/web-search/web-search.js +93 -0
  54. package/dist/builtin/write-document/commission.d.ts +56 -0
  55. package/dist/builtin/write-document/commission.js +163 -0
  56. package/dist/builtin/write-document/design.d.ts +49 -0
  57. package/dist/builtin/write-document/design.js +99 -0
  58. package/dist/builtin/write-document/http.d.ts +34 -0
  59. package/dist/builtin/write-document/http.js +74 -0
  60. package/dist/builtin/write-document/index.d.ts +6 -0
  61. package/dist/builtin/write-document/index.js +4 -0
  62. package/dist/builtin/write-document/types.d.ts +33 -0
  63. package/dist/builtin/write-document/types.js +1 -0
  64. package/dist/builtin/write-document/write-document.d.ts +69 -0
  65. package/dist/builtin/write-document/write-document.js +150 -0
  66. package/dist/bus/bus.d.ts +76 -0
  67. package/dist/bus/bus.js +103 -0
  68. package/dist/bus/index.d.ts +3 -0
  69. package/dist/bus/index.js +2 -0
  70. package/dist/bus/invoke.d.ts +9 -0
  71. package/dist/bus/invoke.js +50 -0
  72. package/dist/bus/options.d.ts +22 -0
  73. package/dist/bus/options.js +1 -0
  74. package/dist/bus/overlap.d.ts +12 -0
  75. package/dist/bus/overlap.js +43 -0
  76. package/dist/bus/score.d.ts +9 -0
  77. package/dist/bus/score.js +19 -0
  78. package/dist/bus/select.d.ts +18 -0
  79. package/dist/bus/select.js +71 -0
  80. package/dist/bus/step.d.ts +4 -0
  81. package/dist/bus/step.js +12 -0
  82. package/dist/bus/trace.d.ts +42 -0
  83. package/dist/bus/trace.js +34 -0
  84. package/dist/index.d.ts +6 -0
  85. package/dist/index.js +3 -0
  86. package/dist/internals.d.ts +16 -0
  87. package/dist/internals.js +16 -0
  88. package/dist/tool/bind.d.ts +42 -0
  89. package/dist/tool/bind.js +89 -0
  90. package/dist/tool/index.d.ts +4 -0
  91. package/dist/tool/index.js +2 -0
  92. package/dist/tool/tool.d.ts +20 -0
  93. package/dist/tool/tool.js +62 -0
  94. package/dist/types/index.d.ts +2 -0
  95. package/dist/types/index.js +1 -0
  96. package/dist/types/result.d.ts +10 -0
  97. package/dist/types/result.js +1 -0
  98. package/dist/types/tool.d.ts +105 -0
  99. package/dist/types/tool.js +1 -0
  100. package/dist/wire/index.d.ts +3 -0
  101. package/dist/wire/index.js +3 -0
  102. package/dist/wire/json-schema.d.ts +26 -0
  103. package/dist/wire/json-schema.js +81 -0
  104. package/dist/wire/markdown.d.ts +9 -0
  105. package/dist/wire/markdown.js +42 -0
  106. package/dist/wire/providers.d.ts +26 -0
  107. package/dist/wire/providers.js +20 -0
  108. 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,2 @@
1
+ export { calculate } from "./calculate.js";
2
+ export { evaluate } from "./parse.js";
@@ -0,0 +1,2 @@
1
+ export { calculate } from "./calculate.js";
2
+ export { evaluate } from "./parse.js";
@@ -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
+ }
@@ -0,0 +1,2 @@
1
+ export { dateMath } from "./date.js";
2
+ export { between, parseDate, shift, type Unit } from "./shift.js";