@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
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/** Where Moa lives, unless a config says otherwise. */
|
|
2
|
+
export const MOA_BASE_URL = "https://api.fnmoa.com";
|
|
3
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
|
|
4
|
+
const ERROR_DETAIL_CHARS = 500;
|
|
5
|
+
/** A non-2xx from Moa, carrying the status so a caller can tell 4xx from 5xx. */
|
|
6
|
+
export class MoaApiError extends Error {
|
|
7
|
+
status;
|
|
8
|
+
detail;
|
|
9
|
+
constructor(message, status, detail) {
|
|
10
|
+
super(detail ? `${message}: ${detail}` : message);
|
|
11
|
+
this.name = "MoaApiError";
|
|
12
|
+
this.status = status;
|
|
13
|
+
this.detail = detail;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function reasonOf(err) {
|
|
17
|
+
const cause = err.cause;
|
|
18
|
+
return cause?.code ?? cause?.message ?? err.message;
|
|
19
|
+
}
|
|
20
|
+
function deadlineSignal(timeoutMs, caller) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const stop = () => controller.abort();
|
|
23
|
+
let expired = false;
|
|
24
|
+
const timer = setTimeout(() => {
|
|
25
|
+
expired = true;
|
|
26
|
+
stop();
|
|
27
|
+
}, timeoutMs);
|
|
28
|
+
caller?.addEventListener("abort", stop, { once: true });
|
|
29
|
+
if (caller?.aborted)
|
|
30
|
+
stop();
|
|
31
|
+
return {
|
|
32
|
+
signal: controller.signal,
|
|
33
|
+
expired: () => expired,
|
|
34
|
+
release: () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
caller?.removeEventListener("abort", stop);
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
/** One `request<T>(path, opts)` against Moa, with auth, timeout and error shape settled. */
|
|
41
|
+
export function createRequest(config = {}) {
|
|
42
|
+
const doFetch = config.fetch ?? globalThis.fetch;
|
|
43
|
+
const base = (config.baseUrl ?? MOA_BASE_URL).replace(/\/+$/, "");
|
|
44
|
+
return async function request(path, opts = {}) {
|
|
45
|
+
const timeoutMs = opts.timeoutMs ?? config.timeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
46
|
+
const method = opts.method ?? "GET";
|
|
47
|
+
const url = `${base}${path}`;
|
|
48
|
+
const { signal, expired, release } = deadlineSignal(timeoutMs, opts.signal);
|
|
49
|
+
try {
|
|
50
|
+
const res = await doFetch(url, {
|
|
51
|
+
method,
|
|
52
|
+
signal,
|
|
53
|
+
headers: {
|
|
54
|
+
...(config.apiKey ? { authorization: `Bearer ${config.apiKey}` } : {}),
|
|
55
|
+
...(opts.body === undefined ? {} : { "content-type": "application/json" }),
|
|
56
|
+
},
|
|
57
|
+
...(opts.body === undefined ? {} : { body: JSON.stringify(opts.body) }),
|
|
58
|
+
}).catch((err) => {
|
|
59
|
+
if (opts.signal?.aborted)
|
|
60
|
+
throw err;
|
|
61
|
+
if (expired())
|
|
62
|
+
throw new MoaApiError(`Moa did not answer ${method} ${path}`, 504, `${timeoutMs}ms`);
|
|
63
|
+
throw new MoaApiError(`Moa is unreachable at ${url}`, 0, reasonOf(err));
|
|
64
|
+
});
|
|
65
|
+
if (!res.ok) {
|
|
66
|
+
throw new MoaApiError(`Moa returned ${res.status} for ${method} ${path}`, res.status, (await res.text()).slice(0, ERROR_DETAIL_CHARS));
|
|
67
|
+
}
|
|
68
|
+
return res.status === 204 ? undefined : (await res.json());
|
|
69
|
+
}
|
|
70
|
+
finally {
|
|
71
|
+
release();
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { CommissionClient, PollOptions } from "./commission.js";
|
|
2
|
+
export { CommissionAbortedError, CommissionTimeoutError, createCommissionClient, INTERACTIVE_TIMEOUT_MS, pollUntilDone, } from "./commission.js";
|
|
3
|
+
export { DEFAULT_DESIGN, type DesignGuide, mergeDesign, renderDesign } from "./design.js";
|
|
4
|
+
export { MOA_BASE_URL, MoaApiError, type MoaClientConfig } from "./http.js";
|
|
5
|
+
export type { ArtifactFormat, CommissionArtifact, CommissionInput, CommissionResult, CommissionStatus, } from "./types.js";
|
|
6
|
+
export { type WriteDocumentOptions, writeDocument } from "./write-document.js";
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { CommissionAbortedError, CommissionTimeoutError, createCommissionClient, INTERACTIVE_TIMEOUT_MS, pollUntilDone, } from "./commission.js";
|
|
2
|
+
export { DEFAULT_DESIGN, mergeDesign, renderDesign } from "./design.js";
|
|
3
|
+
export { MOA_BASE_URL, MoaApiError } from "./http.js";
|
|
4
|
+
export { writeDocument } from "./write-document.js";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/** A document format Moa can leave behind in `artifacts/`. */
|
|
2
|
+
export type ArtifactFormat = "md" | "pdf" | "docx" | "pptx";
|
|
3
|
+
/** Where a commission has got to. `pending` is queued, `failed` is settled. */
|
|
4
|
+
export type CommissionStatus = "pending" | "running" | "done" | "failed";
|
|
5
|
+
/** One finished file, either downloadable or carried inline as base64. */
|
|
6
|
+
export interface CommissionArtifact {
|
|
7
|
+
name: string;
|
|
8
|
+
format: ArtifactFormat;
|
|
9
|
+
url?: string;
|
|
10
|
+
data?: string;
|
|
11
|
+
mime?: string;
|
|
12
|
+
sizeBytes?: number;
|
|
13
|
+
}
|
|
14
|
+
/** What to commission: the ask, the material it rests on, and the house style. */
|
|
15
|
+
export interface CommissionInput {
|
|
16
|
+
task: string;
|
|
17
|
+
context?: string;
|
|
18
|
+
design?: string;
|
|
19
|
+
artifacts?: readonly ArtifactFormat[];
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
newSessionId?: string;
|
|
22
|
+
}
|
|
23
|
+
/** A commission as it stands right now. */
|
|
24
|
+
export interface CommissionResult {
|
|
25
|
+
id: string;
|
|
26
|
+
status: CommissionStatus;
|
|
27
|
+
output?: string;
|
|
28
|
+
error?: string;
|
|
29
|
+
costUsd?: number;
|
|
30
|
+
numTurns?: number;
|
|
31
|
+
sessionId?: string;
|
|
32
|
+
artifacts?: CommissionArtifact[];
|
|
33
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { type CommissionClient } from "./commission.js";
|
|
2
|
+
import { type DesignGuide } from "./design.js";
|
|
3
|
+
import type { AbortSignalLike } from "./http.js";
|
|
4
|
+
import type { ArtifactFormat } from "./types.js";
|
|
5
|
+
export interface WriteDocumentOptions {
|
|
6
|
+
/** Moa key. Required, and read from here rather than the environment. */
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/** Defaults to `https://api.fnmoa.com`. */
|
|
9
|
+
baseUrl?: string;
|
|
10
|
+
/**
|
|
11
|
+
* House style laid over {@link DEFAULT_DESIGN}: named fields win, `rules`
|
|
12
|
+
* accumulate. This is where a brand goes, so the model cannot dilute it.
|
|
13
|
+
*/
|
|
14
|
+
design?: DesignGuide;
|
|
15
|
+
/** Formats this deployment will produce. Default all four. */
|
|
16
|
+
formats?: readonly ArtifactFormat[];
|
|
17
|
+
/** Give up waiting after this. Default 600000 — rendering is minutes, not seconds. */
|
|
18
|
+
timeoutMs?: number;
|
|
19
|
+
/** Stops the wait. With `cancelOnAbort`, stops the task too. */
|
|
20
|
+
signal?: AbortSignalLike;
|
|
21
|
+
/** Cancel the task server-side when `signal` fires. Default true. */
|
|
22
|
+
cancelOnAbort?: boolean;
|
|
23
|
+
/** Return artifact bytes inline. Default false: base64 of a PDF is not context. */
|
|
24
|
+
includeData?: boolean;
|
|
25
|
+
/** Longest write-up handed back. Default 4000 characters. */
|
|
26
|
+
maxChars?: number;
|
|
27
|
+
/** Injected for tests, and for anyone fronting Moa with their own transport. */
|
|
28
|
+
client?: CommissionClient;
|
|
29
|
+
/** Injected for tests. */
|
|
30
|
+
fetch?: typeof fetch;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A document agent, briefed in your house style.
|
|
34
|
+
*
|
|
35
|
+
* Style is the deployment's business and content is the model's: the guide
|
|
36
|
+
* comes from `options.design`, and `design_notes` may bend it for one document
|
|
37
|
+
* rather than replace it. `effect` is `destructive` because a second call is a
|
|
38
|
+
* second bill and a second set of files — never something to retry blindly.
|
|
39
|
+
*/
|
|
40
|
+
export declare function writeDocument(opts: WriteDocumentOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
|
|
41
|
+
readonly name: "brief";
|
|
42
|
+
readonly type: "text";
|
|
43
|
+
readonly required: true;
|
|
44
|
+
readonly maxLength: 8000;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
}, {
|
|
47
|
+
readonly name: "format";
|
|
48
|
+
readonly type: "enum";
|
|
49
|
+
readonly required: true;
|
|
50
|
+
readonly options: readonly ["pdf", "pptx", "docx", "md"];
|
|
51
|
+
readonly description: "pptx for anything presented, pdf for anything sent, docx for anything edited.";
|
|
52
|
+
}, {
|
|
53
|
+
readonly name: "source";
|
|
54
|
+
readonly type: "text";
|
|
55
|
+
readonly required: false;
|
|
56
|
+
readonly maxLength: 40000;
|
|
57
|
+
readonly description: string;
|
|
58
|
+
}, {
|
|
59
|
+
readonly name: "design_notes";
|
|
60
|
+
readonly type: "text";
|
|
61
|
+
readonly required: false;
|
|
62
|
+
readonly maxLength: 2000;
|
|
63
|
+
readonly description: string;
|
|
64
|
+
}, {
|
|
65
|
+
readonly name: "session_id";
|
|
66
|
+
readonly type: "string";
|
|
67
|
+
readonly required: false;
|
|
68
|
+
readonly description: string;
|
|
69
|
+
}]>>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { tool } from "../../tool/index.js";
|
|
2
|
+
import { CommissionAbortedError, CommissionTimeoutError, createCommissionClient, pollUntilDone, } from "./commission.js";
|
|
3
|
+
import { DEFAULT_DESIGN, mergeDesign, renderDesign } from "./design.js";
|
|
4
|
+
const ALL_FORMATS = ["pdf", "pptx", "docx", "md"];
|
|
5
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
6
|
+
const DEFAULT_SETTLE_MS = 2_000;
|
|
7
|
+
const DEFAULT_MAX_CHARS = 4_000;
|
|
8
|
+
function describe(formats, design) {
|
|
9
|
+
return ("Commissions a document agent to write and render a finished file — " +
|
|
10
|
+
`${formats.map((f) => `.${f}`).join(", ")} — and hands back where to download it. Use it ` +
|
|
11
|
+
"when the answer has to be a document somebody opens: a report, a deck, a proposal, a " +
|
|
12
|
+
"one-pager. Not for text you can simply write in your reply. It runs for minutes and it " +
|
|
13
|
+
`costs money, so brief it once and brief it fully — the agent cannot see this ` +
|
|
14
|
+
"conversation, so every fact, figure and quote it must use goes in `source`. Design is " +
|
|
15
|
+
`already settled by ${design.name ?? "the house style"}; use \`design_notes\` only for what ` +
|
|
16
|
+
"this one document must do differently.");
|
|
17
|
+
}
|
|
18
|
+
function delivered(result, style, maxChars, includeData) {
|
|
19
|
+
const documents = (result.artifacts ?? []).map((a) => ({
|
|
20
|
+
name: a.name,
|
|
21
|
+
format: a.format,
|
|
22
|
+
url: a.url,
|
|
23
|
+
mime: a.mime,
|
|
24
|
+
sizeBytes: a.sizeBytes,
|
|
25
|
+
...(includeData && a.data ? { data: a.data } : {}),
|
|
26
|
+
}));
|
|
27
|
+
return {
|
|
28
|
+
taskId: result.id,
|
|
29
|
+
sessionId: result.sessionId,
|
|
30
|
+
status: "done",
|
|
31
|
+
style,
|
|
32
|
+
documents,
|
|
33
|
+
delivered: documents.length > 0,
|
|
34
|
+
writeUp: result.output?.slice(0, maxChars),
|
|
35
|
+
truncated: (result.output?.length ?? 0) > maxChars,
|
|
36
|
+
turns: result.numTurns,
|
|
37
|
+
costMicroUsd: result.costUsd === undefined ? undefined : Math.round(result.costUsd * 1_000_000),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function stillRendering(id, style, err) {
|
|
41
|
+
return {
|
|
42
|
+
taskId: id,
|
|
43
|
+
status: "running",
|
|
44
|
+
style,
|
|
45
|
+
documents: [],
|
|
46
|
+
delivered: false,
|
|
47
|
+
note: `Still rendering after ${Math.round(err.timeoutMs / 1000)}s. The task is not lost — ` +
|
|
48
|
+
"say so rather than commissioning it again.",
|
|
49
|
+
};
|
|
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
|
+
export function writeDocument(opts) {
|
|
60
|
+
if (!opts.client && !opts.apiKey?.trim()) {
|
|
61
|
+
throw new Error("writeDocument needs a Moa apiKey; it will not read one from the process.");
|
|
62
|
+
}
|
|
63
|
+
const allowed = opts.formats?.length ? opts.formats : ALL_FORMATS;
|
|
64
|
+
const design = mergeDesign(DEFAULT_DESIGN, opts.design);
|
|
65
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
66
|
+
const client = opts.client ??
|
|
67
|
+
createCommissionClient({
|
|
68
|
+
apiKey: opts.apiKey,
|
|
69
|
+
baseUrl: opts.baseUrl,
|
|
70
|
+
...(opts.fetch ? { fetch: opts.fetch } : {}),
|
|
71
|
+
});
|
|
72
|
+
return tool({
|
|
73
|
+
name: "write_document",
|
|
74
|
+
description: describe(allowed, design),
|
|
75
|
+
effect: "destructive",
|
|
76
|
+
args: [
|
|
77
|
+
{
|
|
78
|
+
name: "brief",
|
|
79
|
+
type: "text",
|
|
80
|
+
required: true,
|
|
81
|
+
maxLength: 8000,
|
|
82
|
+
description: "What to produce, who reads it, what it must cover and how long it runs. Write it " +
|
|
83
|
+
"as you would brief a person who knows the craft and nothing about this account.",
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "format",
|
|
87
|
+
type: "enum",
|
|
88
|
+
required: true,
|
|
89
|
+
options: ["pdf", "pptx", "docx", "md"],
|
|
90
|
+
description: "pptx for anything presented, pdf for anything sent, docx for anything edited.",
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
name: "source",
|
|
94
|
+
type: "text",
|
|
95
|
+
required: false,
|
|
96
|
+
maxLength: 40000,
|
|
97
|
+
description: "The material it must build from — figures, findings, quotes, an outline — verbatim. " +
|
|
98
|
+
"Anything missing here the agent will either omit or invent.",
|
|
99
|
+
},
|
|
100
|
+
{
|
|
101
|
+
name: "design_notes",
|
|
102
|
+
type: "text",
|
|
103
|
+
required: false,
|
|
104
|
+
maxLength: 2000,
|
|
105
|
+
description: "How this one document departs from the house style, in plain words. Leave it empty " +
|
|
106
|
+
"unless the user asked for something specific.",
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
name: "session_id",
|
|
110
|
+
type: "string",
|
|
111
|
+
required: false,
|
|
112
|
+
description: "Continue an earlier write_document instead of starting over — take it from that " +
|
|
113
|
+
"result's sessionId. Use it to revise a document or to recast one into another format.",
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
run: async ({ brief, format, source, design_notes, session_id }) => {
|
|
117
|
+
if (!allowed.includes(format)) {
|
|
118
|
+
throw new Error(`write_document was asked for .${format}, which this deployment does not produce ` +
|
|
119
|
+
`(${allowed.map((f) => `.${f}`).join(", ")}).`);
|
|
120
|
+
}
|
|
121
|
+
const id = await client.create({
|
|
122
|
+
task: brief,
|
|
123
|
+
context: source,
|
|
124
|
+
design: renderDesign(design, format, design_notes),
|
|
125
|
+
artifacts: [format],
|
|
126
|
+
...(session_id ? { sessionId: session_id } : {}),
|
|
127
|
+
});
|
|
128
|
+
try {
|
|
129
|
+
const result = await pollUntilDone(client, id, {
|
|
130
|
+
timeoutMs: opts.timeoutMs ?? DEFAULT_TIMEOUT_MS,
|
|
131
|
+
settleMs: DEFAULT_SETTLE_MS,
|
|
132
|
+
expectArtifacts: true,
|
|
133
|
+
...(opts.signal ? { signal: opts.signal } : {}),
|
|
134
|
+
});
|
|
135
|
+
if (result.status === "failed") {
|
|
136
|
+
throw new Error(`write_document: Moa task ${id} failed: ${result.error ?? "no reason given"}`);
|
|
137
|
+
}
|
|
138
|
+
return delivered(result, design.name, maxChars, opts.includeData === true);
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
if (err instanceof CommissionAbortedError && opts.cancelOnAbort !== false) {
|
|
142
|
+
await client.cancel?.(id).catch(() => undefined);
|
|
143
|
+
}
|
|
144
|
+
if (err instanceof CommissionTimeoutError)
|
|
145
|
+
return stillRendering(id, design.name, err);
|
|
146
|
+
throw err;
|
|
147
|
+
}
|
|
148
|
+
},
|
|
149
|
+
});
|
|
150
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { RolloutStep } from "@spendgraph/sdk";
|
|
2
|
+
import type { Effect, Tool, ToolDeclaration, ToolResult } from "../types/index.js";
|
|
3
|
+
import { type AnthropicTool, type OpenAITool } from "../wire/index.js";
|
|
4
|
+
import type { ToolBusOptions } from "./options.js";
|
|
5
|
+
import { type ToolTrace } from "./trace.js";
|
|
6
|
+
/**
|
|
7
|
+
* The tools an agent can reach, and how it reaches them.
|
|
8
|
+
*
|
|
9
|
+
* Holding them in one place is what makes selection possible. Fifty tools at
|
|
10
|
+
* roughly 150 tokens of declaration each is 7.5k tokens on every call before the
|
|
11
|
+
* user has said anything, so past a point you stop sending all of them.
|
|
12
|
+
*
|
|
13
|
+
* Selection is keyword and usage, deliberately: no embedding provider, no extra
|
|
14
|
+
* call, no latency. It is also the baseline that says what embeddings would buy.
|
|
15
|
+
*/
|
|
16
|
+
export declare function toolbus(tools: Tool<never>[], opts?: ToolBusOptions): {
|
|
17
|
+
/** Every tool, in declaration order. */
|
|
18
|
+
all: () => Tool<never>[];
|
|
19
|
+
/**
|
|
20
|
+
* Names something expects that this bus does not have.
|
|
21
|
+
*
|
|
22
|
+
* The failure it catches is silent: a prompt edited to expect a `refund`
|
|
23
|
+
* tool, deployed against a bus without one, does not error. The agent is
|
|
24
|
+
* never offered it and answers anyway.
|
|
25
|
+
*/
|
|
26
|
+
missing: (names: string[]) => string[];
|
|
27
|
+
get: (name: string) => Tool<never> | undefined;
|
|
28
|
+
/**
|
|
29
|
+
* What one tool declares calling it does. `undefined` when it never said.
|
|
30
|
+
*
|
|
31
|
+
* A gate reading this must decide what unknown means for it. It is not
|
|
32
|
+
* `readonly` — nothing has claimed that.
|
|
33
|
+
*/
|
|
34
|
+
effectOf: (name: string) => Effect | undefined;
|
|
35
|
+
/**
|
|
36
|
+
* Names of the tools declared `destructive`.
|
|
37
|
+
*
|
|
38
|
+
* Only the ones that said so. Pair it with `unannotated` before treating
|
|
39
|
+
* the rest as safe, or a tool nobody got round to labelling passes a gate
|
|
40
|
+
* built on this.
|
|
41
|
+
*/
|
|
42
|
+
destructive: () => string[];
|
|
43
|
+
/** Names of the tools with no effect declared, so a gate can refuse to guess. */
|
|
44
|
+
unannotated: () => string[];
|
|
45
|
+
/** Declarations for the model. Neutral shape; adapt per provider. */
|
|
46
|
+
declarations: (names?: string[]) => ToolDeclaration[];
|
|
47
|
+
/**
|
|
48
|
+
* Everything, in the shape the Anthropic Messages API takes.
|
|
49
|
+
*
|
|
50
|
+
* Selection is not applied for you — pass a shortlist to narrow it. The
|
|
51
|
+
* `tools` array sits ahead of the messages in the cached prefix, so a list
|
|
52
|
+
* that changes every turn invalidates the system prompt with it, and hiding
|
|
53
|
+
* the `select` call in here would hide that.
|
|
54
|
+
*/
|
|
55
|
+
anthropic: (decls?: ToolDeclaration[]) => AnthropicTool[];
|
|
56
|
+
/** Everything, in the shape the OpenAI chat completions API takes. */
|
|
57
|
+
openai: (decls?: ToolDeclaration[]) => OpenAITool[];
|
|
58
|
+
/**
|
|
59
|
+
* Everything as markdown, to append to a rendered system prompt.
|
|
60
|
+
*
|
|
61
|
+
* For a model with no tools API. Where there is one, prefer it: this is
|
|
62
|
+
* cheaper in tokens and worse in every other way.
|
|
63
|
+
*/
|
|
64
|
+
markdown: (decls?: ToolDeclaration[], heading?: string) => string;
|
|
65
|
+
/** The working set for one request. */
|
|
66
|
+
select: (query?: string, limit?: number) => ToolDeclaration[];
|
|
67
|
+
/** Runs one, validating its arguments first. Never throws. */
|
|
68
|
+
invoke: (name: string, args: Record<string, unknown>) => Promise<ToolResult>;
|
|
69
|
+
/** How many times each has been called on this bus. */
|
|
70
|
+
usage: () => Record<string, number>;
|
|
71
|
+
/** A result as a rollout step, for `report({ steps })`. */
|
|
72
|
+
step: (result: ToolResult, index: number) => RolloutStep;
|
|
73
|
+
/** One turn's tool use, selected once and recorded for the rollout. */
|
|
74
|
+
trace: (query?: string, limit?: number) => ToolTrace;
|
|
75
|
+
};
|
|
76
|
+
export type ToolBus = ReturnType<typeof toolbus>;
|
package/dist/bus/bus.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { toAnthropic, toMarkdown, toOpenAI, } from "../wire/index.js";
|
|
2
|
+
import { invokeTool } from "./invoke.js";
|
|
3
|
+
import { warnOnOverlap } from "./overlap.js";
|
|
4
|
+
import { selectTools } from "./select.js";
|
|
5
|
+
import { toStep } from "./step.js";
|
|
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
|
+
export function toolbus(tools, opts = {}) {
|
|
18
|
+
const now = opts.now ?? (() => Date.now());
|
|
19
|
+
const ceiling = Math.max(1, opts.limit ?? 6);
|
|
20
|
+
const byName = new Map();
|
|
21
|
+
for (const t of tools) {
|
|
22
|
+
if (byName.has(t.name))
|
|
23
|
+
throw new Error(`Two tools are called "${t.name}".`);
|
|
24
|
+
byName.set(t.name, t);
|
|
25
|
+
}
|
|
26
|
+
/** How often each has been called, so the useful ones stay offered. */
|
|
27
|
+
const calls = new Map();
|
|
28
|
+
if (opts.warnOnOverlap ?? true)
|
|
29
|
+
warnOnOverlap([...byName.values()]);
|
|
30
|
+
const declare = (t) => ({
|
|
31
|
+
name: t.name,
|
|
32
|
+
description: t.description,
|
|
33
|
+
args: t.args,
|
|
34
|
+
...(t.effect ? { effect: t.effect } : {}),
|
|
35
|
+
});
|
|
36
|
+
const all = () => [...byName.values()].map(declare);
|
|
37
|
+
const bus = {
|
|
38
|
+
/** Every tool, in declaration order. */
|
|
39
|
+
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
|
+
missing: (names) => names.filter((n) => !byName.has(n)),
|
|
48
|
+
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
|
+
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
|
+
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
|
+
unannotated: () => [...byName.values()].filter((t) => t.effect === undefined).map((t) => t.name),
|
|
66
|
+
/** Declarations for the model. Neutral shape; adapt per provider. */
|
|
67
|
+
declarations: (names) => names
|
|
68
|
+
? names
|
|
69
|
+
.map((n) => byName.get(n))
|
|
70
|
+
.filter(Boolean)
|
|
71
|
+
.map((t) => declare(t))
|
|
72
|
+
: 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
|
+
anthropic: (decls) => toAnthropic(decls ?? all()),
|
|
82
|
+
/** Everything, in the shape the OpenAI chat completions API takes. */
|
|
83
|
+
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
|
+
markdown: (decls, heading) => toMarkdown(decls ?? all(), heading),
|
|
91
|
+
/** The working set for one request. */
|
|
92
|
+
select: (query = "", limit) => selectTools([...byName.values()], calls, declare, query, Math.max(1, limit ?? ceiling)),
|
|
93
|
+
/** Runs one, validating its arguments first. Never throws. */
|
|
94
|
+
invoke: (name, args) => invokeTool(byName, calls, now, name, args),
|
|
95
|
+
/** How many times each has been called on this bus. */
|
|
96
|
+
usage: () => Object.fromEntries(calls),
|
|
97
|
+
/** A result as a rollout step, for `report({ steps })`. */
|
|
98
|
+
step: (result, index) => toStep(result, index),
|
|
99
|
+
/** One turn's tool use, selected once and recorded for the rollout. */
|
|
100
|
+
trace: (query = "", limit) => newTrace(bus.select(query, limit), (name, args) => bus.invoke(name, args)),
|
|
101
|
+
};
|
|
102
|
+
return bus;
|
|
103
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { Tool, ToolResult } from "../types/index.js";
|
|
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
|
+
export declare function invokeTool(byName: Map<string, Tool<never>>, calls: Map<string, number>, now: () => number, name: string, args: Record<string, unknown>): Promise<ToolResult>;
|
|
@@ -0,0 +1,50 @@
|
|
|
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
|
+
export async function invokeTool(byName, calls, now, name, args) {
|
|
10
|
+
const started = now();
|
|
11
|
+
const found = byName.get(name);
|
|
12
|
+
const base = { name, args, latencyMs: 0 };
|
|
13
|
+
if (!found) {
|
|
14
|
+
return {
|
|
15
|
+
...base,
|
|
16
|
+
status: "failed",
|
|
17
|
+
output: "",
|
|
18
|
+
error: `No tool called "${name}". Offered: ${[...byName.keys()].join(", ")}`,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
const errors = validateFields(args, found.args);
|
|
22
|
+
if (errors.length > 0) {
|
|
23
|
+
return {
|
|
24
|
+
...base,
|
|
25
|
+
status: "failed",
|
|
26
|
+
output: "",
|
|
27
|
+
error: new FieldValidationError(errors).message,
|
|
28
|
+
latencyMs: now() - started,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
calls.set(name, (calls.get(name) ?? 0) + 1);
|
|
32
|
+
try {
|
|
33
|
+
const value = await found.run(args);
|
|
34
|
+
return {
|
|
35
|
+
...base,
|
|
36
|
+
status: "completed",
|
|
37
|
+
output: typeof value === "string" ? value : JSON.stringify(value),
|
|
38
|
+
latencyMs: now() - started,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
return {
|
|
43
|
+
...base,
|
|
44
|
+
status: "failed",
|
|
45
|
+
output: "",
|
|
46
|
+
error: err instanceof Error ? err.message : "Tool failed.",
|
|
47
|
+
latencyMs: now() - started,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface ToolBusOptions {
|
|
2
|
+
/** Injectable clock, so tests do not measure real time. */
|
|
3
|
+
now?: () => number;
|
|
4
|
+
/**
|
|
5
|
+
* The most tools `select` will offer. Default 6.
|
|
6
|
+
*
|
|
7
|
+
* An accuracy budget, not a token one: more tools in front of a model makes it
|
|
8
|
+
* worse at picking the right one regardless of context length. Measured
|
|
9
|
+
* shortlists cluster around five to eight.
|
|
10
|
+
*
|
|
11
|
+
* Pinned tools count against it. They used to be added on top, so four pinned
|
|
12
|
+
* and a limit of six offered ten — which is the number that matters.
|
|
13
|
+
*/
|
|
14
|
+
limit?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Warn when two tools describe themselves almost identically. Default true.
|
|
17
|
+
*
|
|
18
|
+
* Nothing fails at runtime when they do; the model just picks wrong more
|
|
19
|
+
* often, which is why it is worth saying at construction.
|
|
20
|
+
*/
|
|
21
|
+
warnOnOverlap?: boolean;
|
|
22
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Tool } from "../types/index.js";
|
|
2
|
+
/**
|
|
3
|
+
* Warns once about tools that describe themselves the same way.
|
|
4
|
+
*
|
|
5
|
+
* Two near-identical descriptions are worse than one tool: the model cannot tell
|
|
6
|
+
* them apart and picks wrong more often, with nothing failing. Said at
|
|
7
|
+
* construction because it is invisible at runtime.
|
|
8
|
+
*
|
|
9
|
+
* One line however many pairs. A line each buries whatever else the process said
|
|
10
|
+
* at startup and teaches people to switch the check off.
|
|
11
|
+
*/
|
|
12
|
+
export declare function warnOnOverlap(tools: Tool<never>[]): void;
|