@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
@@ -0,0 +1,2 @@
1
+ export { dateMath } from "./date.js";
2
+ export { between, parseDate, shift } from "./shift.js";
@@ -0,0 +1,12 @@
1
+ export type Unit = "seconds" | "minutes" | "hours" | "days" | "weeks" | "months" | "years";
2
+ /** Parses an ISO instant, or says which argument was not one. */
3
+ export declare function parseDate(value: string, label: string): Date;
4
+ /** Moves an instant by a whole number of units. Negative goes backwards. */
5
+ export declare function shift(from: Date, amount: number, unit: Unit): Date;
6
+ /**
7
+ * How far apart two instants are, in whole units.
8
+ *
9
+ * Calendar units count boundaries crossed rather than dividing elapsed
10
+ * milliseconds, because a month is not 30 days and a year is not 365.
11
+ */
12
+ export declare function between(from: Date, to: Date, unit: Unit): number;
@@ -0,0 +1,62 @@
1
+ const MS = {
2
+ seconds: 1000,
3
+ minutes: 60_000,
4
+ hours: 3_600_000,
5
+ days: 86_400_000,
6
+ weeks: 604_800_000,
7
+ };
8
+ /** Parses an ISO instant, or says which argument was not one. */
9
+ export function parseDate(value, label) {
10
+ const at = new Date(value);
11
+ if (!Number.isFinite(at.getTime())) {
12
+ throw new Error(`${label} "${value}" is not a date. Use an ISO instant, as in 2026-03-31.`);
13
+ }
14
+ return at;
15
+ }
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
+ function addMonths(from, months) {
24
+ const day = from.getUTCDate();
25
+ const shifted = new Date(from.getTime());
26
+ shifted.setUTCDate(1);
27
+ shifted.setUTCMonth(shifted.getUTCMonth() + months);
28
+ const lastOfMonth = new Date(Date.UTC(shifted.getUTCFullYear(), shifted.getUTCMonth() + 1, 0)).getUTCDate();
29
+ shifted.setUTCDate(Math.min(day, lastOfMonth));
30
+ return shifted;
31
+ }
32
+ /** Moves an instant by a whole number of units. Negative goes backwards. */
33
+ export function shift(from, amount, unit) {
34
+ if (!Number.isInteger(amount))
35
+ throw new Error("amount must be a whole number.");
36
+ if (unit === "months")
37
+ return addMonths(from, amount);
38
+ if (unit === "years")
39
+ return addMonths(from, amount * 12);
40
+ const ms = MS[unit];
41
+ if (!ms)
42
+ throw new Error(`"${unit}" is not a unit this tool knows.`);
43
+ return new Date(from.getTime() + amount * ms);
44
+ }
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
+ export function between(from, to, unit) {
52
+ if (unit === "months" || unit === "years") {
53
+ const months = (to.getUTCFullYear() - from.getUTCFullYear()) * 12 +
54
+ (to.getUTCMonth() - from.getUTCMonth()) -
55
+ (to.getUTCDate() < from.getUTCDate() ? 1 : 0);
56
+ return unit === "years" ? Math.trunc(months / 12) : months;
57
+ }
58
+ const ms = MS[unit];
59
+ if (!ms)
60
+ throw new Error(`"${unit}" is not a unit this tool knows.`);
61
+ return Math.trunc((to.getTime() - from.getTime()) / ms);
62
+ }
@@ -0,0 +1,27 @@
1
+ import type { LocusGraphClient } from "@locusgraph/client";
2
+ export interface DeepRecallOptions {
3
+ /** A `LocusGraphClient`, or anything carrying its `deepRecall`. */
4
+ client: Pick<LocusGraphClient, "deepRecall">;
5
+ /** Graph the walk runs over. Unset, the client's own default is used. */
6
+ graphId?: string;
7
+ /** Longest evidence list handed back. Default 20. */
8
+ maxFacts?: number;
9
+ }
10
+ /**
11
+ * Your own knowledge base, answered with cited evidence.
12
+ *
13
+ * Cost comes back as micro-USD because deep recall runs model calls of its own,
14
+ * and that spend is invisible to whatever loop called this.
15
+ */
16
+ export declare function deepRecall(opts: DeepRecallOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
17
+ readonly name: "question";
18
+ readonly type: "string";
19
+ readonly required: true;
20
+ readonly maxLength: 1000;
21
+ readonly description: "One focused question, not a conversation and not several at once.";
22
+ }, {
23
+ readonly name: "context_ids";
24
+ readonly type: "list";
25
+ readonly required: false;
26
+ readonly description: string;
27
+ }]>>;
@@ -0,0 +1,51 @@
1
+ import { tool } from "../../tool/index.js";
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
+ export function deepRecall(opts) {
10
+ const maxFacts = opts.maxFacts ?? DEFAULT_MAX_FACTS;
11
+ return tool({
12
+ name: "deep_recall",
13
+ description: "Searches your own knowledge base and returns an answer with the evidence it rests " +
14
+ "on. A walker follows linked contexts, so it answers questions spread across several " +
15
+ "notes — what do we know about X, what was decided and why. Ask one focused thing. " +
16
+ "Prefer it over a web search whenever the answer could already be in your records.",
17
+ effect: "readonly",
18
+ args: [
19
+ {
20
+ name: "question",
21
+ type: "string",
22
+ required: true,
23
+ maxLength: 1000,
24
+ description: "One focused question, not a conversation and not several at once.",
25
+ },
26
+ {
27
+ name: "context_ids",
28
+ type: "list",
29
+ required: false,
30
+ description: "Contexts to start the walk from, as type:name — take them from the contexts of " +
31
+ "an earlier result. A hint, not a filter; the walk may go elsewhere.",
32
+ },
33
+ ],
34
+ run: async ({ question, context_ids }) => {
35
+ const result = await opts.client.deepRecall({
36
+ question,
37
+ ...(opts.graphId ? { graphId: opts.graphId } : {}),
38
+ ...(context_ids?.length ? { contextIds: context_ids } : {}),
39
+ });
40
+ return {
41
+ summary: result.summary,
42
+ facts: result.facts.slice(0, maxFacts),
43
+ truncated: result.facts.length > maxFacts,
44
+ contextsWalked: result.contexts_walked,
45
+ coverage: result.coverage,
46
+ fallback: result.fallback,
47
+ costMicroUsd: Math.round(result.usage.cost_usd * 1_000_000),
48
+ };
49
+ },
50
+ });
51
+ }
@@ -0,0 +1 @@
1
+ export { type DeepRecallOptions, deepRecall } from "./deep-recall.js";
@@ -0,0 +1 @@
1
+ export { deepRecall } from "./deep-recall.js";
@@ -0,0 +1,46 @@
1
+ export interface EscalateOptions {
2
+ /**
3
+ * What actually happens — page someone, open a ticket, write to a queue.
4
+ *
5
+ * Without it this records the request and nothing else, which is honest but
6
+ * not useful; the model will believe a human was reached either way.
7
+ */
8
+ onEscalate?: (request: EscalationRequest) => Promise<unknown> | unknown;
9
+ /**
10
+ * Offered whatever selection decides. Default true.
11
+ *
12
+ * A refusal route lost to a similarity score is the failure `pinned` exists
13
+ * for: the one turn that needed it is the one where nothing else fit.
14
+ */
15
+ pinned?: boolean;
16
+ }
17
+ export interface EscalationRequest {
18
+ reason: string;
19
+ summary?: string;
20
+ urgency: "low" | "normal" | "high";
21
+ }
22
+ /**
23
+ * Hand back to a person rather than guess.
24
+ *
25
+ * An agent with no way to stop answers anyway, and a confident wrong answer to
26
+ * something it could not do is worse than a handover.
27
+ *
28
+ * Its `effect` is left unset on purpose: `onEscalate` is yours, and paging
29
+ * someone twice is not the same as paging them once.
30
+ */
31
+ export declare function escalate(opts?: EscalateOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
32
+ readonly name: "reason";
33
+ readonly type: "text";
34
+ readonly required: true;
35
+ readonly maxLength: 2000;
36
+ }, {
37
+ readonly name: "summary";
38
+ readonly type: "text";
39
+ readonly required: false;
40
+ readonly maxLength: 4000;
41
+ }, {
42
+ readonly name: "urgency";
43
+ readonly type: "enum";
44
+ readonly required: false;
45
+ readonly options: readonly ["low", "normal", "high"];
46
+ }]>>;
@@ -0,0 +1,29 @@
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
+ export function escalate(opts = {}) {
12
+ return tool({
13
+ name: "escalate",
14
+ description: "Hand this over to a person. Call it when you are missing something you cannot " +
15
+ "get, when the request needs an authority you do not have, or when acting on a " +
16
+ "guess would be worse than waiting. Say plainly why.",
17
+ pinned: opts.pinned ?? true,
18
+ args: [
19
+ { name: "reason", type: "text", required: true, maxLength: 2000 },
20
+ { name: "summary", type: "text", required: false, maxLength: 4000 },
21
+ { name: "urgency", type: "enum", required: false, options: ["low", "normal", "high"] },
22
+ ],
23
+ run: async ({ reason, summary, urgency = "normal" }) => {
24
+ const request = { reason, summary, urgency };
25
+ const handled = opts.onEscalate ? await opts.onEscalate(request) : undefined;
26
+ return { escalated: true, delivered: Boolean(opts.onEscalate), request, handled };
27
+ },
28
+ });
29
+ }
@@ -0,0 +1 @@
1
+ export { type EscalateOptions, type EscalationRequest, escalate } from "./escalate.js";
@@ -0,0 +1 @@
1
+ export { escalate } from "./escalate.js";
@@ -0,0 +1,27 @@
1
+ import type { ToolResult } from "../../types/index.js";
2
+ /** What `finish` returns, so a loop can recognise its own stop condition. */
3
+ export interface Finished {
4
+ done: true;
5
+ answer: string;
6
+ }
7
+ /**
8
+ * True when this result is the model saying it is done.
9
+ *
10
+ * A loop that ends because the model stopped asking for tools cannot tell
11
+ * finished from confused. This makes the difference explicit.
12
+ */
13
+ export declare function isFinished(result: ToolResult): boolean;
14
+ /** Reads the answer out of a finished result, or null if it is not one. */
15
+ export declare function answerOf(result: ToolResult): string | null;
16
+ /**
17
+ * The model says it is done, and says what the answer is.
18
+ *
19
+ * Without it a loop ends by the model going quiet, which is indistinguishable
20
+ * from it losing the thread.
21
+ */
22
+ export declare function finish(): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
23
+ readonly name: "answer";
24
+ readonly type: "text";
25
+ readonly required: true;
26
+ readonly maxLength: 20000;
27
+ }]>>;
@@ -0,0 +1,39 @@
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
+ export function isFinished(result) {
9
+ if (result.name !== "finish" || result.status !== "completed")
10
+ return false;
11
+ try {
12
+ return JSON.parse(result.output).done === true;
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ }
18
+ /** Reads the answer out of a finished result, or null if it is not one. */
19
+ export function answerOf(result) {
20
+ if (!isFinished(result))
21
+ return null;
22
+ return JSON.parse(result.output).answer;
23
+ }
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
+ export function finish() {
31
+ return tool({
32
+ name: "finish",
33
+ effect: "readonly",
34
+ description: "Call this when you have the answer and need no more tools. Pass the final " +
35
+ "answer as it should be given to the person who asked.",
36
+ args: [{ name: "answer", type: "text", required: true, maxLength: 20_000 }],
37
+ run: ({ answer }) => ({ done: true, answer }),
38
+ });
39
+ }
@@ -0,0 +1 @@
1
+ export { answerOf, type Finished, finish, isFinished } from "./finish.js";
@@ -0,0 +1 @@
1
+ export { answerOf, finish, isFinished } from "./finish.js";
@@ -0,0 +1,8 @@
1
+ /**
2
+ * The URL a request may go to, or the reason it may not.
3
+ *
4
+ * An allowlist rather than a blocklist, and https only. A tool that fetches
5
+ * whatever a model names is how an agent reads a cloud metadata endpoint and
6
+ * hands back the credentials it finds.
7
+ */
8
+ export declare function checkUrl(raw: string, allow: string[]): URL;
@@ -0,0 +1,45 @@
1
+ const PRIVATE_V4 = /^(10\.|127\.|0\.|169\.254\.|192\.168\.|172\.(1[6-9]|2\d|3[01])\.|100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\.)/;
2
+ const LOCAL_NAMES = new Set(["localhost", "metadata.google.internal"]);
3
+ function isPrivateHost(host) {
4
+ const name = host.toLowerCase();
5
+ if (LOCAL_NAMES.has(name))
6
+ return true;
7
+ if (name.endsWith(".local") || name.endsWith(".internal") || name.endsWith(".localhost")) {
8
+ return true;
9
+ }
10
+ if (name.startsWith("["))
11
+ return true;
12
+ return PRIVATE_V4.test(name);
13
+ }
14
+ /** Matches a host exactly, or any subdomain when the rule starts with a dot. */
15
+ function matches(host, rule) {
16
+ const h = host.toLowerCase();
17
+ const r = rule.toLowerCase();
18
+ return r.startsWith(".") ? h === r.slice(1) || h.endsWith(r) : h === r;
19
+ }
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
+ export function checkUrl(raw, allow) {
28
+ let url;
29
+ try {
30
+ url = new URL(raw);
31
+ }
32
+ catch {
33
+ throw new Error(`"${raw}" is not a URL.`);
34
+ }
35
+ if (url.protocol !== "https:") {
36
+ throw new Error(`Only https is allowed, and this is ${url.protocol.replace(":", "")}.`);
37
+ }
38
+ if (isPrivateHost(url.hostname)) {
39
+ throw new Error(`${url.hostname} is a private or link-local address.`);
40
+ }
41
+ if (!allow.some((rule) => matches(url.hostname, rule))) {
42
+ throw new Error(`${url.hostname} is not in the allowlist: ${allow.join(", ")}.`);
43
+ }
44
+ return url;
45
+ }
@@ -0,0 +1,43 @@
1
+ export interface HttpOptions {
2
+ /**
3
+ * Hosts this tool may reach. Required, and there is no default.
4
+ *
5
+ * `"api.example.com"` matches that host; `".example.com"` matches it and any
6
+ * subdomain.
7
+ */
8
+ allow: string[];
9
+ /** Sent on every request — an API key belongs here, never in the args. */
10
+ headers?: Record<string, string>;
11
+ /** Give up after this. Default 10000. */
12
+ timeoutMs?: number;
13
+ /** Longest body handed back. Default 100000 characters. */
14
+ maxChars?: number;
15
+ /** Injected for tests. */
16
+ fetch?: typeof fetch;
17
+ }
18
+ /**
19
+ * An HTTP GET or POST, to hosts you named.
20
+ *
21
+ * The allowlist is required because the alternative is a tool that fetches
22
+ * whatever a model is talked into naming. Headers are set here, not by the
23
+ * model, so a credential is never something it can redirect.
24
+ *
25
+ * Its `effect` is left unset on purpose: a GET is readonly and a POST to your
26
+ * allowlist may be anything at all, and one label cannot be true of both.
27
+ */
28
+ export declare function httpRequest(opts: HttpOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
29
+ readonly name: "url";
30
+ readonly type: "string";
31
+ readonly required: true;
32
+ readonly maxLength: 2000;
33
+ }, {
34
+ readonly name: "method";
35
+ readonly type: "enum";
36
+ readonly required: false;
37
+ readonly options: readonly ["GET", "POST"];
38
+ }, {
39
+ readonly name: "body";
40
+ readonly type: "text";
41
+ readonly required: false;
42
+ readonly maxLength: 20000;
43
+ }]>>;
@@ -0,0 +1,51 @@
1
+ import { tool } from "../../tool/index.js";
2
+ import { checkUrl } from "./allow.js";
3
+ const DEFAULT_TIMEOUT_MS = 10_000;
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
+ export function httpRequest(opts) {
16
+ if (!opts.allow?.length) {
17
+ throw new Error("httpRequest needs an allow list of hosts; there is no safe default.");
18
+ }
19
+ const doFetch = opts.fetch ?? globalThis.fetch;
20
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
21
+ return tool({
22
+ name: "http_request",
23
+ description: `Fetches a URL and returns its status and body. Only these hosts are reachable: ` +
24
+ `${opts.allow.join(", ")}. Use it to read an API or a page you have been pointed at.`,
25
+ args: [
26
+ { name: "url", type: "string", required: true, maxLength: 2000 },
27
+ { name: "method", type: "enum", required: false, options: ["GET", "POST"] },
28
+ { name: "body", type: "text", required: false, maxLength: 20_000 },
29
+ ],
30
+ run: async ({ url, method = "GET", body }) => {
31
+ const target = checkUrl(url, opts.allow);
32
+ const signal = AbortSignal.timeout(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
33
+ const res = await doFetch(target, {
34
+ method,
35
+ signal,
36
+ headers: {
37
+ ...opts.headers,
38
+ ...(body ? { "content-type": "application/json" } : {}),
39
+ },
40
+ ...(body && method === "POST" ? { body } : {}),
41
+ });
42
+ const text = await res.text();
43
+ return {
44
+ status: res.status,
45
+ ok: res.ok,
46
+ body: text.slice(0, maxChars),
47
+ truncated: text.length > maxChars,
48
+ };
49
+ },
50
+ });
51
+ }
@@ -0,0 +1,2 @@
1
+ export { checkUrl } from "./allow.js";
2
+ export { type HttpOptions, httpRequest } from "./http.js";
@@ -0,0 +1,2 @@
1
+ export { checkUrl } from "./allow.js";
2
+ export { httpRequest } from "./http.js";
@@ -0,0 +1,11 @@
1
+ export { calculate, evaluate } from "./calculate/index.js";
2
+ export { type ClockOptions, currentTime } from "./clock/index.js";
3
+ export { dateMath } from "./date/index.js";
4
+ export { type DeepRecallOptions, deepRecall } from "./deep-recall/index.js";
5
+ export { type EscalateOptions, type EscalationRequest, escalate } from "./escalate/index.js";
6
+ export { answerOf, type Finished, finish, isFinished } from "./finish/index.js";
7
+ export { checkUrl, type HttpOptions, httpRequest } from "./http/index.js";
8
+ export { type JsonQueryOptions, jsonQuery } from "./json/index.js";
9
+ export { think } from "./think/index.js";
10
+ export { type SearchDepth, type WebSearchOptions, webSearch } from "./web-search/index.js";
11
+ export { type ArtifactFormat, CommissionAbortedError, type CommissionClient, CommissionTimeoutError, DEFAULT_DESIGN, type DesignGuide, mergeDesign, renderDesign, type WriteDocumentOptions, writeDocument, } from "./write-document/index.js";
@@ -0,0 +1,11 @@
1
+ export { calculate, evaluate } from "./calculate/index.js";
2
+ export { currentTime } from "./clock/index.js";
3
+ export { dateMath } from "./date/index.js";
4
+ export { deepRecall } from "./deep-recall/index.js";
5
+ export { escalate } from "./escalate/index.js";
6
+ export { answerOf, finish, isFinished } from "./finish/index.js";
7
+ export { checkUrl, httpRequest } from "./http/index.js";
8
+ export { jsonQuery } from "./json/index.js";
9
+ export { think } from "./think/index.js";
10
+ export { webSearch } from "./web-search/index.js";
11
+ export { CommissionAbortedError, CommissionTimeoutError, DEFAULT_DESIGN, mergeDesign, renderDesign, writeDocument, } from "./write-document/index.js";
@@ -0,0 +1,2 @@
1
+ export { type JsonQueryOptions, jsonQuery } from "./json.js";
2
+ export { readPath } from "./path.js";
@@ -0,0 +1,2 @@
1
+ export { jsonQuery } from "./json.js";
2
+ export { readPath } from "./path.js";
@@ -0,0 +1,22 @@
1
+ export interface JsonQueryOptions {
2
+ /** Longest value handed back, once stringified. Default 20000. */
3
+ maxChars?: number;
4
+ }
5
+ /**
6
+ * Pulls one value out of a JSON document.
7
+ *
8
+ * The alternative is putting the whole reply in the context and asking the model
9
+ * to read it, which costs tokens on every turn and gets the field wrong when the
10
+ * document is long.
11
+ */
12
+ export declare function jsonQuery(opts?: JsonQueryOptions): import("../../index.js").Tool<import("../../index.js").ArgsOf<readonly [{
13
+ readonly name: "json";
14
+ readonly type: "text";
15
+ readonly required: true;
16
+ readonly maxLength: 200000;
17
+ }, {
18
+ readonly name: "path";
19
+ readonly type: "string";
20
+ readonly required: true;
21
+ readonly maxLength: 500;
22
+ }]>>;
@@ -0,0 +1,44 @@
1
+ import { tool } from "../../tool/index.js";
2
+ import { readPath } from "./path.js";
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
+ export function jsonQuery(opts = {}) {
12
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
13
+ return tool({
14
+ name: "json_query",
15
+ effect: "readonly",
16
+ description: "Reads one value out of a JSON document by path. Paths look like " +
17
+ "`invoices[0].total`, or `invoices[*].total` for every element. Use it instead " +
18
+ "of reading a long document yourself.",
19
+ args: [
20
+ { name: "json", type: "text", required: true, maxLength: 200_000 },
21
+ { name: "path", type: "string", required: true, maxLength: 500 },
22
+ ],
23
+ run: ({ json, path }) => {
24
+ let document;
25
+ try {
26
+ document = JSON.parse(json);
27
+ }
28
+ catch (err) {
29
+ throw new Error(`json is not valid JSON: ${err.message}`);
30
+ }
31
+ const value = readPath(document, path);
32
+ if (value === undefined)
33
+ return { found: false, path, value: null };
34
+ const rendered = JSON.stringify(value) ?? "null";
35
+ return {
36
+ found: true,
37
+ path,
38
+ value,
39
+ truncated: rendered.length > maxChars,
40
+ ...(rendered.length > maxChars ? { value: `${rendered.slice(0, maxChars)}…` } : {}),
41
+ };
42
+ },
43
+ });
44
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Reads a value out of parsed JSON by path.
3
+ *
4
+ * `a.b`, `a[0].b`, and `a[*].b` for every element — enough to pull a field out
5
+ * of an API reply, and small enough that the syntax is guessable from one
6
+ * example, which matters when the thing writing the path is a model.
7
+ */
8
+ export declare function readPath(document: unknown, path: string): unknown;
@@ -0,0 +1,63 @@
1
+ function parsePath(path) {
2
+ const segments = [];
3
+ for (const part of path.split(".")) {
4
+ if (!part)
5
+ continue;
6
+ const [, name, brackets] = /^([^[\]]*)((?:\[[^\]]*\])*)$/.exec(part) ?? [];
7
+ if (name === undefined)
8
+ throw new Error(`"${part}" is not a path segment.`);
9
+ if (name)
10
+ segments.push({ kind: "key", name });
11
+ for (const [, inner] of (brackets ?? "").matchAll(/\[([^\]]*)\]/g)) {
12
+ if (inner === "*") {
13
+ segments.push({ kind: "all" });
14
+ continue;
15
+ }
16
+ const at = Number(inner);
17
+ if (!Number.isInteger(at))
18
+ throw new Error(`"[${inner}]" is not an index or a *.`);
19
+ segments.push({ kind: "index", at });
20
+ }
21
+ }
22
+ return segments;
23
+ }
24
+ function step(value, segment) {
25
+ if (value === null || value === undefined)
26
+ return undefined;
27
+ if (segment.kind === "key") {
28
+ if (typeof value !== "object" || Array.isArray(value))
29
+ return undefined;
30
+ return value[segment.name];
31
+ }
32
+ if (!Array.isArray(value))
33
+ return undefined;
34
+ if (segment.kind === "index") {
35
+ return value.at(segment.at);
36
+ }
37
+ return value;
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
+ export function readPath(document, path) {
47
+ let cursor = document;
48
+ let spread = false;
49
+ for (const segment of parsePath(path)) {
50
+ if (segment.kind === "all") {
51
+ if (!Array.isArray(cursor))
52
+ return undefined;
53
+ cursor = spread ? cursor.flat() : [...cursor];
54
+ spread = true;
55
+ continue;
56
+ }
57
+ cursor =
58
+ spread && Array.isArray(cursor)
59
+ ? cursor.map((item) => step(item, segment))
60
+ : step(cursor, segment);
61
+ }
62
+ return cursor;
63
+ }