@providerkit/core 0.1.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 (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +245 -0
  3. package/dist/context.d.ts +69 -0
  4. package/dist/context.d.ts.map +1 -0
  5. package/dist/context.js +132 -0
  6. package/dist/context.js.map +1 -0
  7. package/dist/errors.d.ts +86 -0
  8. package/dist/errors.d.ts.map +1 -0
  9. package/dist/errors.js +356 -0
  10. package/dist/errors.js.map +1 -0
  11. package/dist/index.d.ts +13 -0
  12. package/dist/index.d.ts.map +1 -0
  13. package/dist/index.js +13 -0
  14. package/dist/index.js.map +1 -0
  15. package/dist/providers/anthropic.d.ts +26 -0
  16. package/dist/providers/anthropic.d.ts.map +1 -0
  17. package/dist/providers/anthropic.js +245 -0
  18. package/dist/providers/anthropic.js.map +1 -0
  19. package/dist/providers/openai.d.ts +30 -0
  20. package/dist/providers/openai.d.ts.map +1 -0
  21. package/dist/providers/openai.js +185 -0
  22. package/dist/providers/openai.js.map +1 -0
  23. package/dist/retry.d.ts +79 -0
  24. package/dist/retry.d.ts.map +1 -0
  25. package/dist/retry.js +200 -0
  26. package/dist/retry.js.map +1 -0
  27. package/dist/schema.d.ts +2 -0
  28. package/dist/schema.d.ts.map +1 -0
  29. package/dist/schema.js +48 -0
  30. package/dist/schema.js.map +1 -0
  31. package/dist/tool-args.d.ts +12 -0
  32. package/dist/tool-args.d.ts.map +1 -0
  33. package/dist/tool-args.js +113 -0
  34. package/dist/tool-args.js.map +1 -0
  35. package/dist/tools.d.ts +82 -0
  36. package/dist/tools.d.ts.map +1 -0
  37. package/dist/tools.js +155 -0
  38. package/dist/tools.js.map +1 -0
  39. package/dist/transport.d.ts +31 -0
  40. package/dist/transport.d.ts.map +1 -0
  41. package/dist/transport.js +157 -0
  42. package/dist/transport.js.map +1 -0
  43. package/dist/types.d.ts +168 -0
  44. package/dist/types.d.ts.map +1 -0
  45. package/dist/types.js +75 -0
  46. package/dist/types.js.map +1 -0
  47. package/dist/usage.d.ts +50 -0
  48. package/dist/usage.d.ts.map +1 -0
  49. package/dist/usage.js +71 -0
  50. package/dist/usage.js.map +1 -0
  51. package/dist/watchdog.d.ts +34 -0
  52. package/dist/watchdog.d.ts.map +1 -0
  53. package/dist/watchdog.js +85 -0
  54. package/dist/watchdog.js.map +1 -0
  55. package/dist/zod.d.ts +32 -0
  56. package/dist/zod.d.ts.map +1 -0
  57. package/dist/zod.js +49 -0
  58. package/dist/zod.js.map +1 -0
  59. package/package.json +76 -0
  60. package/src/context.ts +150 -0
  61. package/src/errors.ts +398 -0
  62. package/src/index.ts +12 -0
  63. package/src/providers/anthropic.ts +315 -0
  64. package/src/providers/openai.ts +246 -0
  65. package/src/retry.ts +246 -0
  66. package/src/schema.ts +67 -0
  67. package/src/tool-args.ts +117 -0
  68. package/src/tools.ts +237 -0
  69. package/src/transport.ts +162 -0
  70. package/src/types.ts +231 -0
  71. package/src/usage.ts +106 -0
  72. package/src/watchdog.ts +119 -0
  73. package/src/zod.ts +74 -0
package/dist/retry.js ADDED
@@ -0,0 +1,200 @@
1
+ // Retrying, and knowing when not to.
2
+ //
3
+ // Three rules do most of the work here, and each was learned separately:
4
+ //
5
+ // 1. Retry only what a retry can fix. A deterministic failure (bad key,
6
+ // invalid request, exhausted balance) hits identically on every attempt,
7
+ // so retrying it just spends the budget to arrive at the same answer later.
8
+ //
9
+ // 2. For a STREAM, retry only while nothing has been emitted. Once a chunk
10
+ // has reached the consumer the stream is committed: a retry would replay
11
+ // tokens the caller already rendered. A mid-output drop is the caller's
12
+ // problem to handle (or a job-level restart's), never a silent re-run.
13
+ //
14
+ // 3. Honour the provider's own number. When it says `Retry-After: 30`, a
15
+ // one-second backoff is three wasted attempts before the same wait.
16
+ import { classify, isBackupEligible, isTransient, parseRetryAfterMs } from "./errors.js";
17
+ const DEFAULT_MAX_ATTEMPTS = 3;
18
+ const DEFAULT_BASE_DELAY_MS = 1_000;
19
+ const DEFAULT_MAX_DELAY_MS = 30_000;
20
+ /**
21
+ * Full-jitter exponential backoff: a random delay in
22
+ * `[0, min(cap, base · 2^(attempt-1))]`.
23
+ *
24
+ * The jitter is the point, not the exponent. Without it, every client that
25
+ * failed against the same overloaded upstream retries in the same instant and
26
+ * rebuilds the thundering herd that caused the failure.
27
+ */
28
+ export function backoffMs(attempt, base = DEFAULT_BASE_DELAY_MS, cap = DEFAULT_MAX_DELAY_MS) {
29
+ const ceiling = Math.min(cap, base * 2 ** Math.max(0, attempt - 1));
30
+ return Math.floor(Math.random() * ceiling);
31
+ }
32
+ /** A sleep that wakes early when the caller aborts, and rejects with the
33
+ * abort reason rather than resolving into work nobody wants any more. */
34
+ export function sleep(ms, signal) {
35
+ return new Promise((resolve, reject) => {
36
+ if (signal?.aborted)
37
+ return reject(signal.reason);
38
+ const timer = setTimeout(() => {
39
+ signal?.removeEventListener("abort", onAbort);
40
+ resolve();
41
+ }, ms);
42
+ function onAbort() {
43
+ clearTimeout(timer);
44
+ reject(signal?.reason);
45
+ }
46
+ signal?.addEventListener("abort", onAbort, { once: true });
47
+ });
48
+ }
49
+ /** The delay before the next attempt: the provider's own figure when it gave
50
+ * one, capped, else full-jitter backoff. */
51
+ function delayFor(err, attempt, opts) {
52
+ const asked = parseRetryAfterMs(err);
53
+ const cap = opts.maxDelayMs ?? DEFAULT_MAX_DELAY_MS;
54
+ if (asked !== undefined)
55
+ return Math.min(asked, cap);
56
+ return backoffMs(attempt, opts.baseDelayMs ?? DEFAULT_BASE_DELAY_MS, cap);
57
+ }
58
+ const defaultShouldRetry = (err) => isTransient(classify(err));
59
+ /**
60
+ * Run `fn`, retrying transient failures with backoff. For one-shot calls —
61
+ * a title, a summary, a compaction pass.
62
+ */
63
+ export async function withRetry(fn, opts = {}) {
64
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
65
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
66
+ const nap = opts.sleep ?? sleep;
67
+ let lastError;
68
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
69
+ if (opts.signal?.aborted)
70
+ throw opts.signal.reason;
71
+ try {
72
+ return await fn(attempt);
73
+ }
74
+ catch (err) {
75
+ lastError = err;
76
+ // A caller's Stop is never a transient failure, whatever it looks like.
77
+ if (opts.signal?.aborted)
78
+ throw err;
79
+ if (attempt >= maxAttempts || !shouldRetry(err, attempt))
80
+ throw err;
81
+ const delayMs = delayFor(err, attempt, opts);
82
+ opts.onRetry?.({ error: err, attempt, delayMs });
83
+ await nap(delayMs, opts.signal);
84
+ }
85
+ }
86
+ throw lastError;
87
+ }
88
+ /**
89
+ * The streaming twin — with the rule that makes it safe: a retry happens only
90
+ * while NOTHING has been yielded yet.
91
+ *
92
+ * `factory` is re-invoked per attempt and gets a fresh signal, so an abandoned
93
+ * attempt's upstream request is cancelled rather than left racing the retry.
94
+ * Once the first chunk is out, the stream is committed and any later failure
95
+ * propagates untouched.
96
+ */
97
+ export async function* withStreamRetry(factory, opts = {}) {
98
+ const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
99
+ const shouldRetry = opts.shouldRetry ?? defaultShouldRetry;
100
+ const nap = opts.sleep ?? sleep;
101
+ let lastError;
102
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
103
+ if (opts.signal?.aborted)
104
+ throw opts.signal.reason;
105
+ // One controller per attempt: abandoning an attempt must cancel its
106
+ // upstream call, or a retry stacks a second live stream against the same
107
+ // rate limit.
108
+ const controller = new AbortController();
109
+ const onOuterAbort = () => controller.abort(opts.signal?.reason);
110
+ opts.signal?.addEventListener("abort", onOuterAbort, { once: true });
111
+ let emitted = false;
112
+ try {
113
+ for await (const chunk of factory(controller.signal, attempt)) {
114
+ emitted = true;
115
+ yield chunk;
116
+ }
117
+ return;
118
+ }
119
+ catch (err) {
120
+ lastError = err;
121
+ if (opts.signal?.aborted)
122
+ throw err;
123
+ // Rule 2: past the first chunk there is no going back.
124
+ if (emitted || attempt >= maxAttempts || !shouldRetry(err, attempt))
125
+ throw err;
126
+ const delayMs = delayFor(err, attempt, opts);
127
+ opts.onRetry?.({ error: err, attempt, delayMs });
128
+ await nap(delayMs, opts.signal);
129
+ }
130
+ finally {
131
+ opts.signal?.removeEventListener("abort", onOuterAbort);
132
+ // Abandoning mid-iteration (the consumer broke out, or we are retrying)
133
+ // must not leave the upstream request running.
134
+ if (!controller.signal.aborted)
135
+ controller.abort();
136
+ }
137
+ }
138
+ throw lastError;
139
+ }
140
+ const defaultShouldTryNext = (err) => isBackupEligible(classify(err));
141
+ function requireModels(models) {
142
+ if (models.length === 0)
143
+ throw new Error("providerkit: `models` must include a primary model");
144
+ }
145
+ /** Walk `[primary, ...backups]` until one succeeds. Rethrows the last error. */
146
+ export async function withBackupModels(attempt, opts) {
147
+ requireModels(opts.models);
148
+ const shouldTryNext = opts.shouldTryNext ?? defaultShouldTryNext;
149
+ const total = opts.models.length;
150
+ let lastError;
151
+ for (const [index, model] of opts.models.entries()) {
152
+ if (index > 0)
153
+ opts.onFallback?.({ model, position: index + 1, total });
154
+ try {
155
+ return await attempt(model);
156
+ }
157
+ catch (err) {
158
+ lastError = err;
159
+ opts.onModelFailed?.({ model, error: err, position: index + 1, total });
160
+ if (!shouldTryNext(err))
161
+ break;
162
+ }
163
+ }
164
+ throw lastError;
165
+ }
166
+ /**
167
+ * The streaming twin — carrying the same commitment rule as `withStreamRetry`.
168
+ *
169
+ * This is the correction worth naming: walking to a backup model AFTER chunks
170
+ * have already reached the consumer replays the answer from the top, in a
171
+ * different model's voice, on top of text the caller has already rendered. So
172
+ * a stream that fails past its first chunk ends the walk, exactly as it ends a
173
+ * retry.
174
+ */
175
+ export async function* streamWithBackupModels(attempt, opts) {
176
+ requireModels(opts.models);
177
+ const shouldTryNext = opts.shouldTryNext ?? defaultShouldTryNext;
178
+ const total = opts.models.length;
179
+ let lastError;
180
+ for (const [index, model] of opts.models.entries()) {
181
+ if (index > 0)
182
+ opts.onFallback?.({ model, position: index + 1, total });
183
+ let emitted = false;
184
+ try {
185
+ for await (const chunk of attempt(model)) {
186
+ emitted = true;
187
+ yield chunk;
188
+ }
189
+ return;
190
+ }
191
+ catch (err) {
192
+ lastError = err;
193
+ opts.onModelFailed?.({ model, error: err, position: index + 1, total });
194
+ if (emitted || !shouldTryNext(err))
195
+ break;
196
+ }
197
+ }
198
+ throw lastError;
199
+ }
200
+ //# sourceMappingURL=retry.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry.js","sourceRoot":"","sources":["../src/retry.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export declare function clampToSchema(value: unknown, node: unknown): unknown;
2
+ //# sourceMappingURL=schema.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAgBA,wBAAgB,aAAa,CAAC,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,GAAG,OAAO,CAkDpE"}
package/dist/schema.js ADDED
@@ -0,0 +1,48 @@
1
+ export function clampToSchema(value, node) {
2
+ if (!node || typeof node !== "object")
3
+ return value;
4
+ const schema = node;
5
+ // Nullable and union fields (`anyOf: [schema, {type: "null"}]`). Fold through
6
+ // every branch: only the one whose type matches transforms, the rest no-op.
7
+ const union = schema.anyOf ?? schema.oneOf;
8
+ if (Array.isArray(union)) {
9
+ return union.reduce((current, branch) => clampToSchema(current, branch), value);
10
+ }
11
+ if (typeof value === "string") {
12
+ const max = schema.maxLength;
13
+ // Ends in an ellipsis so the cut is visible rather than silent. "…" is one
14
+ // UTF-16 unit — the same unit the validators count — so the result lands
15
+ // at exactly `max`.
16
+ if (typeof max === "number" && max >= 1 && value.length > max) {
17
+ return `${value.slice(0, max - 1)}…`;
18
+ }
19
+ return value;
20
+ }
21
+ if (typeof value === "number") {
22
+ let out = value;
23
+ if (typeof schema.maximum === "number" && out > schema.maximum)
24
+ out = schema.maximum;
25
+ if (typeof schema.minimum === "number" && out < schema.minimum)
26
+ out = schema.minimum;
27
+ return out;
28
+ }
29
+ if (Array.isArray(value)) {
30
+ const items = value.map((item) => clampToSchema(item, schema.items));
31
+ const max = schema.maxItems;
32
+ return typeof max === "number" && items.length > max ? items.slice(0, max) : items;
33
+ }
34
+ if (value &&
35
+ typeof value === "object" &&
36
+ schema.properties &&
37
+ typeof schema.properties === "object") {
38
+ const properties = schema.properties;
39
+ const out = { ...value };
40
+ for (const key of Object.keys(out)) {
41
+ if (key in properties)
42
+ out[key] = clampToSchema(out[key], properties[key]);
43
+ }
44
+ return out;
45
+ }
46
+ return value;
47
+ }
48
+ //# sourceMappingURL=schema.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAgBA,MAAM,UAAU,aAAa,CAAC,KAAc,EAAE,IAAa;IACzD,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,KAAK,CAAC;IACpD,MAAM,MAAM,GAAG,IAAkB,CAAC;IAElC,8EAA8E;IAC9E,4EAA4E;IAC5E,MAAM,KAAK,GAAG,MAAM,CAAC,KAAK,IAAI,MAAM,CAAC,KAAK,CAAC;IAC3C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,OAAO,KAAK,CAAC,MAAM,CAAU,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,EAAE,KAAK,CAAC,CAAC;IAC3F,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,GAAG,GAAG,MAAM,CAAC,SAAS,CAAC;QAC7B,2EAA2E;QAC3E,yEAAyE;QACzE,oBAAoB;QACpB,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,IAAI,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,EAAE,CAAC;YAC9D,OAAO,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC;QACvC,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,IAAI,GAAG,GAAG,KAAK,CAAC;QAChB,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO;YAAE,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC;QACrF,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,QAAQ,IAAI,GAAG,GAAG,MAAM,CAAC,OAAO;YAAE,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC;QACrF,OAAO,GAAG,CAAC;IACb,CAAC;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACrE,MAAM,GAAG,GAAG,MAAM,CAAC,QAAQ,CAAC;QAC5B,OAAO,OAAO,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;IACrF,CAAC;IAED,IACE,KAAK;QACL,OAAO,KAAK,KAAK,QAAQ;QACzB,MAAM,CAAC,UAAU;QACjB,OAAO,MAAM,CAAC,UAAU,KAAK,QAAQ,EACrC,CAAC;QACD,MAAM,UAAU,GAAG,MAAM,CAAC,UAAqC,CAAC;QAChE,MAAM,GAAG,GAA4B,EAAE,GAAI,KAAiC,EAAE,CAAC;QAC/E,KAAK,MAAM,GAAG,IAAI,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;YACnC,IAAI,GAAG,IAAI,UAAU;gBAAE,GAAG,CAAC,GAAG,CAAC,GAAG,aAAa,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAC7E,CAAC;QACD,OAAO,GAAG,CAAC;IACb,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Parse a tool call's raw argument string into an object, salvaging what a
3
+ * truncated stream left behind and healing double-escaped text either way.
4
+ *
5
+ * Never throws: a tool call the model malformed is data the caller decides
6
+ * about, not an exception in the transport.
7
+ */
8
+ export declare function parseToolArgs(raw: string): Record<string, unknown>;
9
+ /** Whether `raw` parses at all — how a caller tells a truncated tool call from
10
+ * an intact one, since `parseToolArgs` deliberately never throws. */
11
+ export declare function isCompleteJson(raw: string): boolean;
12
+ //# sourceMappingURL=tool-args.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-args.d.ts","sourceRoot":"","sources":["../src/tool-args.ts"],"names":[],"mappings":"AAuFA;;;;;;GAMG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUlE;AAED;sEACsE;AACtE,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAQnD"}
@@ -0,0 +1,113 @@
1
+ // Reading a model's tool-call arguments, including the ones it broke.
2
+ //
3
+ // Two failure modes cost real answers, and both are invisible — the JSON simply
4
+ // does not parse, and the run reports "no result" while the answer was sitting
5
+ // in the fragments.
6
+ //
7
+ // 1. TRUNCATION. The turn hit its output ceiling mid-argument, so the JSON is
8
+ // cut off. Everything before the cut is still good, and the field the cut
9
+ // landed in holds a half-written answer that beats no answer.
10
+ //
11
+ // 2. DOUBLE ESCAPING. Most models write non-ASCII inside tool JSON as
12
+ // `\uXXXX`, which JSON.parse decodes correctly. Some escape it twice and
13
+ // emit `\\u00e7`, so even a clean parse leaves six literal characters
14
+ // standing and a Portuguese answer reaches the user as `atenção`.
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ /**
19
+ * `\uXXXX` only. An answer that legitimately spells that sequence is
20
+ * vanishingly rare; one that mentions `\n` while talking about code is not.
21
+ */
22
+ const UNICODE_ESCAPE = /\\u([0-9a-fA-F]{4})/g;
23
+ function healUnicodeEscapes(text) {
24
+ return text.replace(UNICODE_ESCAPE, (_match, hex) => String.fromCharCode(parseInt(hex, 16)));
25
+ }
26
+ function healValue(value) {
27
+ if (typeof value === "string")
28
+ return healUnicodeEscapes(value);
29
+ if (Array.isArray(value))
30
+ return value.map(healValue);
31
+ if (isRecord(value))
32
+ return healArgs(value);
33
+ return value;
34
+ }
35
+ function healArgs(args) {
36
+ const out = {};
37
+ for (const [key, value] of Object.entries(args))
38
+ out[key] = healValue(value);
39
+ return out;
40
+ }
41
+ /** The cut can land inside an escape (`…aten\u00`, or a lone `\`). That
42
+ * fragment is not text, and a trailing backslash also stops the field
43
+ * patterns below from matching. */
44
+ const DANGLING_ESCAPE = /\\(?:u[0-9a-fA-F]{0,3})?$/;
45
+ function unescapeJson(text) {
46
+ try {
47
+ return JSON.parse(`"${text}"`);
48
+ }
49
+ catch {
50
+ return text;
51
+ }
52
+ }
53
+ /**
54
+ * Best-effort recovery of `"key": "value"` string fields from truncated JSON.
55
+ *
56
+ * Only strings: they are what a summary field — the one worth rescuing — is
57
+ * made of, and a closing quote is the single reliable boundary in a partial
58
+ * stream. Numbers, booleans and objects are dropped, because a salvaged
59
+ * half-value is worse than none.
60
+ */
61
+ function salvageStringFields(raw) {
62
+ const out = {};
63
+ const text = raw.replace(DANGLING_ESCAPE, "");
64
+ // A completed `"key": "value",` or `"key": "value"}` pair.
65
+ const COMPLETE_FIELD = /"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)"\s*[,}]/gs;
66
+ let match;
67
+ let lastCompleteEnd = 0;
68
+ while ((match = COMPLETE_FIELD.exec(text)) !== null) {
69
+ out[unescapeJson(match[1])] = unescapeJson(match[2]);
70
+ lastCompleteEnd = COMPLETE_FIELD.lastIndex;
71
+ }
72
+ // The tail after the last complete field: if it opens one more string that
73
+ // never closed, keep its content up to the cut.
74
+ const OPEN_FIELD = /"((?:[^"\\]|\\.)*)"\s*:\s*"((?:[^"\\]|\\.)*)$/s;
75
+ const open = OPEN_FIELD.exec(text.slice(lastCompleteEnd));
76
+ if (open?.[2])
77
+ out[unescapeJson(open[1])] = unescapeJson(open[2]);
78
+ return out;
79
+ }
80
+ /**
81
+ * Parse a tool call's raw argument string into an object, salvaging what a
82
+ * truncated stream left behind and healing double-escaped text either way.
83
+ *
84
+ * Never throws: a tool call the model malformed is data the caller decides
85
+ * about, not an exception in the transport.
86
+ */
87
+ export function parseToolArgs(raw) {
88
+ if (!raw.trim())
89
+ return {};
90
+ let parsed;
91
+ try {
92
+ parsed = JSON.parse(raw);
93
+ }
94
+ catch {
95
+ return healArgs(salvageStringFields(raw));
96
+ }
97
+ // A non-object payload is a protocol violation, not a value to pass on.
98
+ return healArgs(isRecord(parsed) ? parsed : {});
99
+ }
100
+ /** Whether `raw` parses at all — how a caller tells a truncated tool call from
101
+ * an intact one, since `parseToolArgs` deliberately never throws. */
102
+ export function isCompleteJson(raw) {
103
+ if (!raw.trim())
104
+ return false;
105
+ try {
106
+ JSON.parse(raw);
107
+ return true;
108
+ }
109
+ catch {
110
+ return false;
111
+ }
112
+ }
113
+ //# sourceMappingURL=tool-args.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tool-args.js","sourceRoot":"","sources":["../src/tool-args.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,82 @@
1
+ import type { JsonObjectSchema, ToolDefinition } from "./types.ts";
2
+ export interface ToolContext {
3
+ /** The model's own call id when there is one — the event, any approval row
4
+ * and the tool message must all key on what the provider expects back. */
5
+ callId?: string;
6
+ signal?: AbortSignal;
7
+ /** Anything the host wants to hand its tools (a db handle, the actor). */
8
+ [key: string]: unknown;
9
+ }
10
+ export type ToolFailure = "invalid_input" | "timeout" | "aborted" | "failed";
11
+ export type ToolOutcome<O> = {
12
+ ok: true;
13
+ callId: string;
14
+ output: O;
15
+ summary: string;
16
+ durationMs: number;
17
+ } | {
18
+ ok: false;
19
+ callId: string;
20
+ kind: ToolFailure;
21
+ /** Fed back to the model verbatim — so it must read as an instruction to
22
+ * a reader who cannot see our stack trace. */
23
+ error: string;
24
+ durationMs: number;
25
+ cause?: unknown;
26
+ };
27
+ export interface ToolSpec<I, O> {
28
+ name: string;
29
+ description: string;
30
+ /** Advertised to the model verbatim. */
31
+ inputSchema: JsonObjectSchema;
32
+ /**
33
+ * Turn raw arguments into `I`, or throw with a message the MODEL can act on.
34
+ * Omit to accept whatever arrived (the schema is then only a hint).
35
+ */
36
+ validate?: (raw: unknown) => I;
37
+ run: (input: I, ctx: ToolContext) => Promise<O>;
38
+ /** How the result reads back to the model. Defaults to JSON. */
39
+ summarize?: (output: O) => string;
40
+ /** Default 60s. A tool with no ceiling can hang a whole run. */
41
+ timeoutMs?: number;
42
+ isReadOnly?: boolean;
43
+ needsApproval?: boolean;
44
+ isConcurrencySafe?: boolean;
45
+ /** A terminal tool ends the run; its validated input is the run's output. */
46
+ isTerminal?: boolean;
47
+ }
48
+ export interface Tool<I = unknown, O = unknown> {
49
+ readonly name: string;
50
+ readonly description: string;
51
+ readonly inputSchema: JsonObjectSchema;
52
+ readonly timeoutMs: number;
53
+ readonly isReadOnly: boolean;
54
+ readonly needsApproval: boolean;
55
+ readonly isConcurrencySafe: boolean;
56
+ readonly isTerminal: boolean;
57
+ definition(): ToolDefinition;
58
+ /** Validate, run, and report the outcome. Never throws except on abort. */
59
+ invoke(rawArgs: unknown, ctx?: ToolContext): Promise<ToolOutcome<O>>;
60
+ /** The typed path for internal callers: throws on failure. */
61
+ call(input: I, ctx?: ToolContext): Promise<O>;
62
+ }
63
+ export declare class ToolTimeoutError extends Error {
64
+ constructor(tool: string, ms: number);
65
+ }
66
+ export declare function defineTool<I = unknown, O = unknown>(spec: ToolSpec<I, O>): Tool<I, O>;
67
+ export declare class ToolRegistry {
68
+ private readonly tools;
69
+ constructor(tools?: readonly Tool[]);
70
+ register(tool: Tool): this;
71
+ get(name: string): Tool | undefined;
72
+ has(name: string): boolean;
73
+ get names(): string[];
74
+ /**
75
+ * Definitions for an allow-list, in the order given — which is the order the
76
+ * model reads them in, and it is part of the cached prompt prefix. Reordering
77
+ * or appending mid-conversation invalidates that prefix, so a caller that
78
+ * cares should freeze the list when the session opens.
79
+ */
80
+ definitions(allow?: readonly string[]): ToolDefinition[];
81
+ }
82
+ //# sourceMappingURL=tools.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tools.d.ts","sourceRoot":"","sources":["../src/tools.ts"],"names":[],"mappings":"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"}
package/dist/tools.js ADDED
@@ -0,0 +1,155 @@
1
+ import { messageOf } from "./errors.js";
2
+ const DEFAULT_TIMEOUT_MS = 60_000;
3
+ export class ToolTimeoutError extends Error {
4
+ constructor(tool, ms) {
5
+ super(`Tool "${tool}" timed out after ${ms}ms`);
6
+ this.name = "ToolTimeoutError";
7
+ }
8
+ }
9
+ /** Rejects when `signal` aborts — races a `run` that ignores its own signal. */
10
+ function abortion(signal) {
11
+ return new Promise((_resolve, reject) => {
12
+ if (signal.aborted)
13
+ reject(signal.reason);
14
+ else
15
+ signal.addEventListener("abort", () => reject(signal.reason), { once: true });
16
+ });
17
+ }
18
+ function newId() {
19
+ return globalThis.crypto?.randomUUID?.() ?? `call_${Math.random().toString(36).slice(2, 12)}`;
20
+ }
21
+ export function defineTool(spec) {
22
+ const timeoutMs = spec.timeoutMs ?? DEFAULT_TIMEOUT_MS;
23
+ const summarize = spec.summarize ?? ((output) => JSON.stringify(output) ?? "");
24
+ let cached = null;
25
+ async function execute(input, ctx) {
26
+ const started = Date.now();
27
+ // Two deadlines compose into the one signal `run` sees: our timeout and
28
+ // the caller's abort. `run` gets a signal it can honour; the race is there
29
+ // for the ones that do not.
30
+ const controller = new AbortController();
31
+ const onOuterAbort = () => controller.abort(ctx.signal?.reason);
32
+ ctx.signal?.addEventListener("abort", onOuterAbort, { once: true });
33
+ let timedOut = false;
34
+ const timer = setTimeout(() => {
35
+ timedOut = true;
36
+ controller.abort(new ToolTimeoutError(spec.name, timeoutMs));
37
+ }, timeoutMs);
38
+ try {
39
+ if (ctx.signal?.aborted)
40
+ throw ctx.signal.reason;
41
+ const output = await Promise.race([
42
+ spec.run(input, { ...ctx, signal: controller.signal }),
43
+ abortion(controller.signal),
44
+ ]);
45
+ return { output, durationMs: Date.now() - started };
46
+ }
47
+ catch (err) {
48
+ throw timedOut ? new ToolTimeoutError(spec.name, timeoutMs) : err;
49
+ }
50
+ finally {
51
+ clearTimeout(timer);
52
+ ctx.signal?.removeEventListener("abort", onOuterAbort);
53
+ }
54
+ }
55
+ function classifyFailure(err, ctx) {
56
+ if (err instanceof ToolTimeoutError)
57
+ return "timeout";
58
+ // The caller's own abort — distinct from our timeout, and never something
59
+ // to report back to the model as a tool that "failed".
60
+ if (ctx.signal?.aborted)
61
+ return "aborted";
62
+ const name = err instanceof Error ? err.name : undefined;
63
+ return name === "AbortError" ? "aborted" : "failed";
64
+ }
65
+ return {
66
+ name: spec.name,
67
+ description: spec.description,
68
+ inputSchema: spec.inputSchema,
69
+ timeoutMs,
70
+ isReadOnly: spec.isReadOnly ?? true,
71
+ needsApproval: spec.needsApproval ?? false,
72
+ isConcurrencySafe: spec.isConcurrencySafe ?? true,
73
+ isTerminal: spec.isTerminal ?? false,
74
+ definition() {
75
+ cached ??= {
76
+ name: spec.name,
77
+ description: spec.description,
78
+ inputSchema: spec.inputSchema,
79
+ };
80
+ return cached;
81
+ },
82
+ async call(input, ctx = {}) {
83
+ return (await execute(input, ctx)).output;
84
+ },
85
+ async invoke(rawArgs, ctx = {}) {
86
+ const callId = ctx.callId ?? newId();
87
+ const started = Date.now();
88
+ let input;
89
+ try {
90
+ input = spec.validate ? spec.validate(rawArgs) : rawArgs;
91
+ }
92
+ catch (err) {
93
+ return {
94
+ ok: false,
95
+ callId,
96
+ kind: "invalid_input",
97
+ error: `Invalid arguments for ${spec.name}: ${messageOf(err)}`,
98
+ durationMs: 0,
99
+ cause: err,
100
+ };
101
+ }
102
+ try {
103
+ const { output, durationMs } = await execute(input, { ...ctx, callId });
104
+ return { ok: true, callId, output, summary: summarize(output), durationMs };
105
+ }
106
+ catch (err) {
107
+ return {
108
+ ok: false,
109
+ callId,
110
+ kind: classifyFailure(err, ctx),
111
+ error: messageOf(err),
112
+ durationMs: Date.now() - started,
113
+ cause: err,
114
+ };
115
+ }
116
+ },
117
+ };
118
+ }
119
+ export class ToolRegistry {
120
+ tools = new Map();
121
+ constructor(tools = []) {
122
+ for (const tool of tools)
123
+ this.register(tool);
124
+ }
125
+ register(tool) {
126
+ this.tools.set(tool.name, tool);
127
+ return this;
128
+ }
129
+ get(name) {
130
+ return this.tools.get(name);
131
+ }
132
+ has(name) {
133
+ return this.tools.has(name);
134
+ }
135
+ get names() {
136
+ return [...this.tools.keys()];
137
+ }
138
+ /**
139
+ * Definitions for an allow-list, in the order given — which is the order the
140
+ * model reads them in, and it is part of the cached prompt prefix. Reordering
141
+ * or appending mid-conversation invalidates that prefix, so a caller that
142
+ * cares should freeze the list when the session opens.
143
+ */
144
+ definitions(allow) {
145
+ const names = allow ?? this.names;
146
+ const out = [];
147
+ for (const name of names) {
148
+ const tool = this.tools.get(name);
149
+ if (tool)
150
+ out.push(tool.definition());
151
+ }
152
+ return out;
153
+ }
154
+ }
155
+ //# sourceMappingURL=tools.js.map