@spendgraph/tools 0.2.0 → 0.3.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 +10 -11
- package/dist/builtin/calculate/calculate.js +0 -6
- package/dist/builtin/calculate/parse.js +0 -7
- package/dist/builtin/clock/clock.js +0 -15
- package/dist/builtin/date/date.js +0 -6
- package/dist/builtin/date/shift.js +0 -15
- package/dist/builtin/deep-recall/deep-recall.js +0 -6
- package/dist/builtin/escalate/escalate.js +0 -9
- package/dist/builtin/finish/finish.js +0 -13
- package/dist/builtin/http/allow.js +0 -8
- package/dist/builtin/http/http.js +0 -10
- package/dist/builtin/json/json.js +0 -7
- package/dist/builtin/json/path.js +0 -7
- package/dist/builtin/think/think.js +0 -7
- package/dist/builtin/web-search/reply.js +0 -8
- package/dist/builtin/web-search/web-search.js +0 -8
- package/dist/builtin/write-document/commission.js +0 -11
- package/dist/builtin/write-document/design.js +0 -13
- package/dist/builtin/write-document/http.js +0 -3
- package/dist/builtin/write-document/write-document.js +0 -8
- package/dist/bus/bus.js +0 -54
- package/dist/bus/invoke.js +0 -7
- package/dist/bus/overlap.js +0 -12
- package/dist/bus/score.js +0 -7
- package/dist/bus/select.js +0 -27
- package/dist/bus/step.js +0 -1
- package/dist/bus/trace.js +0 -12
- package/dist/internals.js +0 -6
- package/dist/tool/bind.js +0 -25
- package/dist/tool/tool.js +0 -11
- package/dist/wire/json-schema.js +0 -16
- package/dist/wire/markdown.js +0 -13
- package/dist/wire/providers.js +0 -2
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -172,9 +172,9 @@ const bus = toolbus([
|
|
|
172
172
|
| `finish()` | the model says it is done, and what the answer is |
|
|
173
173
|
| `currentTime()` | the clock a model does not have |
|
|
174
174
|
| `escalate()` | hand back to a person; `pinned` by default |
|
|
175
|
-
| `httpRequest()` | a GET or POST to hosts you named |
|
|
176
175
|
| `dateMath()` | add, subtract and difference, in whole units |
|
|
177
176
|
| `jsonQuery()` | one value out of a JSON document, by path |
|
|
177
|
+
| `httpRequest()` | a GET or POST to hosts you named |
|
|
178
178
|
| `writeDocument()` | a document agent that returns a rendered file |
|
|
179
179
|
|
|
180
180
|
**`calculate`** parses the expression — numbers, `+ - * / % ^`, brackets, and
|
|
@@ -189,6 +189,11 @@ a multi-step sequence uses the tools better.
|
|
|
189
189
|
model goes quiet, which is indistinguishable from it losing the thread. Read the
|
|
190
190
|
result with `isFinished(result)` and `answerOf(result)`.
|
|
191
191
|
|
|
192
|
+
**`current_time`** is the clock a model does not have. Asked for today's date it
|
|
193
|
+
will otherwise produce its training cutoff, confidently. A wrong IANA zone throws
|
|
194
|
+
rather than falling back, because a confidently wrong time is the failure this
|
|
195
|
+
exists to stop.
|
|
196
|
+
|
|
192
197
|
**`escalate`** is `pinned: true`, because a refusal route lost to a similarity
|
|
193
198
|
score is exactly the failure `pinned` exists for — the one turn that needs it is
|
|
194
199
|
the turn where nothing else fit. Without an `onEscalate` it records the request
|
|
@@ -216,16 +221,6 @@ model can redirect. `"api.example.com"` matches that host; `".example.com"`
|
|
|
216
221
|
matches it and any subdomain. If the guard feels heavy, that is the correct
|
|
217
222
|
amount of heavy: this is the one builtin that can exfiltrate.
|
|
218
223
|
|
|
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
224
|
## `write_document`
|
|
230
225
|
|
|
231
226
|
```ts
|
|
@@ -281,3 +276,7 @@ node examples/02-select/01-shortlist.mjs
|
|
|
281
276
|
| `@spendgraph/tools/internals` | `selectTools` · `score` · `invokeTool` · `newTrace` · `toStep` · `warnOnOverlap` |
|
|
282
277
|
|
|
283
278
|
A bus already wires all of `internals`; reach for it to build your own.
|
|
279
|
+
|
|
280
|
+
## License
|
|
281
|
+
|
|
282
|
+
MIT
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
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
3
|
export function calculate() {
|
|
10
4
|
return tool({
|
|
11
5
|
name: "calculate",
|
|
@@ -125,13 +125,6 @@ function additive(r) {
|
|
|
125
125
|
return value;
|
|
126
126
|
}
|
|
127
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
128
|
export function evaluate(expression) {
|
|
136
129
|
const reader = new Reader(tokenize(expression));
|
|
137
130
|
const value = additive(reader);
|
|
@@ -1,13 +1,4 @@
|
|
|
1
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
2
|
export function currentTime(opts = {}) {
|
|
12
3
|
const now = opts.now ?? (() => new Date());
|
|
13
4
|
return tool({
|
|
@@ -35,17 +26,12 @@ export function currentTime(opts = {}) {
|
|
|
35
26
|
run: ({ timezone, format = "iso" }) => {
|
|
36
27
|
const at = now();
|
|
37
28
|
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
29
|
try {
|
|
41
30
|
new Intl.DateTimeFormat("en-GB", { timeZone: zone });
|
|
42
31
|
}
|
|
43
32
|
catch {
|
|
44
33
|
throw new Error(`"${zone}" is not a known IANA time zone.`);
|
|
45
34
|
}
|
|
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
35
|
const answerZone = format === "iso" ? "UTC" : zone;
|
|
50
36
|
const parts = (options) => new Intl.DateTimeFormat("en-GB", {
|
|
51
37
|
timeZone: answerZone,
|
|
@@ -63,7 +49,6 @@ export function currentTime(opts = {}) {
|
|
|
63
49
|
hour12: false,
|
|
64
50
|
})
|
|
65
51
|
: at.toISOString();
|
|
66
|
-
// The zone travels with the answer, and every field uses the one named.
|
|
67
52
|
return {
|
|
68
53
|
now: formatted,
|
|
69
54
|
iso: at.toISOString(),
|
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
2
|
import { between, parseDate, shift } from "./shift.js";
|
|
3
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
4
|
export function dateMath() {
|
|
11
5
|
return tool({
|
|
12
6
|
name: "date_math",
|
|
@@ -5,7 +5,6 @@ const MS = {
|
|
|
5
5
|
days: 86_400_000,
|
|
6
6
|
weeks: 604_800_000,
|
|
7
7
|
};
|
|
8
|
-
/** Parses an ISO instant, or says which argument was not one. */
|
|
9
8
|
export function parseDate(value, label) {
|
|
10
9
|
const at = new Date(value);
|
|
11
10
|
if (!Number.isFinite(at.getTime())) {
|
|
@@ -13,13 +12,6 @@ export function parseDate(value, label) {
|
|
|
13
12
|
}
|
|
14
13
|
return at;
|
|
15
14
|
}
|
|
16
|
-
/**
|
|
17
|
-
* Adds calendar months, clamping rather than overflowing.
|
|
18
|
-
*
|
|
19
|
-
* `setUTCMonth` turns 31 January plus one month into 3 March, which is nobody's
|
|
20
|
-
* idea of a month later. The last day of a short month is the answer people
|
|
21
|
-
* mean, and the answer every billing system gives.
|
|
22
|
-
*/
|
|
23
15
|
function addMonths(from, months) {
|
|
24
16
|
const day = from.getUTCDate();
|
|
25
17
|
const shifted = new Date(from.getTime());
|
|
@@ -29,7 +21,6 @@ function addMonths(from, months) {
|
|
|
29
21
|
shifted.setUTCDate(Math.min(day, lastOfMonth));
|
|
30
22
|
return shifted;
|
|
31
23
|
}
|
|
32
|
-
/** Moves an instant by a whole number of units. Negative goes backwards. */
|
|
33
24
|
export function shift(from, amount, unit) {
|
|
34
25
|
if (!Number.isInteger(amount))
|
|
35
26
|
throw new Error("amount must be a whole number.");
|
|
@@ -42,12 +33,6 @@ export function shift(from, amount, unit) {
|
|
|
42
33
|
throw new Error(`"${unit}" is not a unit this tool knows.`);
|
|
43
34
|
return new Date(from.getTime() + amount * ms);
|
|
44
35
|
}
|
|
45
|
-
/**
|
|
46
|
-
* How far apart two instants are, in whole units.
|
|
47
|
-
*
|
|
48
|
-
* Calendar units count boundaries crossed rather than dividing elapsed
|
|
49
|
-
* milliseconds, because a month is not 30 days and a year is not 365.
|
|
50
|
-
*/
|
|
51
36
|
export function between(from, to, unit) {
|
|
52
37
|
if (unit === "months" || unit === "years") {
|
|
53
38
|
const months = (to.getUTCFullYear() - from.getUTCFullYear()) * 12 +
|
|
@@ -1,11 +1,5 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
2
|
const DEFAULT_MAX_FACTS = 20;
|
|
3
|
-
/**
|
|
4
|
-
* Your own knowledge base, answered with cited evidence.
|
|
5
|
-
*
|
|
6
|
-
* Cost comes back as micro-USD because deep recall runs model calls of its own,
|
|
7
|
-
* and that spend is invisible to whatever loop called this.
|
|
8
|
-
*/
|
|
9
3
|
export function deepRecall(opts) {
|
|
10
4
|
const maxFacts = opts.maxFacts ?? DEFAULT_MAX_FACTS;
|
|
11
5
|
return tool({
|
|
@@ -1,13 +1,4 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Hand back to a person rather than guess.
|
|
4
|
-
*
|
|
5
|
-
* An agent with no way to stop answers anyway, and a confident wrong answer to
|
|
6
|
-
* something it could not do is worse than a handover.
|
|
7
|
-
*
|
|
8
|
-
* Its `effect` is left unset on purpose: `onEscalate` is yours, and paging
|
|
9
|
-
* someone twice is not the same as paging them once.
|
|
10
|
-
*/
|
|
11
2
|
export function escalate(opts = {}) {
|
|
12
3
|
return tool({
|
|
13
4
|
name: "escalate",
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* True when this result is the model saying it is done.
|
|
4
|
-
*
|
|
5
|
-
* A loop that ends because the model stopped asking for tools cannot tell
|
|
6
|
-
* finished from confused. This makes the difference explicit.
|
|
7
|
-
*/
|
|
8
2
|
export function isFinished(result) {
|
|
9
3
|
if (result.name !== "finish" || result.status !== "completed")
|
|
10
4
|
return false;
|
|
@@ -15,18 +9,11 @@ export function isFinished(result) {
|
|
|
15
9
|
return false;
|
|
16
10
|
}
|
|
17
11
|
}
|
|
18
|
-
/** Reads the answer out of a finished result, or null if it is not one. */
|
|
19
12
|
export function answerOf(result) {
|
|
20
13
|
if (!isFinished(result))
|
|
21
14
|
return null;
|
|
22
15
|
return JSON.parse(result.output).answer;
|
|
23
16
|
}
|
|
24
|
-
/**
|
|
25
|
-
* The model says it is done, and says what the answer is.
|
|
26
|
-
*
|
|
27
|
-
* Without it a loop ends by the model going quiet, which is indistinguishable
|
|
28
|
-
* from it losing the thread.
|
|
29
|
-
*/
|
|
30
17
|
export function finish() {
|
|
31
18
|
return tool({
|
|
32
19
|
name: "finish",
|
|
@@ -11,19 +11,11 @@ function isPrivateHost(host) {
|
|
|
11
11
|
return true;
|
|
12
12
|
return PRIVATE_V4.test(name);
|
|
13
13
|
}
|
|
14
|
-
/** Matches a host exactly, or any subdomain when the rule starts with a dot. */
|
|
15
14
|
function matches(host, rule) {
|
|
16
15
|
const h = host.toLowerCase();
|
|
17
16
|
const r = rule.toLowerCase();
|
|
18
17
|
return r.startsWith(".") ? h === r.slice(1) || h.endsWith(r) : h === r;
|
|
19
18
|
}
|
|
20
|
-
/**
|
|
21
|
-
* The URL a request may go to, or the reason it may not.
|
|
22
|
-
*
|
|
23
|
-
* An allowlist rather than a blocklist, and https only. A tool that fetches
|
|
24
|
-
* whatever a model names is how an agent reads a cloud metadata endpoint and
|
|
25
|
-
* hands back the credentials it finds.
|
|
26
|
-
*/
|
|
27
19
|
export function checkUrl(raw, allow) {
|
|
28
20
|
let url;
|
|
29
21
|
try {
|
|
@@ -2,16 +2,6 @@ import { tool } from "../../tool/index.js";
|
|
|
2
2
|
import { checkUrl } from "./allow.js";
|
|
3
3
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
4
4
|
const DEFAULT_MAX_CHARS = 100_000;
|
|
5
|
-
/**
|
|
6
|
-
* An HTTP GET or POST, to hosts you named.
|
|
7
|
-
*
|
|
8
|
-
* The allowlist is required because the alternative is a tool that fetches
|
|
9
|
-
* whatever a model is talked into naming. Headers are set here, not by the
|
|
10
|
-
* model, so a credential is never something it can redirect.
|
|
11
|
-
*
|
|
12
|
-
* Its `effect` is left unset on purpose: a GET is readonly and a POST to your
|
|
13
|
-
* allowlist may be anything at all, and one label cannot be true of both.
|
|
14
|
-
*/
|
|
15
5
|
export function httpRequest(opts) {
|
|
16
6
|
if (!opts.allow?.length) {
|
|
17
7
|
throw new Error("httpRequest needs an allow list of hosts; there is no safe default.");
|
|
@@ -1,13 +1,6 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
2
|
import { readPath } from "./path.js";
|
|
3
3
|
const DEFAULT_MAX_CHARS = 20_000;
|
|
4
|
-
/**
|
|
5
|
-
* Pulls one value out of a JSON document.
|
|
6
|
-
*
|
|
7
|
-
* The alternative is putting the whole reply in the context and asking the model
|
|
8
|
-
* to read it, which costs tokens on every turn and gets the field wrong when the
|
|
9
|
-
* document is long.
|
|
10
|
-
*/
|
|
11
4
|
export function jsonQuery(opts = {}) {
|
|
12
5
|
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
13
6
|
return tool({
|
|
@@ -36,13 +36,6 @@ function step(value, segment) {
|
|
|
36
36
|
}
|
|
37
37
|
return value;
|
|
38
38
|
}
|
|
39
|
-
/**
|
|
40
|
-
* Reads a value out of parsed JSON by path.
|
|
41
|
-
*
|
|
42
|
-
* `a.b`, `a[0].b`, and `a[*].b` for every element — enough to pull a field out
|
|
43
|
-
* of an API reply, and small enough that the syntax is guessable from one
|
|
44
|
-
* example, which matters when the thing writing the path is a model.
|
|
45
|
-
*/
|
|
46
39
|
export function readPath(document, path) {
|
|
47
40
|
let cursor = document;
|
|
48
41
|
let spread = false;
|
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { tool } from "../../tool/index.js";
|
|
2
|
-
/**
|
|
3
|
-
* Somewhere to reason without it becoming the answer.
|
|
4
|
-
*
|
|
5
|
-
* It does nothing, which is the point: the thought lands in the steps rather
|
|
6
|
-
* than in the reply, and a model given room to plan before a multi-step tool
|
|
7
|
-
* sequence uses the tools better.
|
|
8
|
-
*/
|
|
9
2
|
export function think() {
|
|
10
3
|
return tool({
|
|
11
4
|
name: "think",
|
|
@@ -5,14 +5,6 @@ function sources(raw) {
|
|
|
5
5
|
}
|
|
6
6
|
return (raw.citations ?? []).map((url) => ({ url }));
|
|
7
7
|
}
|
|
8
|
-
/**
|
|
9
|
-
* The provider's reply as the tool hands it back.
|
|
10
|
-
*
|
|
11
|
-
* Sources come from `search_results` where the account returns them and from
|
|
12
|
-
* `citations` otherwise, so one tool works across both reply shapes. Citation
|
|
13
|
-
* and reasoning tokens are their own line items on a grounded model, and
|
|
14
|
-
* `computeCostMicros` takes both — dropping them prices a paid call at zero.
|
|
15
|
-
*/
|
|
16
8
|
export function readReply(body, fallbackModel, maxChars, maxResults) {
|
|
17
9
|
const raw = body;
|
|
18
10
|
const answer = raw.choices?.[0]?.message?.content ?? "";
|
|
@@ -22,14 +22,6 @@ function describe(maxDepth, domains) {
|
|
|
22
22
|
: " deep is not enabled here.") +
|
|
23
23
|
(domains?.length ? ` Results come only from ${domains.join(", ")}.` : ""));
|
|
24
24
|
}
|
|
25
|
-
/**
|
|
26
|
-
* The live web, through Perplexity's Sonar models.
|
|
27
|
-
*
|
|
28
|
-
* The tier is the model's to choose and yours to cap: `sonar-deep-research` is
|
|
29
|
-
* minutes and orders of magnitude, and a model reaching for it unprompted is
|
|
30
|
-
* the failure `maxDepth` exists to prevent. Tokens come back rather than a
|
|
31
|
-
* price, because what a search costs is the pricing table's business.
|
|
32
|
-
*/
|
|
33
25
|
export function webSearch(opts) {
|
|
34
26
|
if (!opts.apiKey?.trim()) {
|
|
35
27
|
throw new Error("webSearch needs a Perplexity apiKey; it will not read one from the process.");
|
|
@@ -46,13 +46,6 @@ function taskText(input) {
|
|
|
46
46
|
task += deliverablesBlock(input.artifacts);
|
|
47
47
|
return task;
|
|
48
48
|
}
|
|
49
|
-
/**
|
|
50
|
-
* HTTP {@link CommissionClient} over Moa's task API (`POST /tasks` → `GET /tasks/:id`).
|
|
51
|
-
*
|
|
52
|
-
* The request body carries the ask as prose — Moa's artifact pipeline is
|
|
53
|
-
* prompt-driven and `POST /tasks` strips unknown fields, so context, design and
|
|
54
|
-
* deliverables are blocks on the task rather than parameters beside it.
|
|
55
|
-
*/
|
|
56
49
|
export function createCommissionClient(config = {}) {
|
|
57
50
|
const request = createRequest(config);
|
|
58
51
|
return {
|
|
@@ -92,12 +85,10 @@ export function createCommissionClient(config = {}) {
|
|
|
92
85
|
},
|
|
93
86
|
};
|
|
94
87
|
}
|
|
95
|
-
/** The sane ceiling for a chat-bound call. A rendered document wants minutes, not this. */
|
|
96
88
|
export const INTERACTIVE_TIMEOUT_MS = 30_000;
|
|
97
89
|
const DEFAULT_POLL_INTERVAL_MS = 2_000;
|
|
98
90
|
const SETTLE_ATTEMPTS = 3;
|
|
99
91
|
const LONGPOLL_WINDOW_MS = 25_000;
|
|
100
|
-
/** Thrown by {@link pollUntilDone} when the caller's abort signal fires. */
|
|
101
92
|
export class CommissionAbortedError extends Error {
|
|
102
93
|
commissionId;
|
|
103
94
|
constructor(commissionId) {
|
|
@@ -106,7 +97,6 @@ export class CommissionAbortedError extends Error {
|
|
|
106
97
|
this.commissionId = commissionId;
|
|
107
98
|
}
|
|
108
99
|
}
|
|
109
|
-
/** Thrown when the wait ran out. The task itself is still running server-side. */
|
|
110
100
|
export class CommissionTimeoutError extends Error {
|
|
111
101
|
commissionId;
|
|
112
102
|
timeoutMs;
|
|
@@ -126,7 +116,6 @@ const sleep = (ms, signal) => new Promise((resolve) => {
|
|
|
126
116
|
resolve();
|
|
127
117
|
}, ms);
|
|
128
118
|
});
|
|
129
|
-
/** Wait for a commission to settle, long-polling rather than tight-polling. */
|
|
130
119
|
export async function pollUntilDone(client, id, opts = {}) {
|
|
131
120
|
const timeoutMs = opts.timeoutMs ?? INTERACTIVE_TIMEOUT_MS;
|
|
132
121
|
const pollIntervalMs = opts.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* What a document looks like when nobody has said.
|
|
3
|
-
*
|
|
4
|
-
* Chosen to be defensible rather than distinctive: one typeface, one accent,
|
|
5
|
-
* real whitespace, and figures that carry a single message.
|
|
6
|
-
*/
|
|
7
1
|
export const DEFAULT_DESIGN = {
|
|
8
2
|
name: "Spendgraph house style",
|
|
9
3
|
fonts: { heading: "Inter", body: "Inter", mono: "JetBrains Mono" },
|
|
@@ -46,12 +40,6 @@ function mergeGroup(base, over) {
|
|
|
46
40
|
return base;
|
|
47
41
|
return { ...base, ...over };
|
|
48
42
|
}
|
|
49
|
-
/**
|
|
50
|
-
* A caller's guide over the defaults: named fields win, `rules` accumulate.
|
|
51
|
-
*
|
|
52
|
-
* Rules append because they are constraints — a brand adding "never use red"
|
|
53
|
-
* means it as well as the defaults, not instead of them.
|
|
54
|
-
*/
|
|
55
43
|
export function mergeDesign(base, over) {
|
|
56
44
|
if (!over)
|
|
57
45
|
return base;
|
|
@@ -64,7 +52,6 @@ export function mergeDesign(base, over) {
|
|
|
64
52
|
};
|
|
65
53
|
}
|
|
66
54
|
const line = (label, value) => (value ? [`${label}: ${value}`] : []);
|
|
67
|
-
/** The guide as the block Moa reads, since its artifact pipeline is prompt-driven. */
|
|
68
55
|
export function renderDesign(guide, format, notes) {
|
|
69
56
|
const { fonts, palette } = guide;
|
|
70
57
|
const parts = [
|
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
/** Where Moa lives, unless a config says otherwise. */
|
|
2
1
|
export const MOA_BASE_URL = "https://api.fnmoa.com";
|
|
3
2
|
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
4
3
|
const ERROR_DETAIL_CHARS = 500;
|
|
5
|
-
/** A non-2xx from Moa, carrying the status so a caller can tell 4xx from 5xx. */
|
|
6
4
|
export class MoaApiError extends Error {
|
|
7
5
|
status;
|
|
8
6
|
detail;
|
|
@@ -37,7 +35,6 @@ function deadlineSignal(timeoutMs, caller) {
|
|
|
37
35
|
},
|
|
38
36
|
};
|
|
39
37
|
}
|
|
40
|
-
/** One `request<T>(path, opts)` against Moa, with auth, timeout and error shape settled. */
|
|
41
38
|
export function createRequest(config = {}) {
|
|
42
39
|
const doFetch = config.fetch ?? globalThis.fetch;
|
|
43
40
|
const base = (config.baseUrl ?? MOA_BASE_URL).replace(/\/+$/, "");
|
|
@@ -48,14 +48,6 @@ function stillRendering(id, style, err) {
|
|
|
48
48
|
"say so rather than commissioning it again.",
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
|
-
/**
|
|
52
|
-
* A document agent, briefed in your house style.
|
|
53
|
-
*
|
|
54
|
-
* Style is the deployment's business and content is the model's: the guide
|
|
55
|
-
* comes from `options.design`, and `design_notes` may bend it for one document
|
|
56
|
-
* rather than replace it. `effect` is `destructive` because a second call is a
|
|
57
|
-
* second bill and a second set of files — never something to retry blindly.
|
|
58
|
-
*/
|
|
59
51
|
export function writeDocument(opts) {
|
|
60
52
|
if (!opts.client && !opts.apiKey?.trim()) {
|
|
61
53
|
throw new Error("writeDocument needs a Moa apiKey; it will not read one from the process.");
|
package/dist/bus/bus.js
CHANGED
|
@@ -4,16 +4,6 @@ import { warnOnOverlap } from "./overlap.js";
|
|
|
4
4
|
import { selectTools } from "./select.js";
|
|
5
5
|
import { toStep } from "./step.js";
|
|
6
6
|
import { newTrace } from "./trace.js";
|
|
7
|
-
/**
|
|
8
|
-
* The tools an agent can reach, and how it reaches them.
|
|
9
|
-
*
|
|
10
|
-
* Holding them in one place is what makes selection possible. Fifty tools at
|
|
11
|
-
* roughly 150 tokens of declaration each is 7.5k tokens on every call before the
|
|
12
|
-
* user has said anything, so past a point you stop sending all of them.
|
|
13
|
-
*
|
|
14
|
-
* Selection is keyword and usage, deliberately: no embedding provider, no extra
|
|
15
|
-
* call, no latency. It is also the baseline that says what embeddings would buy.
|
|
16
|
-
*/
|
|
17
7
|
export function toolbus(tools, opts = {}) {
|
|
18
8
|
const now = opts.now ?? (() => Date.now());
|
|
19
9
|
const ceiling = Math.max(1, opts.limit ?? 6);
|
|
@@ -23,7 +13,6 @@ export function toolbus(tools, opts = {}) {
|
|
|
23
13
|
throw new Error(`Two tools are called "${t.name}".`);
|
|
24
14
|
byName.set(t.name, t);
|
|
25
15
|
}
|
|
26
|
-
/** How often each has been called, so the useful ones stay offered. */
|
|
27
16
|
const calls = new Map();
|
|
28
17
|
if (opts.warnOnOverlap ?? true)
|
|
29
18
|
warnOnOverlap([...byName.values()]);
|
|
@@ -35,68 +24,25 @@ export function toolbus(tools, opts = {}) {
|
|
|
35
24
|
});
|
|
36
25
|
const all = () => [...byName.values()].map(declare);
|
|
37
26
|
const bus = {
|
|
38
|
-
/** Every tool, in declaration order. */
|
|
39
27
|
all: () => [...byName.values()],
|
|
40
|
-
/**
|
|
41
|
-
* Names something expects that this bus does not have.
|
|
42
|
-
*
|
|
43
|
-
* The failure it catches is silent: a prompt edited to expect a `refund`
|
|
44
|
-
* tool, deployed against a bus without one, does not error. The agent is
|
|
45
|
-
* never offered it and answers anyway.
|
|
46
|
-
*/
|
|
47
28
|
missing: (names) => names.filter((n) => !byName.has(n)),
|
|
48
29
|
get: (name) => byName.get(name),
|
|
49
|
-
/**
|
|
50
|
-
* What one tool declares calling it does. `undefined` when it never said.
|
|
51
|
-
*
|
|
52
|
-
* A gate reading this must decide what unknown means for it. It is not
|
|
53
|
-
* `readonly` — nothing has claimed that.
|
|
54
|
-
*/
|
|
55
30
|
effectOf: (name) => byName.get(name)?.effect,
|
|
56
|
-
/**
|
|
57
|
-
* Names of the tools declared `destructive`.
|
|
58
|
-
*
|
|
59
|
-
* Only the ones that said so. Pair it with `unannotated` before treating
|
|
60
|
-
* the rest as safe, or a tool nobody got round to labelling passes a gate
|
|
61
|
-
* built on this.
|
|
62
|
-
*/
|
|
63
31
|
destructive: () => [...byName.values()].filter((t) => t.effect === "destructive").map((t) => t.name),
|
|
64
|
-
/** Names of the tools with no effect declared, so a gate can refuse to guess. */
|
|
65
32
|
unannotated: () => [...byName.values()].filter((t) => t.effect === undefined).map((t) => t.name),
|
|
66
|
-
/** Declarations for the model. Neutral shape; adapt per provider. */
|
|
67
33
|
declarations: (names) => names
|
|
68
34
|
? names
|
|
69
35
|
.map((n) => byName.get(n))
|
|
70
36
|
.filter(Boolean)
|
|
71
37
|
.map((t) => declare(t))
|
|
72
38
|
: all(),
|
|
73
|
-
/**
|
|
74
|
-
* Everything, in the shape the Anthropic Messages API takes.
|
|
75
|
-
*
|
|
76
|
-
* Selection is not applied for you — pass a shortlist to narrow it. The
|
|
77
|
-
* `tools` array sits ahead of the messages in the cached prefix, so a list
|
|
78
|
-
* that changes every turn invalidates the system prompt with it, and hiding
|
|
79
|
-
* the `select` call in here would hide that.
|
|
80
|
-
*/
|
|
81
39
|
anthropic: (decls) => toAnthropic(decls ?? all()),
|
|
82
|
-
/** Everything, in the shape the OpenAI chat completions API takes. */
|
|
83
40
|
openai: (decls) => toOpenAI(decls ?? all()),
|
|
84
|
-
/**
|
|
85
|
-
* Everything as markdown, to append to a rendered system prompt.
|
|
86
|
-
*
|
|
87
|
-
* For a model with no tools API. Where there is one, prefer it: this is
|
|
88
|
-
* cheaper in tokens and worse in every other way.
|
|
89
|
-
*/
|
|
90
41
|
markdown: (decls, heading) => toMarkdown(decls ?? all(), heading),
|
|
91
|
-
/** The working set for one request. */
|
|
92
42
|
select: (query = "", limit) => selectTools([...byName.values()], calls, declare, query, Math.max(1, limit ?? ceiling)),
|
|
93
|
-
/** Runs one, validating its arguments first. Never throws. */
|
|
94
43
|
invoke: (name, args) => invokeTool(byName, calls, now, name, args),
|
|
95
|
-
/** How many times each has been called on this bus. */
|
|
96
44
|
usage: () => Object.fromEntries(calls),
|
|
97
|
-
/** A result as a rollout step, for `report({ steps })`. */
|
|
98
45
|
step: (result, index) => toStep(result, index),
|
|
99
|
-
/** One turn's tool use, selected once and recorded for the rollout. */
|
|
100
46
|
trace: (query = "", limit) => newTrace(bus.select(query, limit), (name, args) => bus.invoke(name, args)),
|
|
101
47
|
};
|
|
102
48
|
return bus;
|
package/dist/bus/invoke.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
1
|
import { FieldValidationError, validateFields } from "@spendgraph/sdk";
|
|
2
|
-
/**
|
|
3
|
-
* Runs one tool, validating its arguments first.
|
|
4
|
-
*
|
|
5
|
-
* Never throws. A tool that fails is an outcome the agent can react to and a
|
|
6
|
-
* step worth recording, not an exception that ends the run — the same argument
|
|
7
|
-
* `status` and `error` exist on a rollout for.
|
|
8
|
-
*/
|
|
9
2
|
export async function invokeTool(byName, calls, now, name, args) {
|
|
10
3
|
const started = now();
|
|
11
4
|
const found = byName.get(name);
|
package/dist/bus/overlap.js
CHANGED
|
@@ -1,22 +1,10 @@
|
|
|
1
|
-
/** Words long enough to carry meaning, for comparing two descriptions. */
|
|
2
1
|
function words(tool) {
|
|
3
2
|
return new Set(tool.description
|
|
4
3
|
.toLowerCase()
|
|
5
4
|
.split(/[^a-z0-9]+/)
|
|
6
5
|
.filter((w) => w.length > 3));
|
|
7
6
|
}
|
|
8
|
-
/**
|
|
9
|
-
* Warns once about tools that describe themselves the same way.
|
|
10
|
-
*
|
|
11
|
-
* Two near-identical descriptions are worse than one tool: the model cannot tell
|
|
12
|
-
* them apart and picks wrong more often, with nothing failing. Said at
|
|
13
|
-
* construction because it is invisible at runtime.
|
|
14
|
-
*
|
|
15
|
-
* One line however many pairs. A line each buries whatever else the process said
|
|
16
|
-
* at startup and teaches people to switch the check off.
|
|
17
|
-
*/
|
|
18
7
|
export function warnOnOverlap(tools) {
|
|
19
|
-
// Built once per tool, not once per pair — sixty tools is 1,770 pairs.
|
|
20
8
|
const sets = tools.map(words);
|
|
21
9
|
const named = [];
|
|
22
10
|
let pairs = 0;
|
package/dist/bus/score.js
CHANGED
|
@@ -1,10 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Overlap between the query and a tool's own words.
|
|
3
|
-
*
|
|
4
|
-
* The name counts for more than the description: "refund" in a question is a
|
|
5
|
-
* stronger signal about the `refund` tool than the same word buried in a
|
|
6
|
-
* paragraph about it.
|
|
7
|
-
*/
|
|
8
1
|
export function score(tool, terms) {
|
|
9
2
|
const name = tool.name.toLowerCase().replace(/_/g, " ");
|
|
10
3
|
const body = `${tool.description} ${tool.args.map((a) => `${a.name} ${a.description ?? ""}`).join(" ")}`.toLowerCase();
|
package/dist/bus/select.js
CHANGED
|
@@ -1,20 +1,4 @@
|
|
|
1
1
|
import { score } from "./score.js";
|
|
2
|
-
/**
|
|
3
|
-
* The working set for one request: pinned, then what the query matches, then
|
|
4
|
-
* what actually gets used.
|
|
5
|
-
*
|
|
6
|
-
* Relevance before usage, which is the opposite of what this did first. At a
|
|
7
|
-
* shortlist of twelve the order was harmless; at six, three previously-used
|
|
8
|
-
* tools take half the slots and push out the tool the query needs.
|
|
9
|
-
*
|
|
10
|
-
* Usage still earns its place as the tiebreaker among equal matches, and as the
|
|
11
|
-
* fallback when nothing matches — the case where an agent would otherwise be
|
|
12
|
-
* handed nothing.
|
|
13
|
-
*
|
|
14
|
-
* How many come back depends on how clearly the query matched. A single strong
|
|
15
|
-
* match needs no company; a flat spread means the ranking does not know, and a
|
|
16
|
-
* wider net is worth the distraction.
|
|
17
|
-
*/
|
|
18
2
|
export function selectTools(tools, calls, declare, query, ceiling) {
|
|
19
3
|
const terms = query
|
|
20
4
|
.toLowerCase()
|
|
@@ -25,8 +9,6 @@ export function selectTools(tools, calls, declare, query, ceiling) {
|
|
|
25
9
|
if (!chosen.includes(t) && chosen.length < ceiling)
|
|
26
10
|
chosen.push(t);
|
|
27
11
|
};
|
|
28
|
-
// Counted against the ceiling like everything else: what is in front of the
|
|
29
|
-
// model is what costs accuracy, whoever put it there.
|
|
30
12
|
for (const t of tools)
|
|
31
13
|
if (t.pinned)
|
|
32
14
|
take(t);
|
|
@@ -35,10 +17,6 @@ export function selectTools(tools, calls, declare, query, ceiling) {
|
|
|
35
17
|
.map((t) => ({ t, n: score(t, terms) }))
|
|
36
18
|
.filter((x) => x.n > 0)
|
|
37
19
|
.sort((a, b) => b.n - a.n || (calls.get(b.t.name) ?? 0) - (calls.get(a.t.name) ?? 0));
|
|
38
|
-
// The depth applies to the slots selection is free to fill, not the whole
|
|
39
|
-
// list. Measured against the whole list it was a bug: four pinned tools
|
|
40
|
-
// already exceed a shortened target of three, so the loop below broke on its
|
|
41
|
-
// first iteration and dropped an exact name match with slots still free.
|
|
42
20
|
const room = ceiling - chosen.length;
|
|
43
21
|
const decisive = ranked.length > 1 && ranked[0].n >= ranked[1].n * 2;
|
|
44
22
|
const budget = chosen.length + (decisive ? Math.min(3, room) : room);
|
|
@@ -47,8 +25,6 @@ export function selectTools(tools, calls, declare, query, ceiling) {
|
|
|
47
25
|
break;
|
|
48
26
|
take(t);
|
|
49
27
|
}
|
|
50
|
-
// Up to the budget, not the ceiling — padding a decisive match back out to
|
|
51
|
-
// the full limit would undo the shortening entirely.
|
|
52
28
|
const byUse = [...rest].sort((a, b) => (calls.get(b.name) ?? 0) - (calls.get(a.name) ?? 0));
|
|
53
29
|
for (const t of byUse) {
|
|
54
30
|
if (chosen.length >= budget)
|
|
@@ -57,9 +33,6 @@ export function selectTools(tools, calls, declare, query, ceiling) {
|
|
|
57
33
|
break;
|
|
58
34
|
take(t);
|
|
59
35
|
}
|
|
60
|
-
// Only when the query singled out nothing. An agent handed no tools cannot
|
|
61
|
-
// act; an agent handed four unrelated ones alongside the obvious answer just
|
|
62
|
-
// has more to get wrong.
|
|
63
36
|
if (ranked.length === 0) {
|
|
64
37
|
for (const t of rest) {
|
|
65
38
|
if (chosen.length >= ceiling)
|
package/dist/bus/step.js
CHANGED
package/dist/bus/trace.js
CHANGED
|
@@ -1,17 +1,5 @@
|
|
|
1
1
|
import { toAnthropic, toMarkdown, toOpenAI, } from "../wire/index.js";
|
|
2
2
|
import { toStep } from "./step.js";
|
|
3
|
-
/**
|
|
4
|
-
* One turn's worth of tool use, holding the bookkeeping a rollout needs.
|
|
5
|
-
*
|
|
6
|
-
* Two things must survive from the moment a shortlist is chosen to the moment
|
|
7
|
-
* the run is reported: which tools were offered, and what each call did in
|
|
8
|
-
* order. Losing the first is worse than it sounds — a rollout that says the
|
|
9
|
-
* agent never called `refund` cannot distinguish "chose not to" from "was never
|
|
10
|
-
* offered it", which are opposite bugs.
|
|
11
|
-
*
|
|
12
|
-
* Selection happens once, here, and the same list is what gets reported. A trace
|
|
13
|
-
* cannot disagree with itself about what the model saw.
|
|
14
|
-
*/
|
|
15
3
|
export function newTrace(offered, invoke) {
|
|
16
4
|
const steps = [];
|
|
17
5
|
return {
|
package/dist/internals.js
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The pieces `toolbus` is built from.
|
|
3
|
-
*
|
|
4
|
-
* Not in the main entry: a bus already wires all of it, and a caller reaching
|
|
5
|
-
* here is building their own. Kept exported so that stays possible.
|
|
6
|
-
*/
|
|
7
1
|
export { evaluate } from "./builtin/calculate/index.js";
|
|
8
2
|
export { between, parseDate, shift } from "./builtin/date/index.js";
|
|
9
3
|
export { checkUrl } from "./builtin/http/index.js";
|
package/dist/tool/bind.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { tool } from "./tool.js";
|
|
2
|
-
/** The stored declaration disagrees with the one this code was written against. */
|
|
3
2
|
export class ToolDriftError extends Error {
|
|
4
3
|
toolName;
|
|
5
4
|
differences;
|
|
@@ -10,7 +9,6 @@ export class ToolDriftError extends Error {
|
|
|
10
9
|
this.name = "ToolDriftError";
|
|
11
10
|
}
|
|
12
11
|
}
|
|
13
|
-
/** Nothing is stored under that name, so there is no wording to bind to. */
|
|
14
12
|
export class ToolNotStoredError extends Error {
|
|
15
13
|
toolName;
|
|
16
14
|
constructor(toolName) {
|
|
@@ -19,14 +17,6 @@ export class ToolNotStoredError extends Error {
|
|
|
19
17
|
this.name = "ToolNotStoredError";
|
|
20
18
|
}
|
|
21
19
|
}
|
|
22
|
-
/**
|
|
23
|
-
* Every way the stored arguments differ from the declared ones.
|
|
24
|
-
*
|
|
25
|
-
* Compared by name, type and requiredness — the three a handler is written
|
|
26
|
-
* against. A description or a bound that differs is the dashboard being edited,
|
|
27
|
-
* which is the point; a renamed argument is a handler about to be passed
|
|
28
|
-
* `undefined` halfway through an agent loop.
|
|
29
|
-
*/
|
|
30
20
|
function drift(declared, stored) {
|
|
31
21
|
const out = [];
|
|
32
22
|
const byName = new Map(stored.map((a) => [a.name, a]));
|
|
@@ -49,27 +39,12 @@ function drift(declared, stored) {
|
|
|
49
39
|
}
|
|
50
40
|
return out;
|
|
51
41
|
}
|
|
52
|
-
/**
|
|
53
|
-
* A stored declaration bound to the function that runs it.
|
|
54
|
-
*
|
|
55
|
-
* The dashboard owns the description, the effect and the pin — the parts you
|
|
56
|
-
* iterate on and want live without a release. This code owns the arguments and
|
|
57
|
-
* the handler, and the two are checked against each other on the way through:
|
|
58
|
-
* a rename in the dashboard fails here, at bind time, rather than as an
|
|
59
|
-
* `undefined` mid-loop.
|
|
60
|
-
*
|
|
61
|
-
* Declare `args` with `as const` as you would for `tool()`, or inference falls
|
|
62
|
-
* back to `Record<string, unknown>` and the check above is the only thing left
|
|
63
|
-
* catching a rename.
|
|
64
|
-
*/
|
|
65
42
|
export async function bindTool(sg, spec, query = {}) {
|
|
66
43
|
let stored;
|
|
67
44
|
try {
|
|
68
45
|
({ tool: stored } = await sg.tools.get(spec.name, query));
|
|
69
46
|
}
|
|
70
47
|
catch (err) {
|
|
71
|
-
// A 404 is the ordinary case of "you have not written this one yet", and
|
|
72
|
-
// saying so beats a status code from three layers down.
|
|
73
48
|
if (err && typeof err === "object" && "status" in err && err.status === 404) {
|
|
74
49
|
throw new ToolNotStoredError(spec.name);
|
|
75
50
|
}
|
package/dist/tool/tool.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/** Names a provider will accept: letters, digits and underscores. */
|
|
2
1
|
const VALID_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3
2
|
const EFFECTS = ["readonly", "idempotent", "destructive"];
|
|
4
3
|
function checkEnum(toolName, arg) {
|
|
@@ -30,16 +29,6 @@ function checkArgs(toolName, args) {
|
|
|
30
29
|
checkEnum(toolName, arg);
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
|
-
/**
|
|
34
|
-
* Declares a tool, checking what is a silent failure otherwise.
|
|
35
|
-
*
|
|
36
|
-
* Every check here is something a provider either rejects opaquely or accepts
|
|
37
|
-
* and behaves oddly about. Caught at import, not on the first loop in prod.
|
|
38
|
-
*
|
|
39
|
-
* Declare `args` with `as const` and `run` is typed from them — rename an
|
|
40
|
-
* argument and the handler stops compiling, rather than being handed
|
|
41
|
-
* `undefined` halfway through an agent loop.
|
|
42
|
-
*/
|
|
43
32
|
export function tool(spec) {
|
|
44
33
|
if (!VALID_NAME.test(spec.name)) {
|
|
45
34
|
throw new Error(`Tool name "${spec.name}" must be letters, digits and underscores, starting with a letter.`);
|
package/dist/wire/json-schema.js
CHANGED
|
@@ -7,16 +7,6 @@ export const JSON_TYPE = {
|
|
|
7
7
|
list: "array",
|
|
8
8
|
json: "object",
|
|
9
9
|
};
|
|
10
|
-
/**
|
|
11
|
-
* A field's default as a value of its own type.
|
|
12
|
-
*
|
|
13
|
-
* `FieldSpec.default` is already-rendered text, so `"5"` on a number field has
|
|
14
|
-
* to come back as `5` — a schema saying `number` and defaulting to `"5"` is
|
|
15
|
-
* something providers accept and models then copy.
|
|
16
|
-
*
|
|
17
|
-
* `list` and `json` defaults are joined and printed forms that cannot be parsed
|
|
18
|
-
* back, so they are dropped rather than guessed at.
|
|
19
|
-
*/
|
|
20
10
|
function coerceDefault(field) {
|
|
21
11
|
const raw = field.default;
|
|
22
12
|
if (raw === undefined)
|
|
@@ -43,12 +33,6 @@ function coerceDefault(field) {
|
|
|
43
33
|
return raw;
|
|
44
34
|
}
|
|
45
35
|
}
|
|
46
|
-
/**
|
|
47
|
-
* `FieldSpec[]` as a JSON Schema object.
|
|
48
|
-
*
|
|
49
|
-
* `separator`, `trueText` and `falseText` are dropped: they interpolate a value
|
|
50
|
-
* into a string, which a tool call does not do. So is `datasetKey`.
|
|
51
|
-
*/
|
|
52
36
|
export function toJsonSchema(args) {
|
|
53
37
|
const properties = {};
|
|
54
38
|
for (const field of args) {
|
package/dist/wire/markdown.js
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
import { JSON_TYPE } from "./json-schema.js";
|
|
2
|
-
/**
|
|
3
|
-
* Neutralises headings inside text about to become markdown.
|
|
4
|
-
*
|
|
5
|
-
* A `###` at the start of a line would close the tool's own section and open one
|
|
6
|
-
* the prompt author never wrote.
|
|
7
|
-
*/
|
|
8
2
|
function escapeHeadings(text) {
|
|
9
3
|
return text.replace(/^(\s*)(#{1,6})(\s|$)/gm, "$1\\$2$3");
|
|
10
4
|
}
|
|
@@ -24,13 +18,6 @@ function argLine(field) {
|
|
|
24
18
|
const head = `- \`${field.name}\` (${type}${field.required ? ", required" : ""})`;
|
|
25
19
|
return notes.length ? `${head} ${notes.join(". ")}.` : `${head}`;
|
|
26
20
|
}
|
|
27
|
-
/**
|
|
28
|
-
* Declarations as markdown, for a model with no tools API.
|
|
29
|
-
*
|
|
30
|
-
* About a quarter cheaper than the JSON, since JSON Schema is mostly
|
|
31
|
-
* scaffolding — but you give up structured tool calls and parse intent out of
|
|
32
|
-
* prose instead. Only worth it where there is no native `tools` array.
|
|
33
|
-
*/
|
|
34
21
|
export function toMarkdown(decls, heading = "## Tools") {
|
|
35
22
|
if (decls.length === 0)
|
|
36
23
|
return "";
|
package/dist/wire/providers.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { toJsonSchema } from "./json-schema.js";
|
|
2
|
-
/** Declarations in the shape the Anthropic Messages API takes. */
|
|
3
2
|
export function toAnthropic(decls) {
|
|
4
3
|
return decls.map((d) => ({
|
|
5
4
|
name: d.name,
|
|
@@ -7,7 +6,6 @@ export function toAnthropic(decls) {
|
|
|
7
6
|
input_schema: toJsonSchema(d.args),
|
|
8
7
|
}));
|
|
9
8
|
}
|
|
10
|
-
/** Declarations in the shape the OpenAI chat completions API takes. */
|
|
11
9
|
export function toOpenAI(decls) {
|
|
12
10
|
return decls.map((d) => ({
|
|
13
11
|
type: "function",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@spendgraph/tools",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Declare a tool once, offer the right few, and record what was called.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -43,16 +43,16 @@
|
|
|
43
43
|
"README.md"
|
|
44
44
|
],
|
|
45
45
|
"scripts": {
|
|
46
|
-
"build": "tsc -p tsconfig.json",
|
|
46
|
+
"build": "tsc -p tsconfig.json --emitDeclarationOnly && tsc -p tsconfig.json --declaration false --removeComments",
|
|
47
47
|
"test": "vitest run",
|
|
48
48
|
"examples": "npm run build && node examples/02-select/01-shortlist.mjs"
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@locusgraph/client": "^0.8.1",
|
|
52
|
-
"@spendgraph/sdk": "^0.
|
|
52
|
+
"@spendgraph/sdk": "^0.3.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@spendgraph/prompt": "^0.
|
|
55
|
+
"@spendgraph/prompt": "^0.3.0",
|
|
56
56
|
"typescript": "^5"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|