@tokz/ai-sdk 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 +21 -0
- package/README.md +120 -0
- package/dist/index.d.ts +342 -0
- package/dist/index.js +367 -0
- package/dist/index.js.map +1 -0
- package/package.json +52 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 tokz
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# `@tokz/ai-sdk`
|
|
2
|
+
|
|
3
|
+
Drop-in wrapper for the [Vercel AI SDK](https://ai-sdk.dev). It intercepts the
|
|
4
|
+
tool-to-model boundary: your tools keep returning what they always returned, and the
|
|
5
|
+
model sees a compressed view of it.
|
|
6
|
+
|
|
7
|
+
Compression is structural — every byte the model reads is a byte-exact substring of
|
|
8
|
+
the tool's own output, chosen deterministically on the CPU. Nothing is rewritten, and
|
|
9
|
+
nothing leaves the process.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add @tokz/ai-sdk ai
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`ai` is a peer dependency (`>=5`). This package builds and typechecks without it; you
|
|
18
|
+
only need it at runtime, and even then only if you do not pass your own `delegate`.
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { generateText } from "@tokz/ai-sdk";
|
|
24
|
+
import { openai } from "@ai-sdk/openai";
|
|
25
|
+
import { tool } from "ai";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
|
|
28
|
+
const listPods = tool({
|
|
29
|
+
description: "List pods in a namespace",
|
|
30
|
+
inputSchema: z.object({ namespace: z.string() }),
|
|
31
|
+
execute: async ({ namespace }) => {
|
|
32
|
+
const res = await fetch(`https://k8s.internal/api/v1/namespaces/${namespace}/pods`);
|
|
33
|
+
return res.json(); // 40 KB of near-identical rows
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const result = await generateText({
|
|
38
|
+
model: openai("gpt-5.4"),
|
|
39
|
+
messages: [{ role: "user", content: "Which pods in prod are unhealthy?" }],
|
|
40
|
+
tools: { listPods },
|
|
41
|
+
tokz: {
|
|
42
|
+
targetRatio: 0.45,
|
|
43
|
+
autoExpand: "on-reference",
|
|
44
|
+
conversationCompaction: true,
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
console.log(result.text);
|
|
49
|
+
console.log(result.tokz.originalBytes, "->", result.tokz.renderedBytes);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Everything except `tokz` is forwarded to `ai.generateText` untouched, and the result
|
|
53
|
+
is the SDK's result with one extra field, `result.tokz`.
|
|
54
|
+
|
|
55
|
+
## The `tokz` option block
|
|
56
|
+
|
|
57
|
+
| Option | Default | Effect |
|
|
58
|
+
| --- | --- | --- |
|
|
59
|
+
| `targetRatio` | `0.45` | Share of tool-output bytes to keep. The structural skeleton is never dropped, so the achieved ratio can be higher. |
|
|
60
|
+
| `autoExpand` | `"never"` | `"on-reference"` recovers elided content the answer refers to and re-prompts **once**. |
|
|
61
|
+
| `conversationCompaction` | `false` | Re-compresses retained tool results in `messages` before delegating. |
|
|
62
|
+
| `compactionRatio` | `0.6 * targetRatio` | Budget used by that compaction. |
|
|
63
|
+
| `store` | shared store | Where originals and span maps are retained. Pass one per conversation if a process serves many. |
|
|
64
|
+
| `delegate` | `ai.generateText` | The underlying call. Injectable for tests. |
|
|
65
|
+
|
|
66
|
+
## Recovering the original
|
|
67
|
+
|
|
68
|
+
The original text never reaches the model, but it never leaves you either:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
import { expand } from "@tokz/ai-sdk";
|
|
72
|
+
|
|
73
|
+
const record = result.tokz.store.get(toolCallId)!;
|
|
74
|
+
record.originalText; // exactly what the tool returned
|
|
75
|
+
expand(record.originalText, record.result.spanMap!); // verified round-trip
|
|
76
|
+
expand(record.originalText, record.result.spanMap!, { elision: 0 }); // just the dropped run
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## Compacting history
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
import { compactHistory } from "@tokz/ai-sdk";
|
|
83
|
+
|
|
84
|
+
const tighter = compactHistory(messages, 0.2);
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
This is a pure local function. The original text and its span map are already in the
|
|
88
|
+
store, so a smaller view of an old tool result is the same deterministic CPU pass that
|
|
89
|
+
produced the first view — **zero API calls, zero inference**. A wrapper that discarded
|
|
90
|
+
the original would have to ask a model to summarise history, which costs more than the
|
|
91
|
+
tokens it saves.
|
|
92
|
+
|
|
93
|
+
Messages are not mutated; changed ones come back as new objects.
|
|
94
|
+
|
|
95
|
+
## Auto-expand is a heuristic
|
|
96
|
+
|
|
97
|
+
`detectReferencedElisions` decides that the model "referred to" elided content by
|
|
98
|
+
vocabulary overlap: it recovers each elided run locally, takes the identifiers in it,
|
|
99
|
+
and flags the run when the response mentions one at a word boundary. It over-fires on
|
|
100
|
+
generic words and under-fires when the model paraphrases without naming anything.
|
|
101
|
+
|
|
102
|
+
Treat a hit as a cheap reason to re-supply context, not as proof. The *recovery* is
|
|
103
|
+
exact — `expand` verifies `srcSha256` and returns the dropped bytes verbatim — only
|
|
104
|
+
the *trigger* is fuzzy.
|
|
105
|
+
|
|
106
|
+
```ts
|
|
107
|
+
import { detectReferencedElisions, expansionMessage } from "@tokz/ai-sdk";
|
|
108
|
+
|
|
109
|
+
const hits = detectReferencedElisions(result.text!, result.tokz.store);
|
|
110
|
+
if (hits.length) messages.push(expansionMessage(hits));
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## What the model actually sees
|
|
114
|
+
|
|
115
|
+
Wrapped tools set `toModelOutput` — the AI SDK's documented tool-to-model hook — *and*
|
|
116
|
+
return the compressed render from `execute`. Both, deliberately: the hook is the
|
|
117
|
+
correct mechanism, and returning the render as well means the original cannot reach a
|
|
118
|
+
message even on an SDK version that skips the hook. Tools with no server-side
|
|
119
|
+
`execute` (client-side or provider-executed) are left alone, as are tools that stream
|
|
120
|
+
their output as an `AsyncIterable`.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The wire contract. Every SDK, API response, and customer integration binds to
|
|
3
|
+
* these shapes. Versioned; changes must be additive.
|
|
4
|
+
*
|
|
5
|
+
* All offsets are UTF-8 **byte** offsets, half-open [s, e) — not JS string
|
|
6
|
+
* indices — so "byte-exact substring" survives non-ASCII payloads and any future
|
|
7
|
+
* non-JS SDK.
|
|
8
|
+
*/
|
|
9
|
+
type Span = {
|
|
10
|
+
s: number;
|
|
11
|
+
e: number;
|
|
12
|
+
};
|
|
13
|
+
/** An elided run. `after` indexes into `spans[]`: the elision sits immediately
|
|
14
|
+
* after that span (`-1` means it precedes the first). `bytes` counts dropped
|
|
15
|
+
* source bytes. */
|
|
16
|
+
type Elision = {
|
|
17
|
+
after: number;
|
|
18
|
+
bytes: number;
|
|
19
|
+
note?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Content types the structural path can parse. `unknown` is an honest
|
|
22
|
+
* passthrough, not an error. */
|
|
23
|
+
type StructuralContentType = "json" | "jsonl" | "markdown-table" | "csv" | "diff" | "unknown";
|
|
24
|
+
/** Content types the semantic path handles. */
|
|
25
|
+
type SemanticContentType = "prose" | "rag" | "chat";
|
|
26
|
+
type ContentType = StructuralContentType | "prose";
|
|
27
|
+
type SpanMap = {
|
|
28
|
+
version: 1;
|
|
29
|
+
srcBytes: number;
|
|
30
|
+
/** SHA-256 hex of the source bytes. Verified before expanding, so a map can
|
|
31
|
+
* never be applied to text it was not built from. */
|
|
32
|
+
srcSha256: string;
|
|
33
|
+
contentType: ContentType;
|
|
34
|
+
/** Sorted ascending, non-overlapping, adjacency-merged. */
|
|
35
|
+
spans: Span[];
|
|
36
|
+
elisions: Elision[];
|
|
37
|
+
stats: {
|
|
38
|
+
keptBytes: number;
|
|
39
|
+
ratio: number;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* One run of rewritten output traced back to the input ranges that produced it.
|
|
44
|
+
* This is what a black-box rewriter cannot offer: even when the text is not
|
|
45
|
+
* byte-identical, every part of it is attributable.
|
|
46
|
+
*/
|
|
47
|
+
type ProvenanceChunk = {
|
|
48
|
+
/** Byte offsets into the compressed output. */
|
|
49
|
+
outS: number;
|
|
50
|
+
outE: number;
|
|
51
|
+
/** Input ranges that contributed. */
|
|
52
|
+
inSpans: Span[];
|
|
53
|
+
/** Model confidence in [0, 1]. */
|
|
54
|
+
confidence: number;
|
|
55
|
+
};
|
|
56
|
+
type ProvenanceMap = {
|
|
57
|
+
version: 1;
|
|
58
|
+
srcBytes: number;
|
|
59
|
+
srcSha256: string;
|
|
60
|
+
contentType: SemanticContentType;
|
|
61
|
+
chunks: ProvenanceChunk[];
|
|
62
|
+
stats: {
|
|
63
|
+
keptBytes: number;
|
|
64
|
+
ratio: number;
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* Discriminated on `method` so a consumer cannot read `compressedText` off a
|
|
69
|
+
* structural result: the structural path never produces text server-side, and
|
|
70
|
+
* the type system should enforce that rather than a convention.
|
|
71
|
+
*/
|
|
72
|
+
type CompressionResult = {
|
|
73
|
+
method: "structural";
|
|
74
|
+
spanMap: SpanMap;
|
|
75
|
+
compressedText?: never;
|
|
76
|
+
provenanceMap?: never;
|
|
77
|
+
} | {
|
|
78
|
+
method: "semantic";
|
|
79
|
+
compressedText: string;
|
|
80
|
+
provenanceMap: ProvenanceMap;
|
|
81
|
+
spanMap?: never;
|
|
82
|
+
} | {
|
|
83
|
+
method: "passthrough";
|
|
84
|
+
spanMap: SpanMap;
|
|
85
|
+
compressedText?: never;
|
|
86
|
+
provenanceMap?: never;
|
|
87
|
+
};
|
|
88
|
+
type CompressOpts = {
|
|
89
|
+
/** Fraction of source bytes to aim to keep. Structural scaffolding is never
|
|
90
|
+
* dropped, so the achieved ratio may exceed this — read `stats.ratio`. */
|
|
91
|
+
targetRatio?: number;
|
|
92
|
+
/** Optional query. Boosts salience of nodes matching its terms, and is passed
|
|
93
|
+
* to the semantic classifier when the semantic path runs. */
|
|
94
|
+
query?: string;
|
|
95
|
+
/** Skip detection and force a handler. */
|
|
96
|
+
contentType?: ContentType | "auto";
|
|
97
|
+
};
|
|
98
|
+
type ExpandOpts = {
|
|
99
|
+
/** Recover only the run dropped by this elision, rather than the whole source. */
|
|
100
|
+
elision?: number;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Local, network-free reconstruction from the caller's own source plus a span
|
|
105
|
+
* map. This is what lets a stateless API offer recovery at all: we hold nothing,
|
|
106
|
+
* because the caller already has everything.
|
|
107
|
+
*
|
|
108
|
+
* - no opts -> the full original text, after verifying `srcSha256`.
|
|
109
|
+
* - {elision: i} -> just the bytes elision `i` dropped, recovered verbatim.
|
|
110
|
+
*
|
|
111
|
+
* Throws if the map was not built from this source, rather than returning
|
|
112
|
+
* plausible-looking misaligned output.
|
|
113
|
+
*/
|
|
114
|
+
declare function expand(source: string | Uint8Array, spanMap: SpanMap, opts?: ExpandOpts): string;
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The Vercel AI SDK surface we bind to, restated structurally.
|
|
118
|
+
*
|
|
119
|
+
* Verified 2026-07-22 against ai@7.0.34 (the copy in this workspace's pnpm store,
|
|
120
|
+
* `dist/index.js` — `createToolModelOutput` awaits `tool.toModelOutput({ toolCallId,
|
|
121
|
+
* input, output })` when building tool-result message parts) and the published docs:
|
|
122
|
+
* - `generateText(options)` takes `{ model, messages | prompt, tools, stopWhen, ... }`
|
|
123
|
+
* and resolves to `{ text, toolCalls, toolResults, steps, response, ... }`.
|
|
124
|
+
* Ref: https://ai-sdk.dev/docs/reference/ai-sdk-core/generate-text
|
|
125
|
+
* - a tool is `{ description, inputSchema, execute?, toModelOutput? }`. `inputSchema`
|
|
126
|
+
* replaced v4's `parameters`; the `tool()` helper is type-inference only and has
|
|
127
|
+
* no runtime behaviour (packages/provider-utils/src/types/tool.ts), so wrapping a
|
|
128
|
+
* plain object is equivalent to wrapping a `tool()` result.
|
|
129
|
+
* - `ToolExecuteFunction` is `(input, { toolCallId, messages, abortSignal, ... }) =>
|
|
130
|
+
* OUTPUT | PromiseLike<OUTPUT> | AsyncIterable<OUTPUT>`; the resolved value becomes
|
|
131
|
+
* the tool output.
|
|
132
|
+
* - `toModelOutput({ toolCallId, input, output }) => ToolResultOutput` maps the tool
|
|
133
|
+
* output to what the *model* sees, independently of what the app sees. That is the
|
|
134
|
+
* tool-to-model boundary this package intercepts.
|
|
135
|
+
*
|
|
136
|
+
* Restated rather than imported because `ai` is an optional peer: the package must
|
|
137
|
+
* typecheck and build with no `ai` installed, and the shapes above have been renamed
|
|
138
|
+
* across majors (parameters -> inputSchema, LanguageModelV2 -> V4) while these three
|
|
139
|
+
* fields have not.
|
|
140
|
+
*/
|
|
141
|
+
type ToolResultOutput = {
|
|
142
|
+
type: "text";
|
|
143
|
+
value: string;
|
|
144
|
+
} | {
|
|
145
|
+
type: "json";
|
|
146
|
+
value: unknown;
|
|
147
|
+
} | {
|
|
148
|
+
type: "error-text";
|
|
149
|
+
value: string;
|
|
150
|
+
} | {
|
|
151
|
+
type: "error-json";
|
|
152
|
+
value: unknown;
|
|
153
|
+
} | {
|
|
154
|
+
type: "content";
|
|
155
|
+
value: unknown[];
|
|
156
|
+
};
|
|
157
|
+
type ToolExecuteOptions = {
|
|
158
|
+
toolCallId?: string;
|
|
159
|
+
messages?: unknown;
|
|
160
|
+
abortSignal?: AbortSignal;
|
|
161
|
+
};
|
|
162
|
+
type ToolLike = {
|
|
163
|
+
description?: string;
|
|
164
|
+
inputSchema?: unknown;
|
|
165
|
+
execute?: (input: never, options: ToolExecuteOptions) => unknown;
|
|
166
|
+
toModelOutput?: (options: {
|
|
167
|
+
toolCallId: string;
|
|
168
|
+
input: unknown;
|
|
169
|
+
output: unknown;
|
|
170
|
+
}) => ToolResultOutput | PromiseLike<ToolResultOutput>;
|
|
171
|
+
};
|
|
172
|
+
type ToolSetLike = Record<string, ToolLike>;
|
|
173
|
+
type ToolResultPart = {
|
|
174
|
+
type: "tool-result";
|
|
175
|
+
toolCallId: string;
|
|
176
|
+
toolName?: string;
|
|
177
|
+
/** v5+ shape. v4 put the value on `result` instead; both are handled. */
|
|
178
|
+
output?: ToolResultOutput;
|
|
179
|
+
result?: unknown;
|
|
180
|
+
};
|
|
181
|
+
type ModelMessageLike = {
|
|
182
|
+
role: string;
|
|
183
|
+
content: unknown;
|
|
184
|
+
};
|
|
185
|
+
type GenerateTextFn = (options: Record<string, unknown>) => Promise<GenerateTextResultLike>;
|
|
186
|
+
type GenerateTextResultLike = {
|
|
187
|
+
text?: string;
|
|
188
|
+
toolCalls?: unknown;
|
|
189
|
+
toolResults?: unknown;
|
|
190
|
+
steps?: unknown;
|
|
191
|
+
response?: {
|
|
192
|
+
messages?: ModelMessageLike[];
|
|
193
|
+
};
|
|
194
|
+
};
|
|
195
|
+
type AutoExpandMode = "on-reference" | "never";
|
|
196
|
+
type TokzOptions = {
|
|
197
|
+
/** Share of source bytes to keep. Defaults to `DEFAULT_TARGET_RATIO` (0.45). */
|
|
198
|
+
targetRatio?: number;
|
|
199
|
+
/** Expand elisions the model's answer refers to, and re-prompt once. Default `never`. */
|
|
200
|
+
autoExpand?: AutoExpandMode;
|
|
201
|
+
/** Re-compress retained tool results at a tighter budget before delegating. */
|
|
202
|
+
conversationCompaction?: boolean;
|
|
203
|
+
/** Budget used when `conversationCompaction` is on. Default 0.6 * targetRatio. */
|
|
204
|
+
compactionRatio?: number;
|
|
205
|
+
/** Where originals and span maps are retained. Defaults to the shared store, so
|
|
206
|
+
* `compactHistory` and auto-expand work across turns without threading state. */
|
|
207
|
+
store?: RetentionStore;
|
|
208
|
+
/** Underlying `ai.generateText`. Injected in tests; otherwise imported from `ai`. */
|
|
209
|
+
delegate?: GenerateTextFn;
|
|
210
|
+
};
|
|
211
|
+
/**
|
|
212
|
+
* The original text plus its map, held next to the compressed render. This pair is
|
|
213
|
+
* what makes re-compaction and expansion local: nothing has to be fetched or
|
|
214
|
+
* re-inferred to produce a different view of the same tool result.
|
|
215
|
+
*/
|
|
216
|
+
type RetentionRecord = {
|
|
217
|
+
toolName: string;
|
|
218
|
+
toolCallId: string;
|
|
219
|
+
originalText: string;
|
|
220
|
+
result: CompressionResult;
|
|
221
|
+
rendered: string;
|
|
222
|
+
targetRatio: number;
|
|
223
|
+
};
|
|
224
|
+
interface RetentionStore {
|
|
225
|
+
get(toolCallId: string): RetentionRecord | undefined;
|
|
226
|
+
set(record: RetentionRecord): void;
|
|
227
|
+
records(): RetentionRecord[];
|
|
228
|
+
clear(): void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
type ReferencedElision = {
|
|
232
|
+
toolCallId: string;
|
|
233
|
+
toolName: string;
|
|
234
|
+
elisionIndex: number;
|
|
235
|
+
/** Terms from the elided bytes that appear in the model's response. */
|
|
236
|
+
matchedTerms: string[];
|
|
237
|
+
/** The elided bytes, recovered verbatim from the caller's own text. */
|
|
238
|
+
text: string;
|
|
239
|
+
note?: string;
|
|
240
|
+
};
|
|
241
|
+
type DetectOptions = {
|
|
242
|
+
/** Shortest token considered a reference. Below 4 characters the match rate is
|
|
243
|
+
* dominated by English words that happen to appear in JSON. */
|
|
244
|
+
minTermLength?: number;
|
|
245
|
+
/** Cap on terms sampled per elision, to bound work on multi-megabyte drops. */
|
|
246
|
+
maxTermsPerElision?: number;
|
|
247
|
+
};
|
|
248
|
+
/**
|
|
249
|
+
* HEURISTIC. There is no reliable signal that a model "meant" an elided field, so
|
|
250
|
+
* this matches vocabulary: recover each elided run locally, take the identifiers it
|
|
251
|
+
* contains, and flag the elision when the response mentions one of them. It over-
|
|
252
|
+
* fires on generic words (mitigated by a stopword list and a length floor) and
|
|
253
|
+
* under-fires when the model paraphrases without naming anything. Treat a hit as a
|
|
254
|
+
* cheap prompt to re-supply context, never as proof of a reference.
|
|
255
|
+
*
|
|
256
|
+
* Recovery itself is exact: `expand` verifies `srcSha256` and returns the dropped
|
|
257
|
+
* bytes verbatim, with no network call.
|
|
258
|
+
*/
|
|
259
|
+
declare function detectReferencedElisions(responseText: string, store?: RetentionStore, opts?: DetectOptions): ReferencedElision[];
|
|
260
|
+
/** A user-role message re-supplying the recovered bytes, ready to append before a
|
|
261
|
+
* follow-up turn. Separate from detection so callers can inspect first. */
|
|
262
|
+
declare function expansionMessage(expansions: readonly ReferencedElision[]): ModelMessageLike;
|
|
263
|
+
|
|
264
|
+
type TokzGenerateTextOptions = Record<string, unknown> & {
|
|
265
|
+
tokz?: TokzOptions;
|
|
266
|
+
};
|
|
267
|
+
type TokzReport = {
|
|
268
|
+
store: RetentionStore;
|
|
269
|
+
targetRatio: number;
|
|
270
|
+
/** Bytes of tool output before and after compression, this call only. */
|
|
271
|
+
originalBytes: number;
|
|
272
|
+
renderedBytes: number;
|
|
273
|
+
expansions: ReferencedElision[];
|
|
274
|
+
/** Whether a second delegate call was made to re-supply expanded content. */
|
|
275
|
+
reprompted: boolean;
|
|
276
|
+
};
|
|
277
|
+
type TokzGenerateTextResult = GenerateTextResultLike & {
|
|
278
|
+
tokz: TokzReport;
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
281
|
+
* Drop-in replacement for `ai.generateText`. Every option is forwarded untouched
|
|
282
|
+
* except `tools`, which is wrapped so results reach the model compressed, and
|
|
283
|
+
* `messages`, which is optionally compacted first.
|
|
284
|
+
*/
|
|
285
|
+
declare function generateText(options: TokzGenerateTextOptions): Promise<TokzGenerateTextResult>;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Re-compress retained tool results at a tighter budget.
|
|
289
|
+
*
|
|
290
|
+
* This is a pure local function: the original text and its span map are already in
|
|
291
|
+
* the store, so producing a smaller view of an old tool result costs zero API calls
|
|
292
|
+
* and zero inference — it is the same deterministic CPU pass that produced the first
|
|
293
|
+
* view. That property is the whole point of retaining the pair; a wrapper that threw
|
|
294
|
+
* the original away would have to re-ask a model to shrink history, which costs more
|
|
295
|
+
* than the tokens it saves.
|
|
296
|
+
*
|
|
297
|
+
* Input messages are not mutated; changed messages are returned as new objects.
|
|
298
|
+
*/
|
|
299
|
+
declare function compactHistory(messages: readonly ModelMessageLike[], targetRatio: number, store?: RetentionStore): ModelMessageLike[];
|
|
300
|
+
|
|
301
|
+
type WrapContext = {
|
|
302
|
+
store: RetentionStore;
|
|
303
|
+
targetRatio: number;
|
|
304
|
+
};
|
|
305
|
+
/** Tool output as text the compressor can work on. Pretty-printed because the
|
|
306
|
+
* structural detector reads JSON shape, and key order follows the object, so the
|
|
307
|
+
* same result always serialises to the same bytes. */
|
|
308
|
+
declare function stringifyToolResult(value: unknown): string;
|
|
309
|
+
/**
|
|
310
|
+
* Wrap one tool so the model receives the compressed render while the original text
|
|
311
|
+
* and its span map stay in the store.
|
|
312
|
+
*
|
|
313
|
+
* `execute` returns the render rather than the raw value, and `toModelOutput` maps
|
|
314
|
+
* to the same string. Doing both is deliberate: `toModelOutput` is the documented
|
|
315
|
+
* tool-to-model hook, but returning the render from `execute` means the original
|
|
316
|
+
* cannot reach a message even under an SDK version that ignores the hook. The
|
|
317
|
+
* original is one `store.get(toolCallId)` away for the caller.
|
|
318
|
+
*/
|
|
319
|
+
declare function wrapTool(toolName: string, tool: ToolLike, ctx: WrapContext): ToolLike;
|
|
320
|
+
declare function wrapTools(tools: ToolSetLike, ctx: WrapContext): ToolSetLike;
|
|
321
|
+
|
|
322
|
+
/** Insertion-ordered so `records()` reads back in call order — history compaction
|
|
323
|
+
* and any future oldest-first budget both depend on that order being stable. */
|
|
324
|
+
declare class MapRetentionStore implements RetentionStore {
|
|
325
|
+
#private;
|
|
326
|
+
get(toolCallId: string): RetentionRecord | undefined;
|
|
327
|
+
set(record: RetentionRecord): void;
|
|
328
|
+
records(): RetentionRecord[];
|
|
329
|
+
clear(): void;
|
|
330
|
+
}
|
|
331
|
+
/**
|
|
332
|
+
* Process-wide default so `compactHistory(messages, ratio)` matches the spec'd
|
|
333
|
+
* two-argument signature and still finds the originals a previous `generateText`
|
|
334
|
+
* retained. Pass an explicit store per conversation when one process serves many.
|
|
335
|
+
*/
|
|
336
|
+
declare const sharedStore: RetentionStore;
|
|
337
|
+
|
|
338
|
+
declare function compressText(text: string, opts: CompressOpts): CompressionResult;
|
|
339
|
+
/** What the model should see for a result, whichever path produced it. */
|
|
340
|
+
declare function renderResult(result: CompressionResult, source: string): string;
|
|
341
|
+
|
|
342
|
+
export { type AutoExpandMode, type DetectOptions, type GenerateTextFn, type GenerateTextResultLike, MapRetentionStore, type ModelMessageLike, type ReferencedElision, type RetentionRecord, type RetentionStore, type TokzGenerateTextOptions, type TokzGenerateTextResult, type TokzOptions, type TokzReport, type ToolLike, type ToolResultOutput, type ToolResultPart, type ToolSetLike, type WrapContext, compactHistory, compressText, detectReferencedElisions, expand, expansionMessage, generateText, renderResult, sharedStore, stringifyToolResult, wrapTool, wrapTools };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
// ../core/dist/index.js
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var enc = new TextEncoder();
|
|
4
|
+
var dec = new TextDecoder("utf-8");
|
|
5
|
+
function sha256hex(bytes) {
|
|
6
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
7
|
+
}
|
|
8
|
+
function toBytes(text) {
|
|
9
|
+
return enc.encode(text);
|
|
10
|
+
}
|
|
11
|
+
function fromBytes(bytes) {
|
|
12
|
+
return dec.decode(bytes);
|
|
13
|
+
}
|
|
14
|
+
function byteLength(text) {
|
|
15
|
+
return enc.encode(text).length;
|
|
16
|
+
}
|
|
17
|
+
function assemble(src, spans) {
|
|
18
|
+
let len = 0;
|
|
19
|
+
for (const sp of spans) len += sp.e - sp.s;
|
|
20
|
+
const out = new Uint8Array(len);
|
|
21
|
+
let o = 0;
|
|
22
|
+
for (const sp of spans) {
|
|
23
|
+
out.set(src.subarray(sp.s, sp.e), o);
|
|
24
|
+
o += sp.e - sp.s;
|
|
25
|
+
}
|
|
26
|
+
return dec.decode(out);
|
|
27
|
+
}
|
|
28
|
+
function elisionRange(spanMap, elision) {
|
|
29
|
+
const start = elision.after < 0 ? 0 : spanMap.spans[elision.after].e;
|
|
30
|
+
return { start, end: start + elision.bytes };
|
|
31
|
+
}
|
|
32
|
+
function expand(source, spanMap, opts = {}) {
|
|
33
|
+
const src = typeof source === "string" ? toBytes(source) : source;
|
|
34
|
+
if (sha256hex(src) !== spanMap.srcSha256) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
"tokz: srcSha256 mismatch \u2014 this span map was not built from the given source (was the text edited?)"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
if (opts.elision !== void 0) {
|
|
40
|
+
const el = spanMap.elisions[opts.elision];
|
|
41
|
+
if (!el) throw new Error(`tokz: no elision at index ${opts.elision}`);
|
|
42
|
+
const { start, end } = elisionRange(spanMap, el);
|
|
43
|
+
return fromBytes(src.subarray(start, end));
|
|
44
|
+
}
|
|
45
|
+
return fromBytes(src);
|
|
46
|
+
}
|
|
47
|
+
var DEFAULT_TARGET_RATIO = 0.45;
|
|
48
|
+
var MAX_PAYLOAD_BYTES = 1024 * 1024;
|
|
49
|
+
var LATENCY_BUDGET_PAYLOAD_BYTES = 64 * 1024;
|
|
50
|
+
|
|
51
|
+
// src/engine.ts
|
|
52
|
+
import * as engineModule from "@tokz/engine";
|
|
53
|
+
var engine = engineModule;
|
|
54
|
+
function compressText(text, opts) {
|
|
55
|
+
return normalize(engine.compress(text, opts));
|
|
56
|
+
}
|
|
57
|
+
function renderResult(result, source) {
|
|
58
|
+
if (typeof engine.render === "function") return engine.render(result, source);
|
|
59
|
+
if (result.method === "semantic") return result.compressedText;
|
|
60
|
+
return assemble(toBytes(source), result.spanMap.spans);
|
|
61
|
+
}
|
|
62
|
+
function normalize(raw) {
|
|
63
|
+
if ("method" in raw) return raw;
|
|
64
|
+
const { spanMap } = raw;
|
|
65
|
+
const untouched = spanMap.elisions.length === 0 && spanMap.stats.keptBytes === spanMap.srcBytes;
|
|
66
|
+
return untouched ? { method: "passthrough", spanMap } : { method: "structural", spanMap };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/store.ts
|
|
70
|
+
var MapRetentionStore = class {
|
|
71
|
+
#byId = /* @__PURE__ */ new Map();
|
|
72
|
+
get(toolCallId) {
|
|
73
|
+
return this.#byId.get(toolCallId);
|
|
74
|
+
}
|
|
75
|
+
set(record) {
|
|
76
|
+
this.#byId.set(record.toolCallId, record);
|
|
77
|
+
}
|
|
78
|
+
records() {
|
|
79
|
+
return [...this.#byId.values()];
|
|
80
|
+
}
|
|
81
|
+
clear() {
|
|
82
|
+
this.#byId.clear();
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var sharedStore = new MapRetentionStore();
|
|
86
|
+
|
|
87
|
+
// src/wrap.ts
|
|
88
|
+
function stringifyToolResult(value) {
|
|
89
|
+
if (typeof value === "string") return value;
|
|
90
|
+
return JSON.stringify(value ?? null, null, 2);
|
|
91
|
+
}
|
|
92
|
+
function wrapTool(toolName, tool, ctx) {
|
|
93
|
+
const inner = tool.execute;
|
|
94
|
+
if (typeof inner !== "function") return tool;
|
|
95
|
+
const wrapped = {
|
|
96
|
+
...tool,
|
|
97
|
+
execute: async (input, options) => {
|
|
98
|
+
const raw = await inner.call(tool, input, options);
|
|
99
|
+
if (isAsyncIterable(raw)) return raw;
|
|
100
|
+
const originalText = stringifyToolResult(raw);
|
|
101
|
+
const result = compressText(originalText, { targetRatio: ctx.targetRatio });
|
|
102
|
+
const rendered = renderResult(result, originalText);
|
|
103
|
+
const toolCallId = options.toolCallId ?? contentAddressedId(toolName, originalText);
|
|
104
|
+
const record = {
|
|
105
|
+
toolName,
|
|
106
|
+
toolCallId,
|
|
107
|
+
originalText,
|
|
108
|
+
result,
|
|
109
|
+
rendered,
|
|
110
|
+
targetRatio: ctx.targetRatio
|
|
111
|
+
};
|
|
112
|
+
ctx.store.set(record);
|
|
113
|
+
return rendered;
|
|
114
|
+
},
|
|
115
|
+
toModelOutput: ({ toolCallId, output }) => {
|
|
116
|
+
const record = ctx.store.get(toolCallId);
|
|
117
|
+
return { type: "text", value: record ? record.rendered : stringifyToolResult(output) };
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
return wrapped;
|
|
121
|
+
}
|
|
122
|
+
function wrapTools(tools, ctx) {
|
|
123
|
+
const out = {};
|
|
124
|
+
for (const name of Object.keys(tools)) {
|
|
125
|
+
const tool = tools[name];
|
|
126
|
+
if (tool) out[name] = wrapTool(name, tool, ctx);
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function resolveTargetRatio(ratio) {
|
|
131
|
+
return ratio ?? DEFAULT_TARGET_RATIO;
|
|
132
|
+
}
|
|
133
|
+
function isAsyncIterable(value) {
|
|
134
|
+
return typeof value === "object" && value !== null && typeof value[Symbol.asyncIterator] === "function";
|
|
135
|
+
}
|
|
136
|
+
function contentAddressedId(toolName, originalText) {
|
|
137
|
+
return `tokz-${sha256hex(toBytes(`${toolName}\0${originalText}`)).slice(0, 16)}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// src/compact.ts
|
|
141
|
+
function compactHistory(messages, targetRatio, store = sharedStore) {
|
|
142
|
+
return messages.map((message) => {
|
|
143
|
+
if (!Array.isArray(message.content)) return message;
|
|
144
|
+
let changed = false;
|
|
145
|
+
const content = message.content.map((part) => {
|
|
146
|
+
if (!isToolResultPart(part)) return part;
|
|
147
|
+
const record = store.get(part.toolCallId);
|
|
148
|
+
if (!record) return part;
|
|
149
|
+
const tighter = compressText(record.originalText, { targetRatio });
|
|
150
|
+
const rendered = renderResult(tighter, record.originalText);
|
|
151
|
+
if (rendered === currentValue(part)) return part;
|
|
152
|
+
store.set({ ...record, result: tighter, rendered, targetRatio });
|
|
153
|
+
changed = true;
|
|
154
|
+
return part.output !== void 0 ? { ...part, output: { type: "text", value: rendered } } : { ...part, result: rendered };
|
|
155
|
+
});
|
|
156
|
+
return changed ? { ...message, content } : message;
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function currentValue(part) {
|
|
160
|
+
if (part.output && "value" in part.output && typeof part.output.value === "string") {
|
|
161
|
+
return part.output.value;
|
|
162
|
+
}
|
|
163
|
+
if (part.result !== void 0) return stringifyToolResult(part.result);
|
|
164
|
+
return void 0;
|
|
165
|
+
}
|
|
166
|
+
function isToolResultPart(part) {
|
|
167
|
+
if (typeof part !== "object" || part === null) return false;
|
|
168
|
+
const p = part;
|
|
169
|
+
return p.type === "tool-result" && typeof p.toolCallId === "string";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// src/autoexpand.ts
|
|
173
|
+
function detectReferencedElisions(responseText, store = sharedStore, opts = {}) {
|
|
174
|
+
const minTermLength = opts.minTermLength ?? 4;
|
|
175
|
+
const maxTerms = opts.maxTermsPerElision ?? 256;
|
|
176
|
+
const haystack = responseText.toLowerCase();
|
|
177
|
+
const hits = [];
|
|
178
|
+
for (const record of store.records()) {
|
|
179
|
+
if (record.result.method === "semantic") continue;
|
|
180
|
+
const spanMap = record.result.spanMap;
|
|
181
|
+
for (let i = 0; i < spanMap.elisions.length; i++) {
|
|
182
|
+
const elision = spanMap.elisions[i];
|
|
183
|
+
if (!elision) continue;
|
|
184
|
+
const text = expand(record.originalText, spanMap, { elision: i });
|
|
185
|
+
const matchedTerms = candidateTerms(text, minTermLength, maxTerms).filter(
|
|
186
|
+
(term) => containsWord(haystack, term)
|
|
187
|
+
);
|
|
188
|
+
if (matchedTerms.length === 0) continue;
|
|
189
|
+
hits.push({
|
|
190
|
+
toolCallId: record.toolCallId,
|
|
191
|
+
toolName: record.toolName,
|
|
192
|
+
elisionIndex: i,
|
|
193
|
+
matchedTerms,
|
|
194
|
+
text,
|
|
195
|
+
...elision.note === void 0 ? {} : { note: elision.note }
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return hits;
|
|
200
|
+
}
|
|
201
|
+
function expansionMessage(expansions) {
|
|
202
|
+
const blocks = expansions.map(
|
|
203
|
+
(e) => `Recovered from the elided portion of tool result "${e.toolName}"${e.note ? ` (${e.note})` : ""}:
|
|
204
|
+
${e.text}`
|
|
205
|
+
);
|
|
206
|
+
return { role: "user", content: blocks.join("\n\n") };
|
|
207
|
+
}
|
|
208
|
+
var STOPWORDS = /* @__PURE__ */ new Set([
|
|
209
|
+
"true",
|
|
210
|
+
"false",
|
|
211
|
+
"null",
|
|
212
|
+
"http",
|
|
213
|
+
"https",
|
|
214
|
+
"this",
|
|
215
|
+
"that",
|
|
216
|
+
"with",
|
|
217
|
+
"from",
|
|
218
|
+
"have",
|
|
219
|
+
"been",
|
|
220
|
+
"were",
|
|
221
|
+
"they",
|
|
222
|
+
"there",
|
|
223
|
+
"then",
|
|
224
|
+
"than",
|
|
225
|
+
"your",
|
|
226
|
+
"when",
|
|
227
|
+
"what",
|
|
228
|
+
"which",
|
|
229
|
+
"would",
|
|
230
|
+
"could",
|
|
231
|
+
"should",
|
|
232
|
+
"about",
|
|
233
|
+
"into",
|
|
234
|
+
"more",
|
|
235
|
+
"some",
|
|
236
|
+
"only",
|
|
237
|
+
"also",
|
|
238
|
+
"each",
|
|
239
|
+
"over",
|
|
240
|
+
"just",
|
|
241
|
+
"like",
|
|
242
|
+
"them",
|
|
243
|
+
"these",
|
|
244
|
+
"those"
|
|
245
|
+
]);
|
|
246
|
+
var JSON_KEY = /"([^"\\]+)"\s*:/g;
|
|
247
|
+
var WORD = /[A-Za-z][A-Za-z0-9_.-]*[A-Za-z0-9]/g;
|
|
248
|
+
function candidateTerms(text, minLength, cap) {
|
|
249
|
+
const terms = [];
|
|
250
|
+
const seen = /* @__PURE__ */ new Set();
|
|
251
|
+
const add = (term) => {
|
|
252
|
+
if (term.length < minLength || STOPWORDS.has(term) || seen.has(term)) return;
|
|
253
|
+
seen.add(term);
|
|
254
|
+
terms.push(term);
|
|
255
|
+
};
|
|
256
|
+
const push = (raw) => {
|
|
257
|
+
add(raw.toLowerCase());
|
|
258
|
+
const spaced = raw.replace(/([a-z0-9])([A-Z])/g, "$1 $2").replace(/[_-]+/g, " ").toLowerCase();
|
|
259
|
+
if (spaced !== raw.toLowerCase()) add(spaced);
|
|
260
|
+
};
|
|
261
|
+
for (const m of text.matchAll(JSON_KEY)) {
|
|
262
|
+
if (terms.length >= cap) return terms;
|
|
263
|
+
if (m[1]) push(m[1]);
|
|
264
|
+
}
|
|
265
|
+
for (const m of text.matchAll(WORD)) {
|
|
266
|
+
if (terms.length >= cap) return terms;
|
|
267
|
+
push(m[0]);
|
|
268
|
+
}
|
|
269
|
+
return terms;
|
|
270
|
+
}
|
|
271
|
+
function containsWord(haystack, term) {
|
|
272
|
+
let from = 0;
|
|
273
|
+
for (; ; ) {
|
|
274
|
+
const at = haystack.indexOf(term, from);
|
|
275
|
+
if (at === -1) return false;
|
|
276
|
+
const before = at === 0 ? "" : haystack[at - 1];
|
|
277
|
+
const after = haystack[at + term.length] ?? "";
|
|
278
|
+
if (!isWordChar(before) && !isWordChar(after)) return true;
|
|
279
|
+
from = at + 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
function isWordChar(ch) {
|
|
283
|
+
return ch !== "" && /[A-Za-z0-9_]/.test(ch);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/generate.ts
|
|
287
|
+
async function generateText(options) {
|
|
288
|
+
const { tokz = {}, ...rest } = options;
|
|
289
|
+
const store = tokz.store ?? sharedStore;
|
|
290
|
+
const targetRatio = resolveTargetRatio(tokz.targetRatio);
|
|
291
|
+
const delegate = tokz.delegate ?? await loadDelegate();
|
|
292
|
+
const forwarded = { ...rest };
|
|
293
|
+
if (isToolSet(rest["tools"])) {
|
|
294
|
+
forwarded["tools"] = wrapTools(rest["tools"], { store, targetRatio });
|
|
295
|
+
}
|
|
296
|
+
if (tokz.conversationCompaction && Array.isArray(rest["messages"])) {
|
|
297
|
+
const ratio = tokz.compactionRatio ?? targetRatio * 0.6;
|
|
298
|
+
forwarded["messages"] = compactHistory(rest["messages"], ratio, store);
|
|
299
|
+
}
|
|
300
|
+
const before = snapshotBytes(store);
|
|
301
|
+
let result = await delegate(forwarded);
|
|
302
|
+
const usage = deltaBytes(store, before);
|
|
303
|
+
let expansions = [];
|
|
304
|
+
let reprompted = false;
|
|
305
|
+
if (tokz.autoExpand === "on-reference" && typeof result.text === "string") {
|
|
306
|
+
expansions = detectReferencedElisions(result.text, store);
|
|
307
|
+
if (expansions.length > 0 && Array.isArray(forwarded["messages"])) {
|
|
308
|
+
const messages = [
|
|
309
|
+
...forwarded["messages"],
|
|
310
|
+
{ role: "assistant", content: result.text },
|
|
311
|
+
expansionMessage(expansions)
|
|
312
|
+
];
|
|
313
|
+
result = await delegate({ ...forwarded, messages });
|
|
314
|
+
reprompted = true;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
...result,
|
|
319
|
+
tokz: {
|
|
320
|
+
store,
|
|
321
|
+
targetRatio,
|
|
322
|
+
originalBytes: usage.original,
|
|
323
|
+
renderedBytes: usage.rendered,
|
|
324
|
+
expansions,
|
|
325
|
+
reprompted
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
async function loadDelegate() {
|
|
330
|
+
const specifier = "ai";
|
|
331
|
+
const mod = await import(specifier);
|
|
332
|
+
if (typeof mod.generateText !== "function") {
|
|
333
|
+
throw new Error("tokz: the `ai` package is required unless `tokz.delegate` is supplied");
|
|
334
|
+
}
|
|
335
|
+
return mod.generateText;
|
|
336
|
+
}
|
|
337
|
+
function isToolSet(value) {
|
|
338
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
339
|
+
}
|
|
340
|
+
function snapshotBytes(store) {
|
|
341
|
+
return new Set(store.records().map((r) => r.toolCallId));
|
|
342
|
+
}
|
|
343
|
+
function deltaBytes(store, before) {
|
|
344
|
+
let original = 0;
|
|
345
|
+
let rendered = 0;
|
|
346
|
+
for (const record of store.records()) {
|
|
347
|
+
if (before.has(record.toolCallId)) continue;
|
|
348
|
+
original += byteLength(record.originalText);
|
|
349
|
+
rendered += byteLength(record.rendered);
|
|
350
|
+
}
|
|
351
|
+
return { original, rendered };
|
|
352
|
+
}
|
|
353
|
+
export {
|
|
354
|
+
MapRetentionStore,
|
|
355
|
+
compactHistory,
|
|
356
|
+
compressText,
|
|
357
|
+
detectReferencedElisions,
|
|
358
|
+
expand,
|
|
359
|
+
expansionMessage,
|
|
360
|
+
generateText,
|
|
361
|
+
renderResult,
|
|
362
|
+
sharedStore,
|
|
363
|
+
stringifyToolResult,
|
|
364
|
+
wrapTool,
|
|
365
|
+
wrapTools
|
|
366
|
+
};
|
|
367
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../core/src/bytes.ts","../../core/src/expand.ts","../../core/src/constants.ts","../src/engine.ts","../src/store.ts","../src/wrap.ts","../src/compact.ts","../src/autoexpand.ts","../src/generate.ts"],"sourcesContent":["import { createHash } from \"node:crypto\";\nimport type { Span, SpanMap, Elision } from \"./types.js\";\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder(\"utf-8\");\n\nexport function sha256hex(bytes: Uint8Array): string {\n return createHash(\"sha256\").update(bytes).digest(\"hex\");\n}\n\nexport function toBytes(text: string): Uint8Array {\n return enc.encode(text);\n}\n\nexport function fromBytes(bytes: Uint8Array): string {\n return dec.decode(bytes);\n}\n\nexport function byteLength(text: string): number {\n return enc.encode(text).length;\n}\n\nexport function isAscii(text: string): boolean {\n for (let i = 0; i < text.length; i++) {\n if (text.charCodeAt(i) > 127) return false;\n }\n return true;\n}\n\n/**\n * Concatenate the source bytes covered by `spans` and decode as UTF-8. This is\n * exactly the compressed text: only surviving bytes, in source order, verbatim.\n */\nexport function assemble(src: Uint8Array, spans: Span[]): string {\n let len = 0;\n for (const sp of spans) len += sp.e - sp.s;\n const out = new Uint8Array(len);\n let o = 0;\n for (const sp of spans) {\n out.set(src.subarray(sp.s, sp.e), o);\n o += sp.e - sp.s;\n }\n return dec.decode(out);\n}\n\n/** Byte range in the source that an elision dropped. */\nexport function elisionRange(spanMap: SpanMap, elision: Elision): { start: number; end: number } {\n const start = elision.after < 0 ? 0 : spanMap.spans[elision.after]!.e;\n return { start, end: start + elision.bytes };\n}\n\n/**\n * jsonc-parser and most tokenizers report offsets in UTF-16 code units; the span\n * map speaks UTF-8 bytes. Build a converter from string index to byte offset.\n *\n * For ASCII — which most tool output is — the two are the same number, so the\n * prefix table is skipped entirely rather than allocating 4 bytes per character\n * to answer `i => i`.\n */\nexport function byteMapper(text: string, ascii?: boolean): (strIdx: number) => number {\n if (ascii ?? isAscii(text)) {\n const len = text.length;\n return (strIdx: number) => Math.max(0, Math.min(strIdx, len));\n }\n\n const prefix = new Int32Array(text.length + 1);\n let bytes = 0;\n let i = 0;\n const buf = new Uint8Array(4);\n for (const cp of text) {\n const units = cp.length; // 1 or 2 UTF-16 units\n const n = enc.encodeInto(cp, buf).written ?? 0;\n // Every UTF-16 index inside a code point maps to the byte offset at its start.\n for (let u = 0; u < units; u++) {\n prefix[i] = bytes;\n i++;\n }\n bytes += n;\n }\n prefix[i] = bytes;\n return (strIdx: number) => prefix[Math.max(0, Math.min(strIdx, text.length))]!;\n}\n","import type { SpanMap, ExpandOpts } from \"./types.js\";\nimport { sha256hex, toBytes, fromBytes, elisionRange } from \"./bytes.js\";\n\n/**\n * Local, network-free reconstruction from the caller's own source plus a span\n * map. This is what lets a stateless API offer recovery at all: we hold nothing,\n * because the caller already has everything.\n *\n * - no opts -> the full original text, after verifying `srcSha256`.\n * - {elision: i} -> just the bytes elision `i` dropped, recovered verbatim.\n *\n * Throws if the map was not built from this source, rather than returning\n * plausible-looking misaligned output.\n */\nexport function expand(source: string | Uint8Array, spanMap: SpanMap, opts: ExpandOpts = {}): string {\n const src = typeof source === \"string\" ? toBytes(source) : source;\n if (sha256hex(src) !== spanMap.srcSha256) {\n throw new Error(\n \"tokz: srcSha256 mismatch — this span map was not built from the given source (was the text edited?)\",\n );\n }\n if (opts.elision !== undefined) {\n const el = spanMap.elisions[opts.elision];\n if (!el) throw new Error(`tokz: no elision at index ${opts.elision}`);\n const { start, end } = elisionRange(spanMap, el);\n return fromBytes(src.subarray(start, end));\n }\n return fromBytes(src);\n}\n","/** Default share of source bytes to keep when the caller does not say. */\nexport const DEFAULT_TARGET_RATIO = 0.45;\n\n/** Max request body the hosted API accepts. Compression cost is linear in input\n * size, so this is where predictable latency stops, not an arbitrary number. */\nexport const MAX_PAYLOAD_BYTES = 1024 * 1024;\n\n/**\n * Nesting past this is declined rather than compressed. Parsers and the\n * selection walk both recurse per level, so a deeply nested document overflows\n * the stack — and `\"[\".repeat(6000)` reaches that in ~12 KB, well inside any\n * request body limit. Declining costs the caller nothing; throwing would hand a\n * remote caller a way to kill a request slot.\n */\nexport const MAX_NESTING_DEPTH = 200;\n\n/** Latency budgets from the design doc, used by the eval kill gates. */\nexport const STRUCTURAL_BUDGET_MS = 10;\nexport const SEMANTIC_BUDGET_MS = 30;\n\n/** Payload size the structural latency budget is quoted against. Cost is linear\n * above this, so a latency number without a size attached means little. */\nexport const LATENCY_BUDGET_PAYLOAD_BYTES = 64 * 1024;\n\n/** API key format: `tokz_live_<id8>_<secret>`. The id is public and indexed; only\n * a SHA-256 of the secret is stored. */\nexport const KEY_PREFIX = \"tokz_live_\";\n","import * as engineModule from \"@tokz/engine\";\nimport { assemble, toBytes, type CompressionResult, type CompressOpts, type SpanMap } from \"@tokz/core\";\n\n/**\n * `@tokz/engine` is mid-migration from `compress() -> { text, spanMap }` to the\n * unified `CompressionResult` union, and `render` lands with it. Binding through a\n * declared surface with one cast keeps this package compiling and running against\n * either shape instead of pinning it to whichever version happens to be built in\n * `dist/` at the moment.\n */\ntype EngineSurface = {\n compress: (text: string, opts?: CompressOpts) => CompressionResult | LegacyCompressResult;\n render?: (result: CompressionResult, source: string) => string;\n};\n\ntype LegacyCompressResult = { text: string; spanMap: SpanMap };\n\nconst engine = engineModule as unknown as EngineSurface;\n\nexport function compressText(text: string, opts: CompressOpts): CompressionResult {\n return normalize(engine.compress(text, opts));\n}\n\n/** What the model should see for a result, whichever path produced it. */\nexport function renderResult(result: CompressionResult, source: string): string {\n if (typeof engine.render === \"function\") return engine.render(result, source);\n if (result.method === \"semantic\") return result.compressedText;\n return assemble(toBytes(source), result.spanMap.spans);\n}\n\nfunction normalize(raw: CompressionResult | LegacyCompressResult): CompressionResult {\n if (\"method\" in raw) return raw;\n const { spanMap } = raw;\n // A map that kept everything and dropped nothing is the honest passthrough case,\n // which the union spells out and the legacy shape did not.\n const untouched = spanMap.elisions.length === 0 && spanMap.stats.keptBytes === spanMap.srcBytes;\n return untouched ? { method: \"passthrough\", spanMap } : { method: \"structural\", spanMap };\n}\n","import type { RetentionRecord, RetentionStore } from \"./types.js\";\n\n/** Insertion-ordered so `records()` reads back in call order — history compaction\n * and any future oldest-first budget both depend on that order being stable. */\nexport class MapRetentionStore implements RetentionStore {\n readonly #byId = new Map<string, RetentionRecord>();\n\n get(toolCallId: string): RetentionRecord | undefined {\n return this.#byId.get(toolCallId);\n }\n\n set(record: RetentionRecord): void {\n this.#byId.set(record.toolCallId, record);\n }\n\n records(): RetentionRecord[] {\n return [...this.#byId.values()];\n }\n\n clear(): void {\n this.#byId.clear();\n }\n}\n\n/**\n * Process-wide default so `compactHistory(messages, ratio)` matches the spec'd\n * two-argument signature and still finds the originals a previous `generateText`\n * retained. Pass an explicit store per conversation when one process serves many.\n */\nexport const sharedStore: RetentionStore = new MapRetentionStore();\n","import { DEFAULT_TARGET_RATIO, sha256hex, toBytes } from \"@tokz/core\";\nimport { compressText, renderResult } from \"./engine.js\";\nimport type {\n RetentionRecord,\n RetentionStore,\n ToolExecuteOptions,\n ToolLike,\n ToolSetLike,\n} from \"./types.js\";\n\nexport type WrapContext = {\n store: RetentionStore;\n targetRatio: number;\n};\n\n/** Tool output as text the compressor can work on. Pretty-printed because the\n * structural detector reads JSON shape, and key order follows the object, so the\n * same result always serialises to the same bytes. */\nexport function stringifyToolResult(value: unknown): string {\n if (typeof value === \"string\") return value;\n return JSON.stringify(value ?? null, null, 2);\n}\n\n/**\n * Wrap one tool so the model receives the compressed render while the original text\n * and its span map stay in the store.\n *\n * `execute` returns the render rather than the raw value, and `toModelOutput` maps\n * to the same string. Doing both is deliberate: `toModelOutput` is the documented\n * tool-to-model hook, but returning the render from `execute` means the original\n * cannot reach a message even under an SDK version that ignores the hook. The\n * original is one `store.get(toolCallId)` away for the caller.\n */\nexport function wrapTool(toolName: string, tool: ToolLike, ctx: WrapContext): ToolLike {\n const inner = tool.execute;\n // Client-side and provider-executed tools have no server-side execute to intercept.\n if (typeof inner !== \"function\") return tool;\n\n const wrapped: ToolLike = {\n ...tool,\n execute: async (input: never, options: ToolExecuteOptions): Promise<unknown> => {\n const raw = await inner.call(tool, input, options);\n // A tool may return an AsyncIterable to stream its output. Compressing that\n // would mean buffering it, which changes the tool's semantics, so it passes\n // through uncompressed rather than silently becoming \"{}\".\n if (isAsyncIterable(raw)) return raw;\n const originalText = stringifyToolResult(raw);\n const result = compressText(originalText, { targetRatio: ctx.targetRatio });\n const rendered = renderResult(result, originalText);\n const toolCallId = options.toolCallId ?? contentAddressedId(toolName, originalText);\n const record: RetentionRecord = {\n toolName,\n toolCallId,\n originalText,\n result,\n rendered,\n targetRatio: ctx.targetRatio,\n };\n ctx.store.set(record);\n return rendered;\n },\n toModelOutput: ({ toolCallId, output }) => {\n const record = ctx.store.get(toolCallId);\n return { type: \"text\", value: record ? record.rendered : stringifyToolResult(output) };\n },\n };\n return wrapped;\n}\n\nexport function wrapTools(tools: ToolSetLike, ctx: WrapContext): ToolSetLike {\n const out: ToolSetLike = {};\n for (const name of Object.keys(tools)) {\n const tool = tools[name];\n if (tool) out[name] = wrapTool(name, tool, ctx);\n }\n return out;\n}\n\nexport function resolveTargetRatio(ratio: number | undefined): number {\n return ratio ?? DEFAULT_TARGET_RATIO;\n}\n\nfunction isAsyncIterable(value: unknown): value is AsyncIterable<unknown> {\n return (\n typeof value === \"object\" &&\n value !== null &&\n typeof (value as AsyncIterable<unknown>)[Symbol.asyncIterator] === \"function\"\n );\n}\n\n/** Content-addressed, not counter- or clock-based: two runs of the same tool call\n * must key the same record, or a customer's prompt cache stops hitting. */\nfunction contentAddressedId(toolName: string, originalText: string): string {\n return `tokz-${sha256hex(toBytes(`${toolName}\u0000${originalText}`)).slice(0, 16)}`;\n}\n","import { compressText, renderResult } from \"./engine.js\";\nimport { sharedStore } from \"./store.js\";\nimport { stringifyToolResult } from \"./wrap.js\";\nimport type { ModelMessageLike, RetentionStore, ToolResultPart } from \"./types.js\";\n\n/**\n * Re-compress retained tool results at a tighter budget.\n *\n * This is a pure local function: the original text and its span map are already in\n * the store, so producing a smaller view of an old tool result costs zero API calls\n * and zero inference — it is the same deterministic CPU pass that produced the first\n * view. That property is the whole point of retaining the pair; a wrapper that threw\n * the original away would have to re-ask a model to shrink history, which costs more\n * than the tokens it saves.\n *\n * Input messages are not mutated; changed messages are returned as new objects.\n */\nexport function compactHistory(\n messages: readonly ModelMessageLike[],\n targetRatio: number,\n store: RetentionStore = sharedStore,\n): ModelMessageLike[] {\n return messages.map((message) => {\n if (!Array.isArray(message.content)) return message;\n\n let changed = false;\n const content = (message.content as unknown[]).map((part) => {\n if (!isToolResultPart(part)) return part;\n const record = store.get(part.toolCallId);\n if (!record) return part;\n\n const tighter = compressText(record.originalText, { targetRatio });\n const rendered = renderResult(tighter, record.originalText);\n if (rendered === currentValue(part)) return part;\n\n store.set({ ...record, result: tighter, rendered, targetRatio });\n changed = true;\n return part.output !== undefined\n ? { ...part, output: { type: \"text\" as const, value: rendered } }\n : { ...part, result: rendered };\n });\n\n return changed ? { ...message, content } : message;\n });\n}\n\nfunction currentValue(part: ToolResultPart): string | undefined {\n if (part.output && \"value\" in part.output && typeof part.output.value === \"string\") {\n return part.output.value;\n }\n if (part.result !== undefined) return stringifyToolResult(part.result);\n return undefined;\n}\n\nfunction isToolResultPart(part: unknown): part is ToolResultPart {\n if (typeof part !== \"object\" || part === null) return false;\n const p = part as { type?: unknown; toolCallId?: unknown };\n return p.type === \"tool-result\" && typeof p.toolCallId === \"string\";\n}\n","import { expand } from \"@tokz/core\";\nimport { sharedStore } from \"./store.js\";\nimport type { ModelMessageLike, RetentionStore } from \"./types.js\";\n\nexport type ReferencedElision = {\n toolCallId: string;\n toolName: string;\n elisionIndex: number;\n /** Terms from the elided bytes that appear in the model's response. */\n matchedTerms: string[];\n /** The elided bytes, recovered verbatim from the caller's own text. */\n text: string;\n note?: string;\n};\n\nexport type DetectOptions = {\n /** Shortest token considered a reference. Below 4 characters the match rate is\n * dominated by English words that happen to appear in JSON. */\n minTermLength?: number;\n /** Cap on terms sampled per elision, to bound work on multi-megabyte drops. */\n maxTermsPerElision?: number;\n};\n\n/**\n * HEURISTIC. There is no reliable signal that a model \"meant\" an elided field, so\n * this matches vocabulary: recover each elided run locally, take the identifiers it\n * contains, and flag the elision when the response mentions one of them. It over-\n * fires on generic words (mitigated by a stopword list and a length floor) and\n * under-fires when the model paraphrases without naming anything. Treat a hit as a\n * cheap prompt to re-supply context, never as proof of a reference.\n *\n * Recovery itself is exact: `expand` verifies `srcSha256` and returns the dropped\n * bytes verbatim, with no network call.\n */\nexport function detectReferencedElisions(\n responseText: string,\n store: RetentionStore = sharedStore,\n opts: DetectOptions = {},\n): ReferencedElision[] {\n const minTermLength = opts.minTermLength ?? 4;\n const maxTerms = opts.maxTermsPerElision ?? 256;\n const haystack = responseText.toLowerCase();\n const hits: ReferencedElision[] = [];\n\n for (const record of store.records()) {\n // Semantic results carry a provenance map, not recoverable byte ranges.\n if (record.result.method === \"semantic\") continue;\n const spanMap = record.result.spanMap;\n\n for (let i = 0; i < spanMap.elisions.length; i++) {\n const elision = spanMap.elisions[i];\n if (!elision) continue;\n const text = expand(record.originalText, spanMap, { elision: i });\n const matchedTerms = candidateTerms(text, minTermLength, maxTerms).filter((term) =>\n containsWord(haystack, term),\n );\n if (matchedTerms.length === 0) continue;\n hits.push({\n toolCallId: record.toolCallId,\n toolName: record.toolName,\n elisionIndex: i,\n matchedTerms,\n text,\n ...(elision.note === undefined ? {} : { note: elision.note }),\n });\n }\n }\n return hits;\n}\n\n/** A user-role message re-supplying the recovered bytes, ready to append before a\n * follow-up turn. Separate from detection so callers can inspect first. */\nexport function expansionMessage(expansions: readonly ReferencedElision[]): ModelMessageLike {\n const blocks = expansions.map(\n (e) =>\n `Recovered from the elided portion of tool result \"${e.toolName}\"` +\n `${e.note ? ` (${e.note})` : \"\"}:\\n${e.text}`,\n );\n return { role: \"user\", content: blocks.join(\"\\n\\n\") };\n}\n\nconst STOPWORDS = new Set([\n \"true\",\n \"false\",\n \"null\",\n \"http\",\n \"https\",\n \"this\",\n \"that\",\n \"with\",\n \"from\",\n \"have\",\n \"been\",\n \"were\",\n \"they\",\n \"there\",\n \"then\",\n \"than\",\n \"your\",\n \"when\",\n \"what\",\n \"which\",\n \"would\",\n \"could\",\n \"should\",\n \"about\",\n \"into\",\n \"more\",\n \"some\",\n \"only\",\n \"also\",\n \"each\",\n \"over\",\n \"just\",\n \"like\",\n \"them\",\n \"these\",\n \"those\",\n]);\n\nconst JSON_KEY = /\"([^\"\\\\]+)\"\\s*:/g;\n/** Hyphens and dots are inside the token, not between tokens: the identifiers worth\n * matching are `api-5`, `node-2`, `registry.example.com`, not their fragments. */\nconst WORD = /[A-Za-z][A-Za-z0-9_.-]*[A-Za-z0-9]/g;\n\nfunction candidateTerms(text: string, minLength: number, cap: number): string[] {\n const terms: string[] = [];\n const seen = new Set<string>();\n const add = (term: string): void => {\n if (term.length < minLength || STOPWORDS.has(term) || seen.has(term)) return;\n seen.add(term);\n terms.push(term);\n };\n const push = (raw: string): void => {\n add(raw.toLowerCase());\n // A model asked about `restartCount` usually writes \"restart count\", so the\n // separated spelling is a term in its own right.\n const spaced = raw.replace(/([a-z0-9])([A-Z])/g, \"$1 $2\").replace(/[_-]+/g, \" \").toLowerCase();\n if (spaced !== raw.toLowerCase()) add(spaced);\n };\n\n // Keys first: a model asking about elided data almost always names the field.\n for (const m of text.matchAll(JSON_KEY)) {\n if (terms.length >= cap) return terms;\n if (m[1]) push(m[1]);\n }\n for (const m of text.matchAll(WORD)) {\n if (terms.length >= cap) return terms;\n push(m[0]);\n }\n return terms;\n}\n\nfunction containsWord(haystack: string, term: string): boolean {\n let from = 0;\n for (;;) {\n const at = haystack.indexOf(term, from);\n if (at === -1) return false;\n const before = at === 0 ? \"\" : haystack[at - 1]!;\n const after = haystack[at + term.length] ?? \"\";\n if (!isWordChar(before) && !isWordChar(after)) return true;\n from = at + 1;\n }\n}\n\nfunction isWordChar(ch: string): boolean {\n return ch !== \"\" && /[A-Za-z0-9_]/.test(ch);\n}\n","import { byteLength } from \"@tokz/core\";\nimport { compactHistory } from \"./compact.js\";\nimport { detectReferencedElisions, expansionMessage, type ReferencedElision } from \"./autoexpand.js\";\nimport { sharedStore } from \"./store.js\";\nimport { resolveTargetRatio, wrapTools } from \"./wrap.js\";\nimport type {\n GenerateTextFn,\n GenerateTextResultLike,\n ModelMessageLike,\n RetentionStore,\n TokzOptions,\n ToolSetLike,\n} from \"./types.js\";\n\nexport type TokzGenerateTextOptions = Record<string, unknown> & { tokz?: TokzOptions };\n\nexport type TokzReport = {\n store: RetentionStore;\n targetRatio: number;\n /** Bytes of tool output before and after compression, this call only. */\n originalBytes: number;\n renderedBytes: number;\n expansions: ReferencedElision[];\n /** Whether a second delegate call was made to re-supply expanded content. */\n reprompted: boolean;\n};\n\nexport type TokzGenerateTextResult = GenerateTextResultLike & { tokz: TokzReport };\n\n/**\n * Drop-in replacement for `ai.generateText`. Every option is forwarded untouched\n * except `tools`, which is wrapped so results reach the model compressed, and\n * `messages`, which is optionally compacted first.\n */\nexport async function generateText(\n options: TokzGenerateTextOptions,\n): Promise<TokzGenerateTextResult> {\n const { tokz = {}, ...rest } = options;\n const store = tokz.store ?? sharedStore;\n const targetRatio = resolveTargetRatio(tokz.targetRatio);\n const delegate = tokz.delegate ?? (await loadDelegate());\n\n const forwarded: Record<string, unknown> = { ...rest };\n\n if (isToolSet(rest[\"tools\"])) {\n forwarded[\"tools\"] = wrapTools(rest[\"tools\"], { store, targetRatio });\n }\n\n if (tokz.conversationCompaction && Array.isArray(rest[\"messages\"])) {\n const ratio = tokz.compactionRatio ?? targetRatio * 0.6;\n forwarded[\"messages\"] = compactHistory(rest[\"messages\"] as ModelMessageLike[], ratio, store);\n }\n\n const before = snapshotBytes(store);\n let result = await delegate(forwarded);\n const usage = deltaBytes(store, before);\n\n let expansions: ReferencedElision[] = [];\n let reprompted = false;\n if (tokz.autoExpand === \"on-reference\" && typeof result.text === \"string\") {\n expansions = detectReferencedElisions(result.text, store);\n if (expansions.length > 0 && Array.isArray(forwarded[\"messages\"])) {\n // One re-prompt only. The expansion is local and free, but the extra turn is\n // not, and a loop that re-expands on its own answer never terminates.\n const messages = [\n ...(forwarded[\"messages\"] as ModelMessageLike[]),\n { role: \"assistant\", content: result.text },\n expansionMessage(expansions),\n ];\n result = await delegate({ ...forwarded, messages });\n reprompted = true;\n }\n }\n\n return {\n ...result,\n tokz: {\n store,\n targetRatio,\n originalBytes: usage.original,\n renderedBytes: usage.rendered,\n expansions,\n reprompted,\n },\n };\n}\n\n/**\n * `ai` is an optional peer, so the specifier is held in a variable: a literal would\n * make TypeScript resolve the module at build time and fail in a workspace that has\n * not installed it, which is exactly the install this package must still compile in.\n */\nasync function loadDelegate(): Promise<GenerateTextFn> {\n const specifier = \"ai\";\n const mod = (await import(specifier)) as { generateText?: GenerateTextFn };\n if (typeof mod.generateText !== \"function\") {\n throw new Error(\"tokz: the `ai` package is required unless `tokz.delegate` is supplied\");\n }\n return mod.generateText;\n}\n\nfunction isToolSet(value: unknown): value is ToolSetLike {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction snapshotBytes(store: RetentionStore): Set<string> {\n return new Set(store.records().map((r) => r.toolCallId));\n}\n\nfunction deltaBytes(\n store: RetentionStore,\n before: Set<string>,\n): { original: number; rendered: number } {\n let original = 0;\n let rendered = 0;\n for (const record of store.records()) {\n if (before.has(record.toolCallId)) continue;\n original += byteLength(record.originalText);\n rendered += byteLength(record.rendered);\n }\n return { original, rendered };\n}\n"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tokz/ai-sdk",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Vercel AI SDK wrapper for tokz: compresses tool results in place, compacts history, and expands on reference.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"homepage": "https://tokz.dev",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"tokz",
|
|
10
|
+
"ai-sdk",
|
|
11
|
+
"vercel-ai",
|
|
12
|
+
"prompt-compression",
|
|
13
|
+
"context-window",
|
|
14
|
+
"tool-results",
|
|
15
|
+
"agents"
|
|
16
|
+
],
|
|
17
|
+
"main": "./dist/index.js",
|
|
18
|
+
"types": "./dist/index.d.ts",
|
|
19
|
+
"exports": {
|
|
20
|
+
".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"dist"
|
|
24
|
+
],
|
|
25
|
+
"sideEffects": false,
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"publishConfig": {
|
|
30
|
+
"access": "public"
|
|
31
|
+
},
|
|
32
|
+
"scripts": {
|
|
33
|
+
"build": "tsup",
|
|
34
|
+
"test": "vitest run",
|
|
35
|
+
"prepublishOnly": "pnpm build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@tokz/engine": "workspace:*"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"ai": ">=5"
|
|
42
|
+
},
|
|
43
|
+
"peerDependenciesMeta": {
|
|
44
|
+
"ai": { "optional": true }
|
|
45
|
+
},
|
|
46
|
+
"devDependencies": {
|
|
47
|
+
"@tokz/core": "workspace:*",
|
|
48
|
+
"tsup": "^8.3.5",
|
|
49
|
+
"typescript": "^5.7.2",
|
|
50
|
+
"vitest": "^2.1.8"
|
|
51
|
+
}
|
|
52
|
+
}
|