@jigs.design/ai-sdk 0.0.0-stage → 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 +44 -2
- package/dist/chat.d.ts +58 -0
- package/dist/chat.js +223 -0
- package/dist/client.d.ts +2 -0
- package/dist/client.js +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/model.d.ts +21 -0
- package/dist/model.js +53 -0
- package/dist/schemas.d.ts +66 -0
- package/dist/schemas.js +51 -0
- package/dist/stored.d.ts +58 -0
- package/dist/stored.js +50 -0
- package/package.json +43 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tom Bates
|
|
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
CHANGED
|
@@ -1,3 +1,45 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @jigs.design/ai-sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Optional. Connects Pickle to the [AI SDK](https://ai-sdk.dev) in two ways:
|
|
4
|
+
|
|
5
|
+
- `aiSdkModel(model, settings?)` is a `model` for `createPickleServer`. It passes the host's prompt as `instructions`, streams the text through `streamText`, and reports token usage to `onTurn`. It takes a gateway id such as `"anthropic/claude-sonnet-5.5"` or any provider's `LanguageModel`.
|
|
6
|
+
- `pickleChat(pickle, { request, messages })` runs chat mode on a `useChat` conversation. The assistant answers in Markdown, and each fenced `pickle` block becomes a `data-pickle` part you draw with `PickleView`.
|
|
7
|
+
|
|
8
|
+
```ts
|
|
9
|
+
import { aiSdkModel } from "@jigs.design/ai-sdk";
|
|
10
|
+
import { createPickleServer } from "@jigs.design/server";
|
|
11
|
+
|
|
12
|
+
export const pickle = createPickleServer({
|
|
13
|
+
host,
|
|
14
|
+
context,
|
|
15
|
+
run,
|
|
16
|
+
model: aiSdkModel("anthropic/claude-sonnet-5.5", { temperature: 0.2 }),
|
|
17
|
+
});
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Import the root entry from server code. The browser imports `@jigs.design/ai-sdk/client`: `withoutPrograms`, `recentMessages` and the message types, with no server code and no Zod behind them.
|
|
21
|
+
|
|
22
|
+
## Exports
|
|
23
|
+
|
|
24
|
+
| Export | What it is |
|
|
25
|
+
| -------------------------------------------- | -------------------------------------------------------------------------------- |
|
|
26
|
+
| `aiSdkModel`, `ModelSettings` | The model function and the `streamText` settings it applies to every call |
|
|
27
|
+
| `pickleChat` | One chat turn: the system prompt, the history converter and the stream transform |
|
|
28
|
+
| `pickleDataSchemas` | Schemas for `validateUIMessages`. They drop each pickle's `program` |
|
|
29
|
+
| `revivePickles` | Compiles a stored conversation's pickles again, typed from the messages passed |
|
|
30
|
+
| `withoutPrograms` | Strips programs from messages, for the request body and for storage |
|
|
31
|
+
| `recentMessages` | The recent turns of a long conversation, after every earlier pickle |
|
|
32
|
+
| `PickleUIMessage`, `PickleBlockData` | Types for `useChat` and for one `data-pickle` part, generic over your metadata |
|
|
33
|
+
| `StoredPickleUIMessage`, `StoredPickleBlock` | The same without programs: what the server receives and what you store |
|
|
34
|
+
| `@jigs.design/ai-sdk/client` | The browser entry: `withoutPrograms`, `recentMessages` and the types |
|
|
35
|
+
|
|
36
|
+
## Source
|
|
37
|
+
|
|
38
|
+
- `src/model.ts`: `aiSdkModel`.
|
|
39
|
+
- `src/chat.ts`: chat mode on `useChat`.
|
|
40
|
+
- `src/stored.ts`, `src/client.ts`: the message types, `withoutPrograms`, `recentMessages`, and the browser entry. No Zod here.
|
|
41
|
+
- `src/schemas.ts`: `pickleDataSchemas`, server only.
|
|
42
|
+
|
|
43
|
+
## Docs
|
|
44
|
+
|
|
45
|
+
[AI SDK](../../apps/docs/docs/running/ai-sdk.mdx) and [Chat](../../apps/docs/docs/running/chat.mdx). [`examples/with-ai-chat`](../../examples/with-ai-chat) runs both.
|
package/dist/chat.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { PickleBlockData, PickleUIMessage, WithPickleBlocks } from "./stored.js";
|
|
2
|
+
import { ChatTurn, PickleServer } from "@jigs.design/server";
|
|
3
|
+
import { InferUIMessageChunk, UIMessage, UIMessageChunk } from "ai";
|
|
4
|
+
//#region src/chat.d.ts
|
|
5
|
+
export interface PickleChat {
|
|
6
|
+
/**
|
|
7
|
+
* For `convertToModelMessages(messages, { convertDataPart })`: writes each
|
|
8
|
+
* pickle back as the fenced block the model wrote, so it sees its UIs.
|
|
9
|
+
* Returns `undefined` for other data parts; chain your own after it.
|
|
10
|
+
*/
|
|
11
|
+
convertDataPart: (part: {
|
|
12
|
+
data: unknown;
|
|
13
|
+
type: string;
|
|
14
|
+
}) => {
|
|
15
|
+
text: string;
|
|
16
|
+
type: "text";
|
|
17
|
+
} | undefined;
|
|
18
|
+
/** The chat-mode prompt for the server's host. Add your instructions to it. */
|
|
19
|
+
system: string;
|
|
20
|
+
/**
|
|
21
|
+
* The start of `system` that is the same on every turn. Pass both to
|
|
22
|
+
* `cachedInstructions` so the provider caches it.
|
|
23
|
+
*/
|
|
24
|
+
systemPrefix: string;
|
|
25
|
+
/**
|
|
26
|
+
* Turns a UI message stream, e.g. `result.toUIMessageStream()`, into one
|
|
27
|
+
* where pickles are `data-pickle` parts. Prose stays in text parts, in the
|
|
28
|
+
* reply's order; a block's part updates in place as it streams and when it
|
|
29
|
+
* is repaired, and the stream finishes after every repair. Cancelling it
|
|
30
|
+
* stops the repairs. `TMessage` is the app's message type, for
|
|
31
|
+
* `createUIMessageStream`; its `pickle` data part must be a
|
|
32
|
+
* `PickleBlockData` or a `StoredPickleBlock`.
|
|
33
|
+
*/
|
|
34
|
+
toUIMessageStream: <TMessage extends UIMessage = PickleUIMessage>(stream: ReadableStream<UIMessageChunk>) => ReadableStream<InferUIMessageChunk<TMessage>>;
|
|
35
|
+
/** The chat turn under it, for anything the stream transform does not cover. */
|
|
36
|
+
turn: ChatTurn;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Starts one chat reply on the AI SDK. Runs the server's `context` for
|
|
40
|
+
* `request` (a PickleError it throws rejects, as on a call) and reads the
|
|
41
|
+
* conversation's earlier pickles from `messages`, so edits and versions
|
|
42
|
+
* continue from them.
|
|
43
|
+
*/
|
|
44
|
+
export declare function pickleChat<TContext, TSpecName extends string>(pickle: PickleServer<TContext, TSpecName>, input: {
|
|
45
|
+
messages: ReadonlyArray<UIMessage>;
|
|
46
|
+
/** App options for the server's `options` schema, passed to repairs. */
|
|
47
|
+
options?: unknown;
|
|
48
|
+
request: Request;
|
|
49
|
+
}): Promise<PickleChat>;
|
|
50
|
+
/**
|
|
51
|
+
* Compiles the pickles of a stored conversation again, for showing it after
|
|
52
|
+
* a reload. Programs are rebuilt from each block's `source` here, never read
|
|
53
|
+
* back from storage. Other parts and the metadata pass through as they are,
|
|
54
|
+
* so a `StoredPickleUIMessage<Metadata>`, or the stored form of your own
|
|
55
|
+
* message type, comes back with `PickleBlockData` in its `data-pickle` parts.
|
|
56
|
+
*/
|
|
57
|
+
export declare function revivePickles<TMessage extends UIMessage>(pickle: Pick<PickleServer<unknown>, "compile">, messages: ReadonlyArray<TMessage>): Promise<Array<WithPickleBlocks<TMessage, PickleBlockData>>>;
|
|
58
|
+
//#endregion
|
package/dist/chat.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { storedBlock } from "./schemas.js";
|
|
2
|
+
import { chatBlockText } from "@jigs.design/server";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
//#region src/chat.ts
|
|
5
|
+
const blockSource = z.object({
|
|
6
|
+
id: z.string(),
|
|
7
|
+
source: z.string()
|
|
8
|
+
});
|
|
9
|
+
/** The conversation's blocks, oldest first, read from its `data-pickle` parts. */
|
|
10
|
+
function blocksIn(messages) {
|
|
11
|
+
const blocks = [];
|
|
12
|
+
for (const message of messages) {
|
|
13
|
+
if (message.role !== "assistant") continue;
|
|
14
|
+
for (const part of message.parts) {
|
|
15
|
+
if (part.type !== "data-pickle" || !("data" in part)) continue;
|
|
16
|
+
const parsed = blockSource.safeParse(part.data);
|
|
17
|
+
if (parsed.success) blocks.push(parsed.data);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return blocks;
|
|
21
|
+
}
|
|
22
|
+
/** The text of the last user message: what this reply answers, for repairs. */
|
|
23
|
+
function lastPrompt(messages) {
|
|
24
|
+
const text = messages.findLast((entry) => entry.role === "user")?.parts.flatMap((part) => part.type === "text" ? [part.text] : []).join("\n");
|
|
25
|
+
return text === void 0 || text === "" ? void 0 : text;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Starts one chat reply on the AI SDK. Runs the server's `context` for
|
|
29
|
+
* `request` (a PickleError it throws rejects, as on a call) and reads the
|
|
30
|
+
* conversation's earlier pickles from `messages`, so edits and versions
|
|
31
|
+
* continue from them.
|
|
32
|
+
*/
|
|
33
|
+
async function pickleChat(pickle, input) {
|
|
34
|
+
const stop = new AbortController();
|
|
35
|
+
const turn = await pickle.chat(input.request, {
|
|
36
|
+
blocks: blocksIn(input.messages),
|
|
37
|
+
options: input.options,
|
|
38
|
+
prompt: lastPrompt(input.messages),
|
|
39
|
+
signal: AbortSignal.any([input.request.signal, stop.signal])
|
|
40
|
+
});
|
|
41
|
+
return {
|
|
42
|
+
convertDataPart: (part) => {
|
|
43
|
+
if (part.type !== "data-pickle") return;
|
|
44
|
+
const parsed = blockSource.safeParse(part.data);
|
|
45
|
+
return parsed.success ? {
|
|
46
|
+
text: chatBlockText(parsed.data),
|
|
47
|
+
type: "text"
|
|
48
|
+
} : void 0;
|
|
49
|
+
},
|
|
50
|
+
system: turn.system,
|
|
51
|
+
systemPrefix: turn.systemPrefix,
|
|
52
|
+
toUIMessageStream: (stream) => transform(turn, stream, stop),
|
|
53
|
+
turn
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** What a block that was still open says when its reply stops. */
|
|
57
|
+
const STOPPED = {
|
|
58
|
+
code: "INTERNAL",
|
|
59
|
+
message: "The reply stopped before this pickle was finished."
|
|
60
|
+
};
|
|
61
|
+
function transform(turn, stream, stop) {
|
|
62
|
+
const reader = stream.getReader();
|
|
63
|
+
let closed = false;
|
|
64
|
+
return new ReadableStream({
|
|
65
|
+
async cancel(reason) {
|
|
66
|
+
closed = true;
|
|
67
|
+
stop.abort(reason);
|
|
68
|
+
await reader.cancel(reason);
|
|
69
|
+
},
|
|
70
|
+
async start(controller) {
|
|
71
|
+
const repairs = /* @__PURE__ */ new Set();
|
|
72
|
+
let open = null;
|
|
73
|
+
let replying = null;
|
|
74
|
+
let segment = 0;
|
|
75
|
+
const send = (chunk) => {
|
|
76
|
+
if (!closed) controller.enqueue(chunk);
|
|
77
|
+
};
|
|
78
|
+
const closeText = () => {
|
|
79
|
+
if (open !== null) {
|
|
80
|
+
send({
|
|
81
|
+
id: open,
|
|
82
|
+
type: "text-end"
|
|
83
|
+
});
|
|
84
|
+
open = null;
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const sendBlock = (block) => {
|
|
88
|
+
send({
|
|
89
|
+
data: block,
|
|
90
|
+
id: block.key,
|
|
91
|
+
type: "data-pickle"
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
const repair = (block) => {
|
|
95
|
+
const task = (async () => {
|
|
96
|
+
try {
|
|
97
|
+
for await (const fixed of turn.repair(block)) sendBlock(fixed);
|
|
98
|
+
} catch {
|
|
99
|
+
sendBlock({
|
|
100
|
+
...block,
|
|
101
|
+
error: {
|
|
102
|
+
code: "INTERNAL",
|
|
103
|
+
message: "The repair failed."
|
|
104
|
+
},
|
|
105
|
+
status: "done"
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
})();
|
|
109
|
+
repairs.add(task);
|
|
110
|
+
task.finally(() => repairs.delete(task));
|
|
111
|
+
};
|
|
112
|
+
const handle = (events, base, stopped = false) => {
|
|
113
|
+
for (const event of events) if (event.type === "text") {
|
|
114
|
+
if (event.text === "") continue;
|
|
115
|
+
if (open === null) {
|
|
116
|
+
open = `${base}-${segment}`;
|
|
117
|
+
segment++;
|
|
118
|
+
send({
|
|
119
|
+
id: open,
|
|
120
|
+
type: "text-start"
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
send({
|
|
124
|
+
delta: event.text,
|
|
125
|
+
id: open,
|
|
126
|
+
type: "text-delta"
|
|
127
|
+
});
|
|
128
|
+
} else if (stopped) {
|
|
129
|
+
closeText();
|
|
130
|
+
sendBlock({
|
|
131
|
+
...event.block,
|
|
132
|
+
error: STOPPED,
|
|
133
|
+
status: "done"
|
|
134
|
+
});
|
|
135
|
+
} else {
|
|
136
|
+
closeText();
|
|
137
|
+
sendBlock(event.block);
|
|
138
|
+
if (event.block.status === "repairing") repair(event.block);
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
const cutShort = () => {
|
|
142
|
+
if (replying !== null) {
|
|
143
|
+
handle(turn.end(), replying, true);
|
|
144
|
+
replying = null;
|
|
145
|
+
}
|
|
146
|
+
closeText();
|
|
147
|
+
};
|
|
148
|
+
try {
|
|
149
|
+
for (;;) {
|
|
150
|
+
const step = await reader.read();
|
|
151
|
+
if (step.done) break;
|
|
152
|
+
const chunk = step.value;
|
|
153
|
+
switch (chunk.type) {
|
|
154
|
+
case "text-start":
|
|
155
|
+
replying = chunk.id;
|
|
156
|
+
break;
|
|
157
|
+
case "text-delta":
|
|
158
|
+
handle(turn.push(chunk.delta), chunk.id);
|
|
159
|
+
break;
|
|
160
|
+
case "text-end":
|
|
161
|
+
replying = null;
|
|
162
|
+
handle(turn.end(), chunk.id);
|
|
163
|
+
closeText();
|
|
164
|
+
break;
|
|
165
|
+
case "abort":
|
|
166
|
+
case "error":
|
|
167
|
+
cutShort();
|
|
168
|
+
send(chunk);
|
|
169
|
+
break;
|
|
170
|
+
case "finish":
|
|
171
|
+
closeText();
|
|
172
|
+
await Promise.all(repairs);
|
|
173
|
+
send(chunk);
|
|
174
|
+
break;
|
|
175
|
+
default: send(chunk);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
cutShort();
|
|
179
|
+
await Promise.all(repairs);
|
|
180
|
+
if (!closed) {
|
|
181
|
+
closed = true;
|
|
182
|
+
controller.close();
|
|
183
|
+
}
|
|
184
|
+
} catch (error) {
|
|
185
|
+
if (!closed) {
|
|
186
|
+
cutShort();
|
|
187
|
+
closed = true;
|
|
188
|
+
controller.error(error);
|
|
189
|
+
}
|
|
190
|
+
stop.abort(error);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Compiles the pickles of a stored conversation again, for showing it after
|
|
197
|
+
* a reload. Programs are rebuilt from each block's `source` here, never read
|
|
198
|
+
* back from storage. Other parts and the metadata pass through as they are,
|
|
199
|
+
* so a `StoredPickleUIMessage<Metadata>`, or the stored form of your own
|
|
200
|
+
* message type, comes back with `PickleBlockData` in its `data-pickle` parts.
|
|
201
|
+
*/
|
|
202
|
+
async function revivePickles(pickle, messages) {
|
|
203
|
+
return await Promise.all(messages.map(async (message) => ({
|
|
204
|
+
...message,
|
|
205
|
+
parts: await Promise.all(message.parts.map(async (part) => {
|
|
206
|
+
if (part.type !== "data-pickle" || !("data" in part)) return part;
|
|
207
|
+
const stored = storedBlock.parse(part.data);
|
|
208
|
+
const compiled = await pickle.compile(stored.source, { strict: false });
|
|
209
|
+
const data = {
|
|
210
|
+
...stored,
|
|
211
|
+
diagnostics: compiled.diagnostics,
|
|
212
|
+
program: compiled.program,
|
|
213
|
+
status: "done"
|
|
214
|
+
};
|
|
215
|
+
return {
|
|
216
|
+
...part,
|
|
217
|
+
data
|
|
218
|
+
};
|
|
219
|
+
}))
|
|
220
|
+
})));
|
|
221
|
+
}
|
|
222
|
+
//#endregion
|
|
223
|
+
export { pickleChat, revivePickles };
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import { PickleBlockData, PickleDataTypes, PickleUIMessage, StoredPickleBlock, StoredPickleUIMessage, recentMessages, withoutPrograms } from "./stored.js";
|
|
2
|
+
export { type PickleBlockData, type PickleDataTypes, type PickleUIMessage, type StoredPickleBlock, type StoredPickleUIMessage, recentMessages, withoutPrograms };
|
package/dist/client.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { recentMessages, withoutPrograms } from "./stored.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { PickleBlockData, PickleDataTypes, PickleUIMessage, StoredPickleBlock, StoredPickleUIMessage, recentMessages, withoutPrograms } from "./stored.js";
|
|
2
|
+
import { PickleChat, pickleChat, revivePickles } from "./chat.js";
|
|
3
|
+
import { ModelSettings, aiSdkModel, cachedInstructions } from "./model.js";
|
|
4
|
+
import { pickleDataSchemas } from "./schemas.js";
|
|
5
|
+
export { type ModelSettings, type PickleBlockData, type PickleChat, type PickleDataTypes, type PickleUIMessage, type StoredPickleBlock, type StoredPickleUIMessage, aiSdkModel, cachedInstructions, pickleChat, pickleDataSchemas, recentMessages, revivePickles, withoutPrograms };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { pickleDataSchemas } from "./schemas.js";
|
|
2
|
+
import { pickleChat, revivePickles } from "./chat.js";
|
|
3
|
+
import { aiSdkModel, cachedInstructions } from "./model.js";
|
|
4
|
+
import { recentMessages, withoutPrograms } from "./stored.js";
|
|
5
|
+
export { aiSdkModel, cachedInstructions, pickleChat, pickleDataSchemas, recentMessages, revivePickles, withoutPrograms };
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { ModelFunction } from "@jigs.design/server";
|
|
2
|
+
import { Instructions, LanguageModel, streamText } from "ai";
|
|
3
|
+
//#region src/model.d.ts
|
|
4
|
+
/** `streamText` settings that apply to every call, such as `temperature`. */
|
|
5
|
+
export type ModelSettings = Omit<Parameters<typeof streamText>[0], "abortSignal" | "instructions" | "messages" | "model" | "prompt" | "system">;
|
|
6
|
+
/**
|
|
7
|
+
* `system` as instructions whose first part, `prefix`, a provider may cache:
|
|
8
|
+
* two system messages, the first marked as a cache breakpoint for providers
|
|
9
|
+
* that need one (Anthropic). Providers that cache a repeated prefix on their
|
|
10
|
+
* own (OpenAI, DeepSeek, Google) read the two as one. Without a prefix that
|
|
11
|
+
* `system` starts with, `system` alone.
|
|
12
|
+
*/
|
|
13
|
+
export declare function cachedInstructions(system: string, prefix: string): Instructions;
|
|
14
|
+
/**
|
|
15
|
+
* A `model` for `createPickleServer` from an AI SDK model: document-mode
|
|
16
|
+
* sessions and chat repairs stream through `streamText`, and token usage
|
|
17
|
+
* reaches `onTurn`. The prompt's stable start is marked for the provider's
|
|
18
|
+
* cache (`cachedInstructions`).
|
|
19
|
+
*/
|
|
20
|
+
export declare function aiSdkModel(model: LanguageModel, settings?: ModelSettings): ModelFunction<unknown, unknown>;
|
|
21
|
+
//#endregion
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { streamText } from "ai";
|
|
2
|
+
//#region src/model.ts
|
|
3
|
+
/**
|
|
4
|
+
* `system` as instructions whose first part, `prefix`, a provider may cache:
|
|
5
|
+
* two system messages, the first marked as a cache breakpoint for providers
|
|
6
|
+
* that need one (Anthropic). Providers that cache a repeated prefix on their
|
|
7
|
+
* own (OpenAI, DeepSeek, Google) read the two as one. Without a prefix that
|
|
8
|
+
* `system` starts with, `system` alone.
|
|
9
|
+
*/
|
|
10
|
+
function cachedInstructions(system, prefix) {
|
|
11
|
+
if (prefix === "" || !system.startsWith(prefix)) return system;
|
|
12
|
+
return [{
|
|
13
|
+
content: prefix,
|
|
14
|
+
providerOptions: { anthropic: { cacheControl: { type: "ephemeral" } } },
|
|
15
|
+
role: "system"
|
|
16
|
+
}, {
|
|
17
|
+
content: system.slice(prefix.length),
|
|
18
|
+
role: "system"
|
|
19
|
+
}];
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A `model` for `createPickleServer` from an AI SDK model: document-mode
|
|
23
|
+
* sessions and chat repairs stream through `streamText`, and token usage
|
|
24
|
+
* reaches `onTurn`. The prompt's stable start is marked for the provider's
|
|
25
|
+
* cache (`cachedInstructions`).
|
|
26
|
+
*/
|
|
27
|
+
function aiSdkModel(model, settings = {}) {
|
|
28
|
+
return async function* (request) {
|
|
29
|
+
const result = streamText({
|
|
30
|
+
...settings,
|
|
31
|
+
abortSignal: request.signal,
|
|
32
|
+
instructions: cachedInstructions(request.system, request.systemPrefix),
|
|
33
|
+
messages: request.messages.map((message) => ({
|
|
34
|
+
content: message.content,
|
|
35
|
+
role: message.role
|
|
36
|
+
})),
|
|
37
|
+
model
|
|
38
|
+
});
|
|
39
|
+
for await (const part of result.stream) if (part.type === "text-delta") yield part.text;
|
|
40
|
+
else if (part.type === "error") throw part.error;
|
|
41
|
+
const usage = await result.usage;
|
|
42
|
+
const sent = {
|
|
43
|
+
inputTokens: usage.inputTokens ?? 0,
|
|
44
|
+
outputTokens: usage.outputTokens ?? 0
|
|
45
|
+
};
|
|
46
|
+
const details = usage.inputTokenDetails;
|
|
47
|
+
if (details.cacheReadTokens !== void 0) sent.cacheReadTokens = details.cacheReadTokens;
|
|
48
|
+
if (details.cacheWriteTokens !== void 0) sent.cacheWriteTokens = details.cacheWriteTokens;
|
|
49
|
+
return { usage: sent };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { aiSdkModel, cachedInstructions };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
//#region src/schemas.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Schemas for `validateUIMessages<StoredPickleUIMessage>({ messages,
|
|
5
|
+
* dataSchemas })`, to check a conversation the browser sent or one read
|
|
6
|
+
* from storage. A pickle's program is dropped, never checked: the server
|
|
7
|
+
* reads only `id` and `source`, and a source is only ever compiled.
|
|
8
|
+
*/
|
|
9
|
+
export declare const pickleDataSchemas: {
|
|
10
|
+
pickle: z.ZodObject<{
|
|
11
|
+
diagnostics: z.ZodArray<z.ZodObject<{
|
|
12
|
+
code: z.ZodString;
|
|
13
|
+
fix: z.ZodExactOptional<z.ZodObject<{
|
|
14
|
+
edits: z.ZodArray<z.ZodObject<{
|
|
15
|
+
span: z.ZodObject<{
|
|
16
|
+
column: z.ZodExactOptional<z.ZodNumber>;
|
|
17
|
+
end: z.ZodExactOptional<z.ZodNumber>;
|
|
18
|
+
line: z.ZodNumber;
|
|
19
|
+
start: z.ZodExactOptional<z.ZodNumber>;
|
|
20
|
+
}, z.core.$strip>;
|
|
21
|
+
text: z.ZodString;
|
|
22
|
+
}, z.core.$strip>>;
|
|
23
|
+
title: z.ZodString;
|
|
24
|
+
}, z.core.$strip>>;
|
|
25
|
+
message: z.ZodString;
|
|
26
|
+
severity: z.ZodEnum<{
|
|
27
|
+
error: "error";
|
|
28
|
+
hint: "hint";
|
|
29
|
+
info: "info";
|
|
30
|
+
warning: "warning";
|
|
31
|
+
}>;
|
|
32
|
+
span: z.ZodObject<{
|
|
33
|
+
column: z.ZodExactOptional<z.ZodNumber>;
|
|
34
|
+
end: z.ZodExactOptional<z.ZodNumber>;
|
|
35
|
+
line: z.ZodNumber;
|
|
36
|
+
start: z.ZodExactOptional<z.ZodNumber>;
|
|
37
|
+
}, z.core.$strip>;
|
|
38
|
+
}, z.core.$strip>>;
|
|
39
|
+
error: z.ZodExactOptional<z.ZodObject<{
|
|
40
|
+
code: z.ZodEnum<{
|
|
41
|
+
BAD_INPUT: "BAD_INPUT";
|
|
42
|
+
CONFLICT: "CONFLICT";
|
|
43
|
+
DECLINED: "DECLINED";
|
|
44
|
+
FORBIDDEN: "FORBIDDEN";
|
|
45
|
+
INTERNAL: "INTERNAL";
|
|
46
|
+
NETWORK: "NETWORK";
|
|
47
|
+
NOT_FOUND: "NOT_FOUND";
|
|
48
|
+
RATE_LIMITED: "RATE_LIMITED";
|
|
49
|
+
UNAUTHORIZED: "UNAUTHORIZED";
|
|
50
|
+
UNAVAILABLE: "UNAVAILABLE";
|
|
51
|
+
UNKNOWN_PROCEDURE: "UNKNOWN_PROCEDURE";
|
|
52
|
+
}>;
|
|
53
|
+
message: z.ZodString;
|
|
54
|
+
}, z.core.$strip>>;
|
|
55
|
+
id: z.ZodString;
|
|
56
|
+
key: z.ZodString;
|
|
57
|
+
source: z.ZodString;
|
|
58
|
+
status: z.ZodEnum<{
|
|
59
|
+
done: "done";
|
|
60
|
+
repairing: "repairing";
|
|
61
|
+
streaming: "streaming";
|
|
62
|
+
}>;
|
|
63
|
+
version: z.ZodNumber;
|
|
64
|
+
}, z.core.$strip>;
|
|
65
|
+
};
|
|
66
|
+
//#endregion
|
package/dist/schemas.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { pickleErrorCodes } from "@jigs.design/runtime";
|
|
3
|
+
//#region src/schemas.ts
|
|
4
|
+
const span = z.object({
|
|
5
|
+
column: z.number().exactOptional(),
|
|
6
|
+
end: z.number().exactOptional(),
|
|
7
|
+
line: z.number(),
|
|
8
|
+
start: z.number().exactOptional()
|
|
9
|
+
});
|
|
10
|
+
const storedBlock = z.object({
|
|
11
|
+
diagnostics: z.array(z.object({
|
|
12
|
+
code: z.string(),
|
|
13
|
+
fix: z.object({
|
|
14
|
+
edits: z.array(z.object({
|
|
15
|
+
span,
|
|
16
|
+
text: z.string()
|
|
17
|
+
})),
|
|
18
|
+
title: z.string()
|
|
19
|
+
}).exactOptional(),
|
|
20
|
+
message: z.string(),
|
|
21
|
+
severity: z.enum([
|
|
22
|
+
"error",
|
|
23
|
+
"hint",
|
|
24
|
+
"info",
|
|
25
|
+
"warning"
|
|
26
|
+
]),
|
|
27
|
+
span
|
|
28
|
+
})),
|
|
29
|
+
error: z.object({
|
|
30
|
+
code: z.enum(pickleErrorCodes),
|
|
31
|
+
message: z.string()
|
|
32
|
+
}).exactOptional(),
|
|
33
|
+
id: z.string(),
|
|
34
|
+
key: z.string(),
|
|
35
|
+
source: z.string(),
|
|
36
|
+
status: z.enum([
|
|
37
|
+
"streaming",
|
|
38
|
+
"repairing",
|
|
39
|
+
"done"
|
|
40
|
+
]),
|
|
41
|
+
version: z.number()
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Schemas for `validateUIMessages<StoredPickleUIMessage>({ messages,
|
|
45
|
+
* dataSchemas })`, to check a conversation the browser sent or one read
|
|
46
|
+
* from storage. A pickle's program is dropped, never checked: the server
|
|
47
|
+
* reads only `id` and `source`, and a source is only ever compiled.
|
|
48
|
+
*/
|
|
49
|
+
const pickleDataSchemas = { pickle: storedBlock };
|
|
50
|
+
//#endregion
|
|
51
|
+
export { pickleDataSchemas, storedBlock };
|
package/dist/stored.d.ts
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ChatBlock } from "@jigs.design/runtime";
|
|
2
|
+
import { UIMessage } from "ai";
|
|
3
|
+
//#region src/stored.d.ts
|
|
4
|
+
/** A pickle's `data-pickle` part: one fenced block of a reply, compiled. */
|
|
5
|
+
export type PickleBlockData = ChatBlock;
|
|
6
|
+
/**
|
|
7
|
+
* A pickle as the browser sends it back and as you store it: everything but
|
|
8
|
+
* the program. Only `revivePickles` turns it back into a `PickleBlockData`,
|
|
9
|
+
* by compiling `source` again.
|
|
10
|
+
*/
|
|
11
|
+
export type StoredPickleBlock = Omit<PickleBlockData, "program">;
|
|
12
|
+
/**
|
|
13
|
+
* The data parts chat mode adds to a message. A type, not an interface: the
|
|
14
|
+
* AI SDK's data types need an implicit index signature.
|
|
15
|
+
*/
|
|
16
|
+
export type PickleDataTypes = {
|
|
17
|
+
pickle: PickleBlockData;
|
|
18
|
+
};
|
|
19
|
+
/** A `useChat` message whose replies may hold pickles. */
|
|
20
|
+
export type PickleUIMessage<TMetadata = unknown> = UIMessage<TMetadata, PickleDataTypes>;
|
|
21
|
+
/**
|
|
22
|
+
* A conversation as the server receives it and as you store it: its pickles
|
|
23
|
+
* carry no programs. `validateUIMessages` with `pickleDataSchemas` returns
|
|
24
|
+
* this shape.
|
|
25
|
+
*/
|
|
26
|
+
export type StoredPickleUIMessage<TMetadata = unknown> = UIMessage<TMetadata, {
|
|
27
|
+
pickle: StoredPickleBlock;
|
|
28
|
+
}>;
|
|
29
|
+
/** A message part, with a `data-pickle` part's data replaced by `TBlock`. */
|
|
30
|
+
type WithBlock<TPart, TBlock> = TPart extends {
|
|
31
|
+
type: "data-pickle";
|
|
32
|
+
} ? Omit<TPart, "data"> & {
|
|
33
|
+
data: TBlock;
|
|
34
|
+
} : TPart;
|
|
35
|
+
/**
|
|
36
|
+
* `TMessage` with each pickle's data replaced by `TBlock`, and every other
|
|
37
|
+
* part, the metadata included, as it was.
|
|
38
|
+
*/
|
|
39
|
+
export type WithPickleBlocks<TMessage extends UIMessage, TBlock> = Omit<TMessage, "parts"> & {
|
|
40
|
+
parts: Array<WithBlock<TMessage["parts"][number], TBlock>>;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* The messages without their pickles' programs: for the body `useChat`
|
|
44
|
+
* posts (`prepareSendMessagesRequest`), and for storing a conversation. The
|
|
45
|
+
* server reads only each block's `id` and `source`. Every other part, and
|
|
46
|
+
* every other field of a block, is kept as it is.
|
|
47
|
+
*/
|
|
48
|
+
export declare function withoutPrograms<TMessage extends UIMessage>(messages: ReadonlyArray<TMessage>): Array<WithPickleBlocks<TMessage, StoredPickleBlock>>;
|
|
49
|
+
/**
|
|
50
|
+
* The end of a long conversation, for the body `useChat` posts: the last
|
|
51
|
+
* `keep` messages, from a user message on. Every earlier pickle comes first,
|
|
52
|
+
* in one assistant message of their `data-pickle` parts, so the model still
|
|
53
|
+
* sees each one and edits and versions continue from them; earlier prose is
|
|
54
|
+
* left out. A conversation no longer than `keep` is returned as it is.
|
|
55
|
+
*/
|
|
56
|
+
export declare function recentMessages<TMessage extends UIMessage>(messages: ReadonlyArray<TMessage>, keep?: number): Array<TMessage>;
|
|
57
|
+
//#endregion
|
|
58
|
+
export {};
|
package/dist/stored.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
//#region src/stored.ts
|
|
2
|
+
/** Whether `part` is a pickle whose block still holds its program. */
|
|
3
|
+
function hasProgram(part) {
|
|
4
|
+
return part.type === "data-pickle" && "data" in part && typeof part.data === "object" && part.data !== null && "program" in part.data;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The messages without their pickles' programs: for the body `useChat`
|
|
8
|
+
* posts (`prepareSendMessagesRequest`), and for storing a conversation. The
|
|
9
|
+
* server reads only each block's `id` and `source`. Every other part, and
|
|
10
|
+
* every other field of a block, is kept as it is.
|
|
11
|
+
*/
|
|
12
|
+
function withoutPrograms(messages) {
|
|
13
|
+
return messages.map((message) => ({
|
|
14
|
+
...message,
|
|
15
|
+
parts: message.parts.map((part) => {
|
|
16
|
+
if (!hasProgram(part)) return part;
|
|
17
|
+
const { program: _program, ...data } = part.data;
|
|
18
|
+
return {
|
|
19
|
+
...part,
|
|
20
|
+
data
|
|
21
|
+
};
|
|
22
|
+
})
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
/** How many messages `recentMessages` keeps by default. */
|
|
26
|
+
const RECENT = 20;
|
|
27
|
+
/**
|
|
28
|
+
* The end of a long conversation, for the body `useChat` posts: the last
|
|
29
|
+
* `keep` messages, from a user message on. Every earlier pickle comes first,
|
|
30
|
+
* in one assistant message of their `data-pickle` parts, so the model still
|
|
31
|
+
* sees each one and edits and versions continue from them; earlier prose is
|
|
32
|
+
* left out. A conversation no longer than `keep` is returned as it is.
|
|
33
|
+
*/
|
|
34
|
+
function recentMessages(messages, keep = RECENT) {
|
|
35
|
+
if (messages.length <= keep) return [...messages];
|
|
36
|
+
let start = messages.length - keep;
|
|
37
|
+
const nextUser = messages.findIndex((message, index) => index >= start && message.role === "user");
|
|
38
|
+
if (nextUser !== -1) start = nextUser;
|
|
39
|
+
const earlier = messages.slice(0, start).filter((message) => message.role === "assistant");
|
|
40
|
+
const pickles = earlier.flatMap((message) => message.parts.filter((part) => part.type === "data-pickle"));
|
|
41
|
+
const recent = messages.slice(start);
|
|
42
|
+
const [first] = earlier;
|
|
43
|
+
if (first === void 0 || pickles.length === 0) return recent;
|
|
44
|
+
return [{
|
|
45
|
+
...first,
|
|
46
|
+
parts: pickles
|
|
47
|
+
}, ...recent];
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { recentMessages, withoutPrograms };
|
package/package.json
CHANGED
|
@@ -1,6 +1,46 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jigs.design/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Pickle on the AI SDK: a model function for createPickleServer, and chat mode for useChat.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/yoamomonstruos/pickle.git",
|
|
9
|
+
"directory": "packages/ai-sdk"
|
|
10
|
+
},
|
|
11
|
+
"type": "module",
|
|
12
|
+
"sideEffects": false,
|
|
13
|
+
"files": [
|
|
14
|
+
"dist",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"exports": {
|
|
19
|
+
".": {
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"import": "./dist/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./client": {
|
|
24
|
+
"types": "./dist/client.d.ts",
|
|
25
|
+
"import": "./dist/client.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@jigs.design/server": "0.1.0",
|
|
33
|
+
"@jigs.design/runtime": "0.1.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"ai": "^7.0.0",
|
|
37
|
+
"zod": "^4.0.0"
|
|
38
|
+
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"@ai-sdk/provider": "4.0.27",
|
|
41
|
+
"ai": "7.0.140",
|
|
42
|
+
"zod": "4.6.7",
|
|
43
|
+
"@jigs.design/host": "0.1.0",
|
|
44
|
+
"@jigs.design/catalog": "0.1.0"
|
|
45
|
+
}
|
|
6
46
|
}
|