@mindees/ai 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.
- package/LICENSE +31 -0
- package/README.md +57 -0
- package/dist/contract.d.ts +113 -0
- package/dist/contract.d.ts.map +1 -0
- package/dist/contract.js +18 -0
- package/dist/contract.js.map +1 -0
- package/dist/devtools.d.ts +43 -0
- package/dist/devtools.d.ts.map +1 -0
- package/dist/devtools.js +77 -0
- package/dist/devtools.js.map +1 -0
- package/dist/errors.d.ts +21 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +26 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -0
- package/dist/json.d.ts +76 -0
- package/dist/json.d.ts.map +1 -0
- package/dist/json.js +256 -0
- package/dist/json.js.map +1 -0
- package/dist/mappers.d.ts +52 -0
- package/dist/mappers.d.ts.map +1 -0
- package/dist/mappers.js +312 -0
- package/dist/mappers.js.map +1 -0
- package/dist/mock.d.ts +26 -0
- package/dist/mock.d.ts.map +1 -0
- package/dist/mock.js +69 -0
- package/dist/mock.js.map +1 -0
- package/dist/object.d.ts +78 -0
- package/dist/object.d.ts.map +1 -0
- package/dist/object.js +140 -0
- package/dist/object.js.map +1 -0
- package/dist/on-device.d.ts +13 -0
- package/dist/on-device.d.ts.map +1 -0
- package/dist/on-device.js +33 -0
- package/dist/on-device.js.map +1 -0
- package/dist/server.d.ts +42 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +64 -0
- package/dist/server.js.map +1 -0
- package/dist/sse.d.ts +24 -0
- package/dist/sse.d.ts.map +1 -0
- package/dist/sse.js +81 -0
- package/dist/sse.js.map +1 -0
- package/dist/standard-schema.d.ts +89 -0
- package/dist/standard-schema.d.ts.map +1 -0
- package/dist/tools.d.ts +61 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +195 -0
- package/dist/tools.js.map +1 -0
- package/package.json +40 -0
package/dist/sse.js
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AiError } from "./errors.js";
|
|
2
|
+
//#region src/sse.ts
|
|
3
|
+
/**
|
|
4
|
+
* A tiny, hand-rolled Server-Sent Events parser (no `eventsource` dep) for the server
|
|
5
|
+
* backend's streaming responses. Pure-TS: buffers across chunk boundaries, joins
|
|
6
|
+
* multi-line `data:` fields, skips `:` comments / keep-alives, and is driven by an
|
|
7
|
+
* `AsyncIterable<string>` so it's golden-fixture-testable with zero network. See
|
|
8
|
+
* `docs/adr/0018-synapse-server-backend.md`.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Cap on un-dispatched parser state (chars, not bytes — we measure post-decode string
|
|
14
|
+
* length). Bounds BOTH a single line that never gets a newline AND an event whose `data:`
|
|
15
|
+
* lines accumulate without a blank-line terminator, so a hostile/buggy server can't
|
|
16
|
+
* exhaust memory (or stall the mid-stream abort) by streaming endless data with no event
|
|
17
|
+
* boundary.
|
|
18
|
+
*/
|
|
19
|
+
const MAX_SSE_BUFFER = 8 * 1024 * 1024;
|
|
20
|
+
/** Parse an SSE byte/string stream into dispatched {@link SseMessage}s. */
|
|
21
|
+
async function* parseSse(chunks) {
|
|
22
|
+
let buffer = "";
|
|
23
|
+
let dataLines = [];
|
|
24
|
+
let dataSize = 0;
|
|
25
|
+
let event;
|
|
26
|
+
const dispatch = () => {
|
|
27
|
+
if (dataLines.length === 0) {
|
|
28
|
+
event = void 0;
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
const message = {
|
|
32
|
+
data: dataLines.join("\n"),
|
|
33
|
+
event
|
|
34
|
+
};
|
|
35
|
+
dataLines = [];
|
|
36
|
+
dataSize = 0;
|
|
37
|
+
event = void 0;
|
|
38
|
+
return message;
|
|
39
|
+
};
|
|
40
|
+
const feedLine = (raw) => {
|
|
41
|
+
const line = raw.endsWith("\r") ? raw.slice(0, -1) : raw;
|
|
42
|
+
if (line.startsWith(":")) return;
|
|
43
|
+
const colon = line.indexOf(":");
|
|
44
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
45
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
46
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
47
|
+
if (field === "data") {
|
|
48
|
+
dataLines.push(value);
|
|
49
|
+
dataSize += value.length;
|
|
50
|
+
} else if (field === "event") event = value;
|
|
51
|
+
};
|
|
52
|
+
for await (const chunk of chunks) {
|
|
53
|
+
buffer += chunk;
|
|
54
|
+
let newline = buffer.indexOf("\n");
|
|
55
|
+
while (newline !== -1) {
|
|
56
|
+
const line = buffer.slice(0, newline);
|
|
57
|
+
buffer = buffer.slice(newline + 1);
|
|
58
|
+
if (line === "" || line === "\r") {
|
|
59
|
+
const message = dispatch();
|
|
60
|
+
if (message) yield message;
|
|
61
|
+
} else feedLine(line);
|
|
62
|
+
if (dataSize > MAX_SSE_BUFFER) throw new AiError("STREAM_PARSE", `SSE event exceeded ${MAX_SSE_BUFFER} chars without a blank-line terminator`);
|
|
63
|
+
newline = buffer.indexOf("\n");
|
|
64
|
+
}
|
|
65
|
+
if (buffer.length > MAX_SSE_BUFFER) throw new AiError("STREAM_PARSE", `SSE line exceeded ${MAX_SSE_BUFFER} chars without a newline`);
|
|
66
|
+
}
|
|
67
|
+
if (buffer !== "") feedLine(buffer);
|
|
68
|
+
const tail = dispatch();
|
|
69
|
+
if (tail) yield tail;
|
|
70
|
+
}
|
|
71
|
+
/** Adapt a byte stream (e.g. `response.body`) to the string chunks {@link parseSse} expects. */
|
|
72
|
+
async function* decodeChunks(bytes) {
|
|
73
|
+
const decoder = new TextDecoder();
|
|
74
|
+
for await (const chunk of bytes) yield decoder.decode(chunk, { stream: true });
|
|
75
|
+
const tail = decoder.decode();
|
|
76
|
+
if (tail) yield tail;
|
|
77
|
+
}
|
|
78
|
+
//#endregion
|
|
79
|
+
export { decodeChunks, parseSse };
|
|
80
|
+
|
|
81
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","names":[],"sources":["../src/sse.ts"],"sourcesContent":["/**\n * A tiny, hand-rolled Server-Sent Events parser (no `eventsource` dep) for the server\n * backend's streaming responses. Pure-TS: buffers across chunk boundaries, joins\n * multi-line `data:` fields, skips `:` comments / keep-alives, and is driven by an\n * `AsyncIterable<string>` so it's golden-fixture-testable with zero network. See\n * `docs/adr/0018-synapse-server-backend.md`.\n *\n * @module\n */\n\nimport { AiError } from './errors'\n\n/**\n * Cap on un-dispatched parser state (chars, not bytes — we measure post-decode string\n * length). Bounds BOTH a single line that never gets a newline AND an event whose `data:`\n * lines accumulate without a blank-line terminator, so a hostile/buggy server can't\n * exhaust memory (or stall the mid-stream abort) by streaming endless data with no event\n * boundary.\n */\nconst MAX_SSE_BUFFER = 8 * 1024 * 1024\n\n/** One dispatched SSE event. */\nexport interface SseMessage {\n /** The joined `data:` payload. */\n readonly data: string\n /** The `event:` type, if any. */\n readonly event: string | undefined\n}\n\n/** Parse an SSE byte/string stream into dispatched {@link SseMessage}s. */\nexport async function* parseSse(chunks: AsyncIterable<string>): AsyncIterable<SseMessage> {\n let buffer = ''\n let dataLines: string[] = []\n let dataSize = 0 // chars accumulated in dataLines since the last dispatch\n let event: string | undefined\n\n const dispatch = (): SseMessage | undefined => {\n if (dataLines.length === 0) {\n event = undefined\n return undefined\n }\n const message: SseMessage = { data: dataLines.join('\\n'), event }\n dataLines = []\n dataSize = 0\n event = undefined\n return message\n }\n\n const feedLine = (raw: string): void => {\n // Strip a trailing CR so CRLF and LF both work.\n const line = raw.endsWith('\\r') ? raw.slice(0, -1) : raw\n if (line.startsWith(':')) return // comment / keep-alive\n const colon = line.indexOf(':')\n const field = colon === -1 ? line : line.slice(0, colon)\n let value = colon === -1 ? '' : line.slice(colon + 1)\n if (value.startsWith(' ')) value = value.slice(1)\n if (field === 'data') {\n dataLines.push(value)\n dataSize += value.length\n } else if (field === 'event') event = value\n }\n\n for await (const chunk of chunks) {\n buffer += chunk\n let newline = buffer.indexOf('\\n')\n while (newline !== -1) {\n const line = buffer.slice(0, newline)\n buffer = buffer.slice(newline + 1)\n if (line === '' || line === '\\r') {\n const message = dispatch()\n if (message) yield message\n } else {\n feedLine(line)\n }\n // Catch an event whose data lines grow without a blank-line terminator.\n if (dataSize > MAX_SSE_BUFFER) {\n throw new AiError(\n 'STREAM_PARSE',\n `SSE event exceeded ${MAX_SSE_BUFFER} chars without a blank-line terminator`,\n )\n }\n newline = buffer.indexOf('\\n')\n }\n // Catch a single line that never receives a newline.\n if (buffer.length > MAX_SSE_BUFFER) {\n throw new AiError(\n 'STREAM_PARSE',\n `SSE line exceeded ${MAX_SSE_BUFFER} chars without a newline`,\n )\n }\n }\n // Process a trailing line with no newline, then flush a final event.\n if (buffer !== '') feedLine(buffer)\n const tail = dispatch()\n if (tail) yield tail\n}\n\n/** Adapt a byte stream (e.g. `response.body`) to the string chunks {@link parseSse} expects. */\nexport async function* decodeChunks(bytes: AsyncIterable<Uint8Array>): AsyncIterable<string> {\n const decoder = new TextDecoder()\n for await (const chunk of bytes) yield decoder.decode(chunk, { stream: true })\n const tail = decoder.decode()\n if (tail) yield tail\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;AAmBA,MAAM,iBAAiB,IAAI,OAAO;;AAWlC,gBAAuB,SAAS,QAA0D;CACxF,IAAI,SAAS;CACb,IAAI,YAAsB,CAAC;CAC3B,IAAI,WAAW;CACf,IAAI;CAEJ,MAAM,iBAAyC;EAC7C,IAAI,UAAU,WAAW,GAAG;GAC1B,QAAQ,KAAA;GACR;EACF;EACA,MAAM,UAAsB;GAAE,MAAM,UAAU,KAAK,IAAI;GAAG;EAAM;EAChE,YAAY,CAAC;EACb,WAAW;EACX,QAAQ,KAAA;EACR,OAAO;CACT;CAEA,MAAM,YAAY,QAAsB;EAEtC,MAAM,OAAO,IAAI,SAAS,IAAI,IAAI,IAAI,MAAM,GAAG,EAAE,IAAI;EACrD,IAAI,KAAK,WAAW,GAAG,GAAG;EAC1B,MAAM,QAAQ,KAAK,QAAQ,GAAG;EAC9B,MAAM,QAAQ,UAAU,KAAK,OAAO,KAAK,MAAM,GAAG,KAAK;EACvD,IAAI,QAAQ,UAAU,KAAK,KAAK,KAAK,MAAM,QAAQ,CAAC;EACpD,IAAI,MAAM,WAAW,GAAG,GAAG,QAAQ,MAAM,MAAM,CAAC;EAChD,IAAI,UAAU,QAAQ;GACpB,UAAU,KAAK,KAAK;GACpB,YAAY,MAAM;EACpB,OAAO,IAAI,UAAU,SAAS,QAAQ;CACxC;CAEA,WAAW,MAAM,SAAS,QAAQ;EAChC,UAAU;EACV,IAAI,UAAU,OAAO,QAAQ,IAAI;EACjC,OAAO,YAAY,IAAI;GACrB,MAAM,OAAO,OAAO,MAAM,GAAG,OAAO;GACpC,SAAS,OAAO,MAAM,UAAU,CAAC;GACjC,IAAI,SAAS,MAAM,SAAS,MAAM;IAChC,MAAM,UAAU,SAAS;IACzB,IAAI,SAAS,MAAM;GACrB,OACE,SAAS,IAAI;GAGf,IAAI,WAAW,gBACb,MAAM,IAAI,QACR,gBACA,sBAAsB,eAAe,uCACvC;GAEF,UAAU,OAAO,QAAQ,IAAI;EAC/B;EAEA,IAAI,OAAO,SAAS,gBAClB,MAAM,IAAI,QACR,gBACA,qBAAqB,eAAe,yBACtC;CAEJ;CAEA,IAAI,WAAW,IAAI,SAAS,MAAM;CAClC,MAAM,OAAO,SAAS;CACtB,IAAI,MAAM,MAAM;AAClB;;AAGA,gBAAuB,aAAa,OAAyD;CAC3F,MAAM,UAAU,IAAI,YAAY;CAChC,WAAW,MAAM,SAAS,OAAO,MAAM,QAAQ,OAAO,OAAO,EAAE,QAAQ,KAAK,CAAC;CAC7E,MAAM,OAAO,QAAQ,OAAO;CAC5B,IAAI,MAAM,MAAM;AAClB"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
//#region src/standard-schema.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Standard Schema — the validator-agnostic interface (types only, vendored).
|
|
4
|
+
*
|
|
5
|
+
* Synapse validates structured output and tool-call arguments through
|
|
6
|
+
* {@link StandardSchemaV1}, the common interface co-designed by the authors of Zod,
|
|
7
|
+
* Valibot, and ArkType. Any compliant validator (Zod ≥ 3.24 / all of 4.x, Valibot ≥ 1,
|
|
8
|
+
* ArkType ≥ 2, and 20+ others) exposes a `~standard` property and is accepted
|
|
9
|
+
* **directly** — no per-library adapters, no lock-in.
|
|
10
|
+
*
|
|
11
|
+
* These ~60 lines are **vendored on purpose** (the spec FAQ explicitly blesses
|
|
12
|
+
* copy/paste). Vendoring means `@mindees/ai` keeps **zero runtime dependency** to support
|
|
13
|
+
* every validator and never imports `@mindees/router` — the "batteries included,
|
|
14
|
+
* dependencies excluded" doctrine. (Kept in type-level sync with the router's copy: the type
|
|
15
|
+
* bodies are identical; only this package-specific header differs.)
|
|
16
|
+
*
|
|
17
|
+
* `validate` is typed single-argument — the subset Synapse needs; a spec 1.1.0 validator's
|
|
18
|
+
* optional second `options` argument remains assignable (fewer-params rule), so Zod 4 /
|
|
19
|
+
* Valibot 1 / ArkType 2 schemas are accepted directly.
|
|
20
|
+
*
|
|
21
|
+
* Portions adapted from `@standard-schema/spec`, MIT License,
|
|
22
|
+
* Copyright (c) 2024 Colin McDonnell. Permission is hereby granted, free of
|
|
23
|
+
* charge, to any person obtaining a copy of this software and associated
|
|
24
|
+
* documentation files, to deal in the Software without restriction. The above
|
|
25
|
+
* copyright notice and this permission notice shall be included in all copies or
|
|
26
|
+
* substantial portions of the Software.
|
|
27
|
+
*
|
|
28
|
+
* @see https://standardschema.dev
|
|
29
|
+
* @see https://github.com/standard-schema/standard-schema
|
|
30
|
+
* @module
|
|
31
|
+
*/
|
|
32
|
+
/** A schema that conforms to the Standard Schema specification. */
|
|
33
|
+
interface StandardSchemaV1<Input = unknown, Output = Input> {
|
|
34
|
+
/** The Standard Schema properties. */
|
|
35
|
+
readonly '~standard': StandardSchemaV1.Props<Input, Output>;
|
|
36
|
+
}
|
|
37
|
+
declare namespace StandardSchemaV1 {
|
|
38
|
+
/** The Standard Schema properties interface. */
|
|
39
|
+
interface Props<Input = unknown, Output = Input> {
|
|
40
|
+
/** The version number of the standard. */
|
|
41
|
+
readonly version: 1;
|
|
42
|
+
/** The vendor name of the schema library. */
|
|
43
|
+
readonly vendor: string;
|
|
44
|
+
/** Validates unknown input values. May be synchronous or asynchronous. */
|
|
45
|
+
readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>;
|
|
46
|
+
/** Inferred types associated with the schema (present only at the type level). */
|
|
47
|
+
readonly types?: Types<Input, Output> | undefined;
|
|
48
|
+
}
|
|
49
|
+
/** The result interface of the validate function. */
|
|
50
|
+
type Result<Output> = SuccessResult<Output> | FailureResult;
|
|
51
|
+
/** The result interface if validation succeeds. */
|
|
52
|
+
interface SuccessResult<Output> {
|
|
53
|
+
/** The typed output value. */
|
|
54
|
+
readonly value: Output;
|
|
55
|
+
/** The non-existent issues. */
|
|
56
|
+
readonly issues?: undefined;
|
|
57
|
+
}
|
|
58
|
+
/** The result interface if validation fails. */
|
|
59
|
+
interface FailureResult {
|
|
60
|
+
/** The issues of failed validation. */
|
|
61
|
+
readonly issues: ReadonlyArray<Issue>;
|
|
62
|
+
}
|
|
63
|
+
/** The issue interface of the failure output. */
|
|
64
|
+
interface Issue {
|
|
65
|
+
/** The error message of the issue. */
|
|
66
|
+
readonly message: string;
|
|
67
|
+
/** The path of the issue, if any. */
|
|
68
|
+
readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined;
|
|
69
|
+
}
|
|
70
|
+
/** The path segment interface of the issue. */
|
|
71
|
+
interface PathSegment {
|
|
72
|
+
/** The key representing a path segment. */
|
|
73
|
+
readonly key: PropertyKey;
|
|
74
|
+
}
|
|
75
|
+
/** The Standard Schema types interface. */
|
|
76
|
+
interface Types<Input = unknown, Output = Input> {
|
|
77
|
+
/** The input type of the schema. */
|
|
78
|
+
readonly input: Input;
|
|
79
|
+
/** The output type of the schema. */
|
|
80
|
+
readonly output: Output;
|
|
81
|
+
}
|
|
82
|
+
/** Infers the input type of a Standard Schema. */
|
|
83
|
+
type InferInput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['input'];
|
|
84
|
+
/** Infers the output type of a Standard Schema. */
|
|
85
|
+
type InferOutput<Schema extends StandardSchemaV1> = NonNullable<Schema['~standard']['types']>['output'];
|
|
86
|
+
}
|
|
87
|
+
//#endregion
|
|
88
|
+
export { StandardSchemaV1 };
|
|
89
|
+
//# sourceMappingURL=standard-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"standard-schema.d.ts","names":[],"sources":["../src/standard-schema.ts"],"mappings":";;AAgCA;;;;;;;;;;;;;;;;;;AAE4D;AAG5D;;;;;;;;;;;UALiB,gBAAA,2BAA2C,KAAA;EAmBf;EAAA,SAjBlC,WAAA,EAAa,gBAAA,CAAiB,KAAA,CAAM,KAAA,EAAO,MAAA;AAAA;AAAA,kBAGrC,gBAAA;EA2BkB;EAAA,UAzBhB,KAAA,2BAAgC,KAAA;IAiCjB;IAAA,SA/BrB,OAAA;IA+BO;IAAA,SA7BP,MAAA;IAuCsC;IAAA,SArCtC,QAAA,GAAW,KAAA,cAAmB,MAAA,CAAO,MAAA,IAAU,OAAA,CAAQ,MAAA,CAAO,MAAA;IAyCtD;IAAA,SAvCR,KAAA,GAAQ,KAAA,CAAM,KAAA,EAAO,MAAA;EAAA;EA2C0B;EAAA,KAvC9C,MAAA,WAAiB,aAAA,CAAc,MAAA,IAAU,aAAA;EA6CnD;EAAA,UA1Ce,aAAA;IAyCqD;IAAA,SAvC3D,KAAA,EAAO,MAAA;IAjBD;IAAA,SAmBN,MAAA;EAAA;EAnBsC;EAAA,UAuBhC,aAAA;IAnBN;IAAA,SAqBA,MAAA,EAAQ,aAAA,CAAc,KAAA;EAAA;EAnBQ;EAAA,UAuBxB,KAAA;IAvByC;IAAA,SAyB/C,OAAA;IAzB8D;IAAA,SA2B9D,IAAA,GAAO,aAAA,CAAc,WAAA,GAAc,WAAA;EAAA;EAzBrB;EAAA,UA6BR,WAAA;IAzBL;IAAA,SA2BD,GAAA,EAAK,WAAA;EAAA;EA3B2B;EAAA,UA+B1B,KAAA,2BAAgC,KAAA;IA5BhC;IAAA,SA8BN,KAAA,EAAO,KAAA;IA5BP;IAAA,SA8BA,MAAA,EAAQ,MAAA;EAAA;EAxBF;EAAA,KA4BL,UAAA,gBAA0B,gBAAA,IAAoB,WAAA,CACxD,MAAA;EA3BiB;EAAA,KA+BP,WAAA,gBAA2B,gBAAA,IAAoB,WAAA,CACzD,MAAA;AAAA"}
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AbortLike, AiBackend, FinishReason, GenerateRequest, Message, Usage } from "./contract.js";
|
|
2
|
+
import { StandardSchemaV1 } from "./standard-schema.js";
|
|
3
|
+
|
|
4
|
+
//#region src/tools.d.ts
|
|
5
|
+
/** Context passed to a {@link Tool}'s `execute`. */
|
|
6
|
+
interface ToolContext {
|
|
7
|
+
/** Cancellation — long-running tools should honor it. */
|
|
8
|
+
readonly signal?: AbortLike;
|
|
9
|
+
}
|
|
10
|
+
/** A callable tool: the wire {@link ToolDefinition} plus runtime validation + `execute`. */
|
|
11
|
+
interface Tool {
|
|
12
|
+
readonly name: string;
|
|
13
|
+
readonly description?: string;
|
|
14
|
+
/** JSON Schema for the args, sent to the provider verbatim. */
|
|
15
|
+
readonly parameters?: Record<string, unknown>;
|
|
16
|
+
/** Optional Standard Schema validating the model's args before `execute` (sync only). */
|
|
17
|
+
readonly validate?: StandardSchemaV1;
|
|
18
|
+
/** Run the tool. Receives validated, pollution-checked args. */
|
|
19
|
+
readonly execute: (args: unknown, context: ToolContext) => unknown | Promise<unknown>;
|
|
20
|
+
}
|
|
21
|
+
/** Options for {@link runTools}. */
|
|
22
|
+
interface RunToolsOptions {
|
|
23
|
+
/** Hard ceiling on model calls (one `generate` = one step). Default `8`. */
|
|
24
|
+
readonly maxSteps?: number;
|
|
25
|
+
/** Cancellation, polled before/after every generate and execute. */
|
|
26
|
+
readonly signal?: AbortLike;
|
|
27
|
+
/** Run a turn's tool calls sequentially instead of in parallel. Default `false`. */
|
|
28
|
+
readonly sequential?: boolean;
|
|
29
|
+
/** Throw `TOOL_FAILED` on an `execute` throw instead of feeding the error back. Default `false`. */
|
|
30
|
+
readonly throwOnToolError?: boolean;
|
|
31
|
+
/** Truncate a stringified tool result longer than this before feeding it back. */
|
|
32
|
+
readonly maxToolResultChars?: number;
|
|
33
|
+
/** Sampling temperature forwarded to the backend. */
|
|
34
|
+
readonly temperature?: number;
|
|
35
|
+
/** Output-token cap forwarded to the backend. */
|
|
36
|
+
readonly maxOutputTokens?: number;
|
|
37
|
+
}
|
|
38
|
+
/** The result of {@link runTools}. */
|
|
39
|
+
interface RunToolsResult {
|
|
40
|
+
/** The model's final text answer. */
|
|
41
|
+
readonly text: string;
|
|
42
|
+
/** How many model calls were made. */
|
|
43
|
+
readonly steps: number;
|
|
44
|
+
/** The full accumulated transcript (caller's input is never mutated). */
|
|
45
|
+
readonly messages: readonly Message[];
|
|
46
|
+
/** Accumulated usage across all steps, when reported. */
|
|
47
|
+
readonly usage?: Usage;
|
|
48
|
+
/** The final finish reason. */
|
|
49
|
+
readonly finishReason: FinishReason;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Run the bounded tool-calling loop.
|
|
53
|
+
*
|
|
54
|
+
* @throws AiError `MAX_STEPS` if the step ceiling is reached, `ABORTED` on cancellation,
|
|
55
|
+
* `TOOL_FAILED` only when `throwOnToolError` is set and an `execute` throws.
|
|
56
|
+
* @throws TypeError for tool-definition misconfiguration (duplicate/empty names).
|
|
57
|
+
*/
|
|
58
|
+
declare function runTools(backend: Pick<AiBackend, 'generate'>, request: GenerateRequest, tools: readonly Tool[], options?: RunToolsOptions): Promise<RunToolsResult>;
|
|
59
|
+
//#endregion
|
|
60
|
+
export { RunToolsOptions, RunToolsResult, Tool, ToolContext, runTools };
|
|
61
|
+
//# sourceMappingURL=tools.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.d.ts","names":[],"sources":["../src/tools.ts"],"mappings":";;;;;UAqCiB,WAAA;EAcN;EAAA,SAZA,MAAA,GAAS,SAAS;AAAA;;UAIZ,IAAA;EAAA,SACN,IAAA;EAAA,SACA,WAAA;EAUM;EAAA,SARN,UAAA,GAAa,MAAA;;WAEb,QAAA,GAAW,gBAAA;EAQX;EAAA,SANA,OAAA,GAAU,IAAA,WAAe,OAAA,EAAS,WAAA,eAA0B,OAAA;AAAA;;UAItD,eAAA;EAUN;EAAA,SARA,QAAA;EAYA;EAAA,SAVA,MAAA,GAAS,SAAS;EAUH;EAAA,SARf,UAAA;EAYoB;EAAA,SAVpB,gBAAA;EAgBmB;EAAA,SAdnB,kBAAA;EAkBc;EAAA,SAhBd,WAAA;EAgB0B;EAAA,SAd1B,eAAA;AAAA;;UAIM,cAAA;EAQN;EAAA,SANA,IAAA;EAQA;EAAA,SANA,KAAA;EAM0B;EAAA,SAJ1B,QAAA,WAAmB,OAAA;EAoFR;EAAA,SAlFX,KAAA,GAAQ,KAAA;;WAER,YAAA,EAAc,YAAA;AAAA;;;;;;;;iBAgFH,QAAA,CACpB,OAAA,EAAS,IAAA,CAAK,SAAA,eACd,OAAA,EAAS,eAAA,EACT,KAAA,WAAgB,IAAA,IAChB,OAAA,GAAS,eAAA,GACR,OAAA,CAAQ,cAAA"}
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { AiError } from "./errors.js";
|
|
2
|
+
import { containsForbiddenKey, formatIssues } from "./json.js";
|
|
3
|
+
//#region src/tools.ts
|
|
4
|
+
/**
|
|
5
|
+
* Deterministic JSON with sorted object keys (order-independent dedup key + size estimate).
|
|
6
|
+
* Cycle- and bigint-safe so it never throws on a tool result: a back-reference becomes
|
|
7
|
+
* `"[Circular]"` and a bigint its decimal string. (Dedup keys come from validated JSON args,
|
|
8
|
+
* which can't cycle; the safety matters for `truncateResult` on arbitrary tool output.)
|
|
9
|
+
*/
|
|
10
|
+
function stableStringify(value, seen = /* @__PURE__ */ new WeakSet()) {
|
|
11
|
+
if (typeof value === "bigint") return `"${value.toString()}"`;
|
|
12
|
+
if (value === null || typeof value !== "object") return JSON.stringify(value) ?? "null";
|
|
13
|
+
if (seen.has(value)) return "\"[Circular]\"";
|
|
14
|
+
seen.add(value);
|
|
15
|
+
const out = Array.isArray(value) ? `[${value.map((v) => stableStringify(v, seen)).join(",")}]` : `{${Object.keys(value).sort().map((k) => `${JSON.stringify(k)}:${stableStringify(value[k], seen)}`).join(",")}}`;
|
|
16
|
+
seen.delete(value);
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
function addUsage(a, b) {
|
|
20
|
+
if (!a) return b;
|
|
21
|
+
if (!b) return a;
|
|
22
|
+
const sum = {};
|
|
23
|
+
if (a.inputTokens !== void 0 || b.inputTokens !== void 0) sum.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0);
|
|
24
|
+
if (a.outputTokens !== void 0 || b.outputTokens !== void 0) sum.outputTokens = (a.outputTokens ?? 0) + (b.outputTokens ?? 0);
|
|
25
|
+
return sum;
|
|
26
|
+
}
|
|
27
|
+
/** Validate args synchronously (tool schemas must be sync — an async validator throws). */
|
|
28
|
+
function validateArgsSync(tool, args) {
|
|
29
|
+
if (!tool.validate) return {
|
|
30
|
+
ok: true,
|
|
31
|
+
value: args
|
|
32
|
+
};
|
|
33
|
+
const raw = tool.validate["~standard"].validate(args);
|
|
34
|
+
if (raw instanceof Promise) throw new AiError("TOOL_FAILED", `tool "${tool.name}" has an async argument schema (unsupported)`);
|
|
35
|
+
if (typeof raw !== "object" || raw === null) return {
|
|
36
|
+
ok: false,
|
|
37
|
+
message: "validator returned no result"
|
|
38
|
+
};
|
|
39
|
+
if (raw.issues) return {
|
|
40
|
+
ok: false,
|
|
41
|
+
message: formatIssues(Array.isArray(raw.issues) ? raw.issues : [])
|
|
42
|
+
};
|
|
43
|
+
return {
|
|
44
|
+
ok: true,
|
|
45
|
+
value: raw.value
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
function truncateResult(result, maxChars) {
|
|
49
|
+
if (maxChars === void 0) return result;
|
|
50
|
+
if (stableStringify(result).length <= maxChars) return result;
|
|
51
|
+
return {
|
|
52
|
+
error: "tool_result_truncated",
|
|
53
|
+
message: `result exceeded ${maxChars} chars`
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Run the bounded tool-calling loop.
|
|
58
|
+
*
|
|
59
|
+
* @throws AiError `MAX_STEPS` if the step ceiling is reached, `ABORTED` on cancellation,
|
|
60
|
+
* `TOOL_FAILED` only when `throwOnToolError` is set and an `execute` throws.
|
|
61
|
+
* @throws TypeError for tool-definition misconfiguration (duplicate/empty names).
|
|
62
|
+
*/
|
|
63
|
+
async function runTools(backend, request, tools, options = {}) {
|
|
64
|
+
const maxSteps = options.maxSteps ?? 8;
|
|
65
|
+
if (!Number.isInteger(maxSteps) || maxSteps < 1) throw new TypeError("maxSteps must be an integer >= 1");
|
|
66
|
+
const signal = options.signal ?? request.signal;
|
|
67
|
+
const byName = /* @__PURE__ */ new Map();
|
|
68
|
+
for (const tool of tools) {
|
|
69
|
+
if (!tool.name) throw new TypeError("every tool must have a non-empty name");
|
|
70
|
+
if (byName.has(tool.name)) throw new TypeError(`duplicate tool name "${tool.name}"`);
|
|
71
|
+
byName.set(tool.name, tool);
|
|
72
|
+
}
|
|
73
|
+
const definitions = tools.map((t) => t.parameters === void 0 ? {
|
|
74
|
+
name: t.name,
|
|
75
|
+
...t.description !== void 0 ? { description: t.description } : {}
|
|
76
|
+
} : {
|
|
77
|
+
name: t.name,
|
|
78
|
+
parameters: t.parameters,
|
|
79
|
+
...t.description !== void 0 ? { description: t.description } : {}
|
|
80
|
+
});
|
|
81
|
+
const messages = [...request.messages];
|
|
82
|
+
const callCache = /* @__PURE__ */ new Map();
|
|
83
|
+
let usage;
|
|
84
|
+
let steps = 0;
|
|
85
|
+
const aborted = () => signal?.aborted === true;
|
|
86
|
+
const runCall = async (call) => {
|
|
87
|
+
const tool = byName.get(call.name);
|
|
88
|
+
if (!tool) return {
|
|
89
|
+
error: "unknown_tool",
|
|
90
|
+
message: `no tool named "${call.name}"`
|
|
91
|
+
};
|
|
92
|
+
if (containsForbiddenKey(call.args)) return {
|
|
93
|
+
error: "invalid_arguments",
|
|
94
|
+
message: "arguments contain a forbidden key"
|
|
95
|
+
};
|
|
96
|
+
const validated = validateArgsSync(tool, call.args);
|
|
97
|
+
if (!validated.ok) return {
|
|
98
|
+
error: "invalid_arguments",
|
|
99
|
+
message: validated.message
|
|
100
|
+
};
|
|
101
|
+
const key = `${tool.name}:${stableStringify(validated.value)}`;
|
|
102
|
+
let exec = callCache.get(key);
|
|
103
|
+
if (!exec) {
|
|
104
|
+
if (aborted()) throw new AiError("ABORTED", "tool loop aborted");
|
|
105
|
+
exec = (async () => {
|
|
106
|
+
const ctx = signal ? { signal } : {};
|
|
107
|
+
const raw = await tool.execute(validated.value, ctx);
|
|
108
|
+
if (aborted()) throw new AiError("ABORTED", "tool loop aborted");
|
|
109
|
+
return truncateResult(raw, options.maxToolResultChars);
|
|
110
|
+
})();
|
|
111
|
+
callCache.set(key, exec);
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return await exec;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err instanceof AiError && err.code === "ABORTED") throw err;
|
|
117
|
+
if (options.throwOnToolError) throw new AiError("TOOL_FAILED", `tool "${tool.name}" failed: ${errorMessage(err)}`);
|
|
118
|
+
return {
|
|
119
|
+
error: "tool_failed",
|
|
120
|
+
message: errorMessage(err)
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
for (;;) {
|
|
125
|
+
if (steps >= maxSteps) throw new AiError("MAX_STEPS", `tool loop exceeded ${maxSteps} steps`);
|
|
126
|
+
if (aborted()) throw new AiError("ABORTED", "tool loop aborted");
|
|
127
|
+
const req = {
|
|
128
|
+
messages,
|
|
129
|
+
tools: definitions,
|
|
130
|
+
...options.temperature !== void 0 ? { temperature: options.temperature } : {},
|
|
131
|
+
...options.maxOutputTokens !== void 0 ? { maxOutputTokens: options.maxOutputTokens } : {},
|
|
132
|
+
...signal ? { signal } : {}
|
|
133
|
+
};
|
|
134
|
+
steps++;
|
|
135
|
+
const result = await backend.generate(req);
|
|
136
|
+
usage = addUsage(usage, result.usage);
|
|
137
|
+
if (aborted()) throw new AiError("ABORTED", "tool loop aborted");
|
|
138
|
+
const calls = result.toolCalls ?? [];
|
|
139
|
+
if (calls.length === 0) {
|
|
140
|
+
if (result.text) messages.push({
|
|
141
|
+
role: "assistant",
|
|
142
|
+
content: result.text
|
|
143
|
+
});
|
|
144
|
+
const finishReason = result.finishReason === "tool-calls" ? "stop" : result.finishReason;
|
|
145
|
+
return usage === void 0 ? {
|
|
146
|
+
text: result.text,
|
|
147
|
+
steps,
|
|
148
|
+
messages,
|
|
149
|
+
finishReason
|
|
150
|
+
} : {
|
|
151
|
+
text: result.text,
|
|
152
|
+
steps,
|
|
153
|
+
messages,
|
|
154
|
+
usage,
|
|
155
|
+
finishReason
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
const assistantParts = [];
|
|
159
|
+
if (result.text) assistantParts.push({
|
|
160
|
+
type: "text",
|
|
161
|
+
text: result.text
|
|
162
|
+
});
|
|
163
|
+
for (const c of calls) assistantParts.push(c);
|
|
164
|
+
messages.push({
|
|
165
|
+
role: "assistant",
|
|
166
|
+
content: assistantParts
|
|
167
|
+
});
|
|
168
|
+
if (aborted()) throw new AiError("ABORTED", "tool loop aborted");
|
|
169
|
+
let results;
|
|
170
|
+
if (options.sequential) {
|
|
171
|
+
results = [];
|
|
172
|
+
for (const call of calls) results.push(await runCall(call));
|
|
173
|
+
} else results = await Promise.all(calls.map((call) => runCall(call)));
|
|
174
|
+
for (let i = 0; i < calls.length; i++) {
|
|
175
|
+
const call = calls[i];
|
|
176
|
+
if (!call) continue;
|
|
177
|
+
messages.push({
|
|
178
|
+
role: "tool",
|
|
179
|
+
content: [{
|
|
180
|
+
type: "tool-result",
|
|
181
|
+
id: call.id,
|
|
182
|
+
name: call.name,
|
|
183
|
+
result: results[i]
|
|
184
|
+
}]
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
function errorMessage(err) {
|
|
190
|
+
return err instanceof Error ? err.message : String(err);
|
|
191
|
+
}
|
|
192
|
+
//#endregion
|
|
193
|
+
export { runTools };
|
|
194
|
+
|
|
195
|
+
//# sourceMappingURL=tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tools.js","names":[],"sources":["../src/tools.ts"],"sourcesContent":["/**\n * The Synapse bounded tool-calling loop. `runTools` drives a model that can call tools:\n * generate → (validate args → execute tools → feed results back) → repeat, until the model\n * answers with text or a hard step ceiling is hit. Built purely on `AiBackend.generate`, so\n * the deterministic mock (scripted tool-call mode) exercises the whole loop offline.\n *\n * Safety is the point (tool args are model-produced ⇒ untrusted, tools can have side effects):\n * a hard `maxSteps` ceiling; deep prototype-pollution rejection + Standard-Schema validation\n * of **args** BEFORE any `execute`; invalid args are fed back as a recoverable tool-result (not\n * a thrown error); each `execute` is isolated (one tool's throw can't abort the batch);\n * identical repeated calls share ONE execution (no duplicate side effects, even within a\n * parallel turn); parallel execution with deterministic (request-order) result history;\n * four-point abort polling. See `docs/adr/0020-synapse-tool-calling.md`.\n *\n * Note: a tool's RETURN value is the tool author's responsibility and is passed through to the\n * transcript/caller as-is (not deep-sanitized) — it may be a rich object (Date, class). If a\n * tool returns untrusted fetched data, the tool should sanitize it before returning.\n *\n * @module\n */\n\nimport type {\n AbortLike,\n AiBackend,\n FinishReason,\n GenerateRequest,\n Message,\n Part,\n ToolCallPart,\n ToolDefinition,\n Usage,\n} from './contract'\nimport { AiError } from './errors'\nimport { containsForbiddenKey, formatIssues } from './json'\nimport type { StandardSchemaV1 } from './standard-schema'\n\n/** Context passed to a {@link Tool}'s `execute`. */\nexport interface ToolContext {\n /** Cancellation — long-running tools should honor it. */\n readonly signal?: AbortLike\n}\n\n/** A callable tool: the wire {@link ToolDefinition} plus runtime validation + `execute`. */\nexport interface Tool {\n readonly name: string\n readonly description?: string\n /** JSON Schema for the args, sent to the provider verbatim. */\n readonly parameters?: Record<string, unknown>\n /** Optional Standard Schema validating the model's args before `execute` (sync only). */\n readonly validate?: StandardSchemaV1\n /** Run the tool. Receives validated, pollution-checked args. */\n readonly execute: (args: unknown, context: ToolContext) => unknown | Promise<unknown>\n}\n\n/** Options for {@link runTools}. */\nexport interface RunToolsOptions {\n /** Hard ceiling on model calls (one `generate` = one step). Default `8`. */\n readonly maxSteps?: number\n /** Cancellation, polled before/after every generate and execute. */\n readonly signal?: AbortLike\n /** Run a turn's tool calls sequentially instead of in parallel. Default `false`. */\n readonly sequential?: boolean\n /** Throw `TOOL_FAILED` on an `execute` throw instead of feeding the error back. Default `false`. */\n readonly throwOnToolError?: boolean\n /** Truncate a stringified tool result longer than this before feeding it back. */\n readonly maxToolResultChars?: number\n /** Sampling temperature forwarded to the backend. */\n readonly temperature?: number\n /** Output-token cap forwarded to the backend. */\n readonly maxOutputTokens?: number\n}\n\n/** The result of {@link runTools}. */\nexport interface RunToolsResult {\n /** The model's final text answer. */\n readonly text: string\n /** How many model calls were made. */\n readonly steps: number\n /** The full accumulated transcript (caller's input is never mutated). */\n readonly messages: readonly Message[]\n /** Accumulated usage across all steps, when reported. */\n readonly usage?: Usage\n /** The final finish reason. */\n readonly finishReason: FinishReason\n}\n\n/** A structured tool-result payload fed back to the model on a recoverable problem. */\ninterface ToolErrorResult {\n readonly error: 'unknown_tool' | 'invalid_arguments' | 'tool_failed'\n readonly message: string\n}\n\n/**\n * Deterministic JSON with sorted object keys (order-independent dedup key + size estimate).\n * Cycle- and bigint-safe so it never throws on a tool result: a back-reference becomes\n * `\"[Circular]\"` and a bigint its decimal string. (Dedup keys come from validated JSON args,\n * which can't cycle; the safety matters for `truncateResult` on arbitrary tool output.)\n */\nfunction stableStringify(value: unknown, seen: WeakSet<object> = new WeakSet()): string {\n if (typeof value === 'bigint') return `\"${value.toString()}\"`\n if (value === null || typeof value !== 'object') return JSON.stringify(value) ?? 'null'\n if (seen.has(value)) return '\"[Circular]\"'\n seen.add(value)\n const out = Array.isArray(value)\n ? `[${value.map((v) => stableStringify(v, seen)).join(',')}]`\n : `{${Object.keys(value as Record<string, unknown>)\n .sort()\n .map(\n (k) =>\n `${JSON.stringify(k)}:${stableStringify((value as Record<string, unknown>)[k], seen)}`,\n )\n .join(',')}}`\n seen.delete(value)\n return out\n}\n\nfunction addUsage(a: Usage | undefined, b: Usage | undefined): Usage | undefined {\n if (!a) return b\n if (!b) return a\n const sum: { inputTokens?: number; outputTokens?: number } = {}\n if (a.inputTokens !== undefined || b.inputTokens !== undefined) {\n sum.inputTokens = (a.inputTokens ?? 0) + (b.inputTokens ?? 0)\n }\n if (a.outputTokens !== undefined || b.outputTokens !== undefined) {\n sum.outputTokens = (a.outputTokens ?? 0) + (b.outputTokens ?? 0)\n }\n return sum\n}\n\n/** Validate args synchronously (tool schemas must be sync — an async validator throws). */\nfunction validateArgsSync(\n tool: Tool,\n args: unknown,\n): { ok: true; value: unknown } | { ok: false; message: string } {\n if (!tool.validate) return { ok: true, value: args }\n const raw = tool.validate['~standard'].validate(args)\n if (raw instanceof Promise) {\n throw new AiError(\n 'TOOL_FAILED',\n `tool \"${tool.name}\" has an async argument schema (unsupported)`,\n )\n }\n if (typeof raw !== 'object' || raw === null)\n return { ok: false, message: 'validator returned no result' }\n if (raw.issues)\n return { ok: false, message: formatIssues(Array.isArray(raw.issues) ? raw.issues : []) }\n return { ok: true, value: raw.value }\n}\n\nfunction truncateResult(result: unknown, maxChars: number | undefined): unknown {\n if (maxChars === undefined) return result\n const serialized = stableStringify(result)\n if (serialized.length <= maxChars) return result\n return { error: 'tool_result_truncated', message: `result exceeded ${maxChars} chars` }\n}\n\n/**\n * Run the bounded tool-calling loop.\n *\n * @throws AiError `MAX_STEPS` if the step ceiling is reached, `ABORTED` on cancellation,\n * `TOOL_FAILED` only when `throwOnToolError` is set and an `execute` throws.\n * @throws TypeError for tool-definition misconfiguration (duplicate/empty names).\n */\nexport async function runTools(\n backend: Pick<AiBackend, 'generate'>,\n request: GenerateRequest,\n tools: readonly Tool[],\n options: RunToolsOptions = {},\n): Promise<RunToolsResult> {\n const maxSteps = options.maxSteps ?? 8\n if (!Number.isInteger(maxSteps) || maxSteps < 1) {\n throw new TypeError('maxSteps must be an integer >= 1')\n }\n const signal = options.signal ?? request.signal\n\n // Validate tool definitions up front (programmer error, not a model error).\n const byName = new Map<string, Tool>()\n for (const tool of tools) {\n if (!tool.name) throw new TypeError('every tool must have a non-empty name')\n if (byName.has(tool.name)) throw new TypeError(`duplicate tool name \"${tool.name}\"`)\n byName.set(tool.name, tool)\n }\n const definitions: ToolDefinition[] = tools.map((t) =>\n t.parameters === undefined\n ? { name: t.name, ...(t.description !== undefined ? { description: t.description } : {}) }\n : {\n name: t.name,\n parameters: t.parameters,\n ...(t.description !== undefined ? { description: t.description } : {}),\n },\n )\n\n const messages: Message[] = [...request.messages]\n // One promise per (name,args) for the whole run: concurrent identical calls share it (no\n // double-fire in a parallel turn) AND a later identical call reuses the settled outcome —\n // success OR failure — so a throwing tool isn't re-executed either (no duplicate side\n // effects). The per-call error POLICY (throw vs feed-back) is applied at await, not cached.\n const callCache = new Map<string, Promise<unknown>>()\n let usage: Usage | undefined\n let steps = 0\n\n const aborted = (): boolean => signal?.aborted === true\n\n // Execute one validated call (or produce a fed-back error result). Honors abort + dedup.\n const runCall = async (call: ToolCallPart): Promise<unknown> => {\n const tool = byName.get(call.name)\n if (!tool) {\n return {\n error: 'unknown_tool',\n message: `no tool named \"${call.name}\"`,\n } satisfies ToolErrorResult\n }\n if (containsForbiddenKey(call.args)) {\n return {\n error: 'invalid_arguments',\n message: 'arguments contain a forbidden key',\n } satisfies ToolErrorResult\n }\n const validated = validateArgsSync(tool, call.args)\n if (!validated.ok) {\n return { error: 'invalid_arguments', message: validated.message } satisfies ToolErrorResult\n }\n const key = `${tool.name}:${stableStringify(validated.value)}`\n let exec = callCache.get(key)\n if (!exec) {\n if (aborted()) throw new AiError('ABORTED', 'tool loop aborted')\n exec = (async () => {\n const ctx: ToolContext = signal ? { signal } : {}\n const raw = await tool.execute(validated.value, ctx)\n if (aborted()) throw new AiError('ABORTED', 'tool loop aborted')\n return truncateResult(raw, options.maxToolResultChars)\n })()\n callCache.set(key, exec)\n }\n try {\n return await exec\n } catch (err) {\n if (err instanceof AiError && err.code === 'ABORTED') throw err\n if (options.throwOnToolError) {\n throw new AiError('TOOL_FAILED', `tool \"${tool.name}\" failed: ${errorMessage(err)}`)\n }\n return { error: 'tool_failed', message: errorMessage(err) } satisfies ToolErrorResult\n }\n }\n\n // The loop is bounded by the maxSteps check below (throws MAX_STEPS) and by terminal turns.\n for (;;) {\n if (steps >= maxSteps) {\n throw new AiError('MAX_STEPS', `tool loop exceeded ${maxSteps} steps`)\n }\n if (aborted()) throw new AiError('ABORTED', 'tool loop aborted')\n\n const req: GenerateRequest = {\n messages,\n tools: definitions,\n ...(options.temperature !== undefined ? { temperature: options.temperature } : {}),\n ...(options.maxOutputTokens !== undefined\n ? { maxOutputTokens: options.maxOutputTokens }\n : {}),\n ...(signal ? { signal } : {}),\n }\n steps++\n const result = await backend.generate(req)\n usage = addUsage(usage, result.usage)\n if (aborted()) throw new AiError('ABORTED', 'tool loop aborted')\n\n const calls = result.toolCalls ?? []\n // Drive on tool-call presence, not finishReason: 'tool-calls' with no calls is terminal,\n // and a provider that under-reports finishReason but includes calls still executes them.\n if (calls.length === 0) {\n // Record the final assistant answer so the returned transcript is complete (callers may\n // resume from `messages`).\n if (result.text) messages.push({ role: 'assistant', content: result.text })\n // Normalize a stale 'tool-calls' (no calls actually emitted) to 'stop' so a caller\n // branching on the result's finishReason isn't told work is still pending.\n const finishReason: FinishReason =\n result.finishReason === 'tool-calls' ? 'stop' : result.finishReason\n return usage === undefined\n ? { text: result.text, steps, messages, finishReason }\n : { text: result.text, steps, messages, usage, finishReason }\n }\n\n // Record the assistant turn (text part, if any, then the tool-call parts).\n const assistantParts: Part[] = []\n if (result.text) assistantParts.push({ type: 'text', text: result.text })\n for (const c of calls) assistantParts.push(c)\n messages.push({ role: 'assistant', content: assistantParts })\n\n if (aborted()) throw new AiError('ABORTED', 'tool loop aborted')\n\n // Execute (parallel by default), but append results in the model's REQUESTED order so the\n // transcript is deterministic regardless of completion order.\n let results: unknown[]\n if (options.sequential) {\n results = []\n for (const call of calls) results.push(await runCall(call))\n } else {\n results = await Promise.all(calls.map((call) => runCall(call)))\n }\n for (let i = 0; i < calls.length; i++) {\n const call = calls[i]\n if (!call) continue\n messages.push({\n role: 'tool',\n content: [{ type: 'tool-result', id: call.id, name: call.name, result: results[i] }],\n })\n }\n }\n}\n\nfunction errorMessage(err: unknown): string {\n return err instanceof Error ? err.message : String(err)\n}\n"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@mindees/ai",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "MindeesNative Synapse - provider-agnostic AI: a pure-TS contract with mock + server backends, streaming via async iterables, structured output, and tool calling (on-device runtime is a research track).",
|
|
5
|
+
"license": "MIT OR Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"sideEffects": false,
|
|
8
|
+
"files": [
|
|
9
|
+
"dist"
|
|
10
|
+
],
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"types": "./dist/index.d.ts",
|
|
14
|
+
"import": "./dist/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./server": {
|
|
17
|
+
"types": "./dist/server.d.ts",
|
|
18
|
+
"import": "./dist/server.js"
|
|
19
|
+
},
|
|
20
|
+
"./devtools": {
|
|
21
|
+
"types": "./dist/devtools.d.ts",
|
|
22
|
+
"import": "./dist/devtools.js"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"publishConfig": {
|
|
26
|
+
"access": "public"
|
|
27
|
+
},
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/mindees/mindees.git",
|
|
31
|
+
"directory": "packages/ai"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@mindees/core": "0.1.0"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsdown",
|
|
38
|
+
"typecheck": "tsc --noEmit"
|
|
39
|
+
}
|
|
40
|
+
}
|