@effect-uai/ai-sdk 0.10.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 +80 -0
- package/dist/Messages.d.mts +40 -0
- package/dist/Messages.d.mts.map +1 -0
- package/dist/Messages.mjs +76 -0
- package/dist/Messages.mjs.map +1 -0
- package/dist/UIMessageStream.d.mts +112 -0
- package/dist/UIMessageStream.d.mts.map +1 -0
- package/dist/UIMessageStream.mjs +160 -0
- package/dist/UIMessageStream.mjs.map +1 -0
- package/dist/UIMessageStream.test.d.mts +1 -0
- package/dist/UIMessageStream.test.mjs +239 -0
- package/dist/UIMessageStream.test.mjs.map +1 -0
- package/dist/chunk-CfYAbeIz.mjs +13 -0
- package/dist/conformance.test.d.mts +1 -0
- package/dist/conformance.test.mjs +11374 -0
- package/dist/conformance.test.mjs.map +1 -0
- package/dist/dist-DaKrk-s5.mjs +13769 -0
- package/dist/dist-DaKrk-s5.mjs.map +1 -0
- package/dist/index.d.mts +3 -0
- package/dist/index.mjs +3 -0
- package/dist/magic-string.es-CaWgF-3p.mjs +1013 -0
- package/dist/magic-string.es-CaWgF-3p.mjs.map +1 -0
- package/package.json +66 -0
- package/src/Messages.ts +122 -0
- package/src/UIMessageStream.test.ts +219 -0
- package/src/UIMessageStream.ts +233 -0
- package/src/conformance.test.ts +93 -0
- package/src/index.ts +2 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Betalyra Sociedade Unipessoal Lda.
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# @effect-uai/ai-sdk
|
|
2
|
+
|
|
3
|
+
Vercel AI SDK compatibility for effect-uai. Keep your `@ai-sdk/react`
|
|
4
|
+
`useChat` frontend exactly as it is and serve it from an effect-uai agent
|
|
5
|
+
loop instead of `streamText`.
|
|
6
|
+
|
|
7
|
+
Two functions bridge the two worlds:
|
|
8
|
+
|
|
9
|
+
- `Messages.decodeMessages` turns the `UIMessage[]` a `useChat` client POSTs
|
|
10
|
+
into effect-uai `HistoryItem`s.
|
|
11
|
+
- `UIMessageStream.toUIMessageStream` projects the loop's `InteractionEvent`
|
|
12
|
+
stream onto the AI SDK UI Message Stream protocol (`v1`), emitting
|
|
13
|
+
`SSE.Event`s.
|
|
14
|
+
|
|
15
|
+
The package owns no HTTP layer: it produces a `Stream<SSE.Event>` and the
|
|
16
|
+
required `responseHeaders`, so it drops into any server.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```sh
|
|
21
|
+
pnpm add @effect-uai/ai-sdk
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## A chat route
|
|
25
|
+
|
|
26
|
+
`decodeMessages` in, run your loop, `toUIMessageStream` out. Provide your
|
|
27
|
+
provider layer so the stream has no remaining requirements, then hand the
|
|
28
|
+
bytes to whatever server you run.
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import * as Messages from "@effect-uai/ai-sdk/Messages"
|
|
32
|
+
import * as UIMessageStream from "@effect-uai/ai-sdk/UIMessageStream"
|
|
33
|
+
import * as SSE from "@effect-uai/core/SSE"
|
|
34
|
+
import { Stream } from "effect"
|
|
35
|
+
|
|
36
|
+
export async function POST(request: Request): Promise<Response> {
|
|
37
|
+
const { messages } = await request.json()
|
|
38
|
+
const history = Messages.decodeMessages(messages)
|
|
39
|
+
|
|
40
|
+
const events = agent(history).pipe(Stream.provide(OpenAILayer)) // your loop, unchanged
|
|
41
|
+
|
|
42
|
+
const body = events.pipe(
|
|
43
|
+
UIMessageStream.toUIMessageStream(crypto.randomUUID()),
|
|
44
|
+
SSE.toBytes,
|
|
45
|
+
Stream.toReadableStream,
|
|
46
|
+
)
|
|
47
|
+
return new Response(body, { headers: UIMessageStream.responseHeaders })
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
### With `@effect/platform`
|
|
52
|
+
|
|
53
|
+
Same stream, wrapped in an `HttpServerResponse` instead of a web `Response`:
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
import { HttpServerResponse } from "@effect/platform"
|
|
57
|
+
|
|
58
|
+
const body = events.pipe(UIMessageStream.toUIMessageStream(id), SSE.toBytes)
|
|
59
|
+
return HttpServerResponse.stream(body, { headers: UIMessageStream.responseHeaders })
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Coverage
|
|
63
|
+
|
|
64
|
+
Outbound, text and reasoning deltas, tool calls (input streaming plus
|
|
65
|
+
resolved output), and refusals map to their protocol parts. Alongside the
|
|
66
|
+
loop's `InteractionEvent`s, `toUIMessageStream` also accepts `dataPart` and
|
|
67
|
+
`messageMetadata` emissions, so callers can interleave custom typed data
|
|
68
|
+
(`data-<name>`, optionally `transient`) and settled per-message metadata:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
events.pipe(
|
|
72
|
+
Stream.map((e) => (Metrics.isThroughput(e) ? dataPart("metrics", e, { transient: true }) : e)),
|
|
73
|
+
UIMessageStream.toUIMessageStream(id),
|
|
74
|
+
)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Inbound, `decodeMessages` reconstructs text, image (`file` with an `image/*`
|
|
78
|
+
media type), and tool parts: an assistant tool call becomes a `function_call`
|
|
79
|
+
item plus a `function_call_output` once the client carries its result.
|
|
80
|
+
Non-image files and unresolved tool states are dropped.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as Items from "@effect-uai/core/Items";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
//#region src/Messages.d.ts
|
|
5
|
+
declare namespace Messages_d_exports {
|
|
6
|
+
export { FilePart, TextPart, ToolPart, UIMessage, UIPart, decodeMessages };
|
|
7
|
+
}
|
|
8
|
+
declare const TextPart: Schema.Struct<{
|
|
9
|
+
readonly type: Schema.Literal<"text">;
|
|
10
|
+
readonly text: Schema.String;
|
|
11
|
+
}>;
|
|
12
|
+
type TextPart = typeof TextPart.Type;
|
|
13
|
+
declare const FilePart: Schema.Struct<{
|
|
14
|
+
readonly type: Schema.Literal<"file">;
|
|
15
|
+
readonly mediaType: Schema.String;
|
|
16
|
+
readonly url: Schema.String;
|
|
17
|
+
}>;
|
|
18
|
+
type FilePart = typeof FilePart.Type;
|
|
19
|
+
declare const ToolPart: Schema.Struct<{
|
|
20
|
+
readonly type: Schema.String;
|
|
21
|
+
readonly toolCallId: Schema.String;
|
|
22
|
+
readonly toolName: Schema.optional<Schema.String>;
|
|
23
|
+
readonly state: Schema.optional<Schema.String>;
|
|
24
|
+
readonly input: Schema.optional<Schema.Unknown>;
|
|
25
|
+
readonly output: Schema.optional<Schema.Unknown>;
|
|
26
|
+
}>;
|
|
27
|
+
type ToolPart = typeof ToolPart.Type;
|
|
28
|
+
type UIPart = {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly [key: string]: unknown;
|
|
31
|
+
};
|
|
32
|
+
type UIMessage = {
|
|
33
|
+
readonly id?: string;
|
|
34
|
+
readonly role: "user" | "assistant" | "system";
|
|
35
|
+
readonly parts: ReadonlyArray<UIPart>;
|
|
36
|
+
};
|
|
37
|
+
declare const decodeMessages: (messages: ReadonlyArray<UIMessage>) => ReadonlyArray<Items.HistoryItem>;
|
|
38
|
+
//#endregion
|
|
39
|
+
export { FilePart, TextPart, ToolPart, UIMessage, UIPart, decodeMessages, Messages_d_exports as t };
|
|
40
|
+
//# sourceMappingURL=Messages.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Messages.d.mts","names":[],"sources":["../src/Messages.ts"],"mappings":";;;;;;;cAmBM,QAAA,EAAQ,MAAA,CAAA,MAAA;EAAA;;;KAIF,QAAA,UAAkB,QAAA,CAAS,IAAA;AAAA,cAEjC,QAAA,EAAQ,MAAA,CAAA,MAAA;EAAA;;;;KAKF,QAAA,UAAkB,QAAA,CAAS,IAAA;AAAA,cAEjC,QAAA,EAAQ,MAAA,CAAA,MAAA;EAAA;;;;;;;KAQF,QAAA,UAAkB,QAAA,CAAS,IAAA;AAAA,KAS3B,MAAA;EAAA,SACD,IAAA;EAAA,UACC,GAAA;AAAA;AAAA,KAGA,SAAA;EAAA,SACD,EAAA;EAAA,SACA,IAAA;EAAA,SACA,KAAA,EAAO,aAAA,CAAc,MAAA;AAAA;AAAA,cA8DnB,cAAA,GACX,QAAA,EAAU,aAAA,CAAc,SAAA,MACvB,aAAA,CAAc,KAAA,CAAM,WAAA"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import * as Image from "@effect-uai/core/Image";
|
|
3
|
+
import * as Items from "@effect-uai/core/Items";
|
|
4
|
+
import { Match, Schema } from "effect";
|
|
5
|
+
//#region src/Messages.ts
|
|
6
|
+
/**
|
|
7
|
+
* Decode the `messages` array a `useChat` client POSTs (Vercel's `UIMessage`
|
|
8
|
+
* shape) back into effect-uai `HistoryItem`s, so an existing loop can run
|
|
9
|
+
* against an unchanged frontend.
|
|
10
|
+
*
|
|
11
|
+
* One `UIMessage` can expand to several history items: an assistant message
|
|
12
|
+
* carrying tool calls becomes an assistant `Message` followed by the
|
|
13
|
+
* `ToolCall` / `ToolCallOutput` items the model needs to see. Text and image
|
|
14
|
+
* (`file` with an `image/*` media type) parts fold into a message's content;
|
|
15
|
+
* non-image files and unresolved tool states are dropped.
|
|
16
|
+
*/
|
|
17
|
+
var Messages_exports = /* @__PURE__ */ __exportAll({ decodeMessages: () => decodeMessages });
|
|
18
|
+
const TextPart = Schema.Struct({
|
|
19
|
+
type: Schema.Literal("text"),
|
|
20
|
+
text: Schema.String
|
|
21
|
+
});
|
|
22
|
+
const FilePart = Schema.Struct({
|
|
23
|
+
type: Schema.Literal("file"),
|
|
24
|
+
mediaType: Schema.String,
|
|
25
|
+
url: Schema.String
|
|
26
|
+
});
|
|
27
|
+
const ToolPart = Schema.Struct({
|
|
28
|
+
type: Schema.String,
|
|
29
|
+
toolCallId: Schema.String,
|
|
30
|
+
toolName: Schema.optional(Schema.String),
|
|
31
|
+
state: Schema.optional(Schema.String),
|
|
32
|
+
input: Schema.optional(Schema.Unknown),
|
|
33
|
+
output: Schema.optional(Schema.Unknown)
|
|
34
|
+
});
|
|
35
|
+
const isText = Schema.is(TextPart);
|
|
36
|
+
const isFile = Schema.is(FilePart);
|
|
37
|
+
const isTool = Schema.is(ToolPart);
|
|
38
|
+
const isImageFile = (part) => part.mediaType.startsWith("image");
|
|
39
|
+
const textOf = (message) => message.parts.filter(isText).map((part) => part.text).join("");
|
|
40
|
+
const imageBlocks = (message) => message.parts.filter(isFile).filter(isImageFile).map((part) => ({
|
|
41
|
+
type: "input_image",
|
|
42
|
+
source: Image.imageUrl(part.url, part.mediaType)
|
|
43
|
+
}));
|
|
44
|
+
const userMessage = (message) => {
|
|
45
|
+
const text = textOf(message);
|
|
46
|
+
return {
|
|
47
|
+
type: "message",
|
|
48
|
+
role: "user",
|
|
49
|
+
content: [...text.length > 0 ? [{
|
|
50
|
+
type: "input_text",
|
|
51
|
+
text
|
|
52
|
+
}] : [], ...imageBlocks(message)]
|
|
53
|
+
};
|
|
54
|
+
};
|
|
55
|
+
const toolName = (part) => part.type === "dynamic-tool" ? part.toolName ?? "" : part.type.slice(5);
|
|
56
|
+
const toolItems = (part) => {
|
|
57
|
+
const call = {
|
|
58
|
+
type: "function_call",
|
|
59
|
+
call_id: part.toolCallId,
|
|
60
|
+
name: toolName(part),
|
|
61
|
+
arguments: JSON.stringify(part.input ?? {})
|
|
62
|
+
};
|
|
63
|
+
if (part.state !== "output-available") return [call];
|
|
64
|
+
const output = typeof part.output === "string" ? part.output : JSON.stringify(part.output);
|
|
65
|
+
return [call, Items.toolCallOutput(part.toolCallId, output)];
|
|
66
|
+
};
|
|
67
|
+
const assistantItems = (message) => {
|
|
68
|
+
const text = textOf(message);
|
|
69
|
+
return [...text.length > 0 ? [Items.assistantText(text)] : [], ...message.parts.filter(isTool).flatMap(toolItems)];
|
|
70
|
+
};
|
|
71
|
+
const decodeMessage = (message) => Match.value(message.role).pipe(Match.when("user", () => [userMessage(message)]), Match.when("system", () => [Items.systemText(textOf(message))]), Match.when("assistant", () => assistantItems(message)), Match.exhaustive);
|
|
72
|
+
const decodeMessages = (messages) => messages.flatMap(decodeMessage);
|
|
73
|
+
//#endregion
|
|
74
|
+
export { decodeMessages, Messages_exports as t };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=Messages.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Messages.mjs","names":[],"sources":["../src/Messages.ts"],"sourcesContent":["/**\n * Decode the `messages` array a `useChat` client POSTs (Vercel's `UIMessage`\n * shape) back into effect-uai `HistoryItem`s, so an existing loop can run\n * against an unchanged frontend.\n *\n * One `UIMessage` can expand to several history items: an assistant message\n * carrying tool calls becomes an assistant `Message` followed by the\n * `ToolCall` / `ToolCallOutput` items the model needs to see. Text and image\n * (`file` with an `image/*` media type) parts fold into a message's content;\n * non-image files and unresolved tool states are dropped.\n */\nimport * as Image from \"@effect-uai/core/Image\"\nimport * as Items from \"@effect-uai/core/Items\"\nimport { Match, Schema } from \"effect\"\n\n// The inbound part variants we understand. Tool parts are identified\n// structurally by `toolCallId` - their wire `type` is a dynamic `tool-<name>`\n// (or `dynamic-tool`) rather than a fixed literal - so their schema keeps\n// `type` open and reads the name off it.\nconst TextPart = Schema.Struct({\n type: Schema.Literal(\"text\"),\n text: Schema.String,\n})\nexport type TextPart = typeof TextPart.Type\n\nconst FilePart = Schema.Struct({\n type: Schema.Literal(\"file\"),\n mediaType: Schema.String,\n url: Schema.String,\n})\nexport type FilePart = typeof FilePart.Type\n\nconst ToolPart = Schema.Struct({\n type: Schema.String,\n toolCallId: Schema.String,\n toolName: Schema.optional(Schema.String),\n state: Schema.optional(Schema.String),\n input: Schema.optional(Schema.Unknown),\n output: Schema.optional(Schema.Unknown),\n})\nexport type ToolPart = typeof ToolPart.Type\n\nconst isText = Schema.is(TextPart)\nconst isFile = Schema.is(FilePart)\nconst isTool = Schema.is(ToolPart)\nconst isImageFile = (part: FilePart): boolean => part.mediaType.startsWith(\"image\")\n\n// A raw inbound part: a discriminating `type` plus whatever else the client\n// attached. Kept open so the schema guards do the narrowing.\nexport type UIPart = {\n readonly type: string\n readonly [key: string]: unknown\n}\n\nexport type UIMessage = {\n readonly id?: string\n readonly role: \"user\" | \"assistant\" | \"system\"\n readonly parts: ReadonlyArray<UIPart>\n}\n\nconst textOf = (message: UIMessage): string =>\n message.parts\n .filter(isText)\n .map((part) => part.text)\n .join(\"\")\n\nconst imageBlocks = (message: UIMessage): ReadonlyArray<Items.InputImage> =>\n message.parts\n .filter(isFile)\n .filter(isImageFile)\n .map((part) => ({ type: \"input_image\", source: Image.imageUrl(part.url, part.mediaType) }))\n\n// A user turn: one joined text block (if any) followed by one image block per\n// image file, preserving the model's expectation of text-then-media.\nconst userMessage = (message: UIMessage): Items.Message => {\n const text = textOf(message)\n return {\n type: \"message\",\n role: \"user\",\n content: [\n ...(text.length > 0 ? [{ type: \"input_text\", text } as const] : []),\n ...imageBlocks(message),\n ],\n }\n}\n\nconst toolName = (part: ToolPart): string =>\n part.type === \"dynamic-tool\" ? (part.toolName ?? \"\") : part.type.slice(\"tool-\".length)\n\n// A resolved tool call becomes a `function_call` item, plus a\n// `function_call_output` once the client carries its output.\nconst toolItems = (part: ToolPart): ReadonlyArray<Items.HistoryItem> => {\n const call: Items.ToolCall = {\n type: \"function_call\",\n call_id: part.toolCallId,\n name: toolName(part),\n arguments: JSON.stringify(part.input ?? {}),\n }\n if (part.state !== \"output-available\") return [call]\n const output = typeof part.output === \"string\" ? part.output : JSON.stringify(part.output)\n return [call, Items.toolCallOutput(part.toolCallId, output)]\n}\n\nconst assistantItems = (message: UIMessage): ReadonlyArray<Items.HistoryItem> => {\n const text = textOf(message)\n return [\n ...(text.length > 0 ? [Items.assistantText(text)] : []),\n ...message.parts.filter(isTool).flatMap(toolItems),\n ]\n}\n\nconst decodeMessage = (message: UIMessage): ReadonlyArray<Items.HistoryItem> =>\n Match.value(message.role).pipe(\n Match.when(\"user\", () => [userMessage(message)]),\n Match.when(\"system\", () => [Items.systemText(textOf(message))]),\n Match.when(\"assistant\", () => assistantItems(message)),\n Match.exhaustive,\n )\n\nexport const decodeMessages = (\n messages: ReadonlyArray<UIMessage>,\n): ReadonlyArray<Items.HistoryItem> => messages.flatMap(decodeMessage)\n"],"mappings":";;;;;;;;;;;;;;;;;AAmBA,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO,QAAQ,OAAO;CAC5B,MAAM,OAAO;CACd,CAAC;AAGF,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO,QAAQ,OAAO;CAC5B,WAAW,OAAO;CAClB,KAAK,OAAO;CACb,CAAC;AAGF,MAAM,WAAW,OAAO,OAAO;CAC7B,MAAM,OAAO;CACb,YAAY,OAAO;CACnB,UAAU,OAAO,SAAS,OAAO,OAAO;CACxC,OAAO,OAAO,SAAS,OAAO,OAAO;CACrC,OAAO,OAAO,SAAS,OAAO,QAAQ;CACtC,QAAQ,OAAO,SAAS,OAAO,QAAQ;CACxC,CAAC;AAGF,MAAM,SAAS,OAAO,GAAG,SAAS;AAClC,MAAM,SAAS,OAAO,GAAG,SAAS;AAClC,MAAM,SAAS,OAAO,GAAG,SAAS;AAClC,MAAM,eAAe,SAA4B,KAAK,UAAU,WAAW,QAAQ;AAenF,MAAM,UAAU,YACd,QAAQ,MACL,OAAO,OAAO,CACd,KAAK,SAAS,KAAK,KAAK,CACxB,KAAK,GAAG;AAEb,MAAM,eAAe,YACnB,QAAQ,MACL,OAAO,OAAO,CACd,OAAO,YAAY,CACnB,KAAK,UAAU;CAAE,MAAM;CAAe,QAAQ,MAAM,SAAS,KAAK,KAAK,KAAK,UAAU;CAAE,EAAE;AAI/F,MAAM,eAAe,YAAsC;CACzD,MAAM,OAAO,OAAO,QAAQ;AAC5B,QAAO;EACL,MAAM;EACN,MAAM;EACN,SAAS,CACP,GAAI,KAAK,SAAS,IAAI,CAAC;GAAE,MAAM;GAAc;GAAM,CAAU,GAAG,EAAE,EAClE,GAAG,YAAY,QAAQ,CACxB;EACF;;AAGH,MAAM,YAAY,SAChB,KAAK,SAAS,iBAAkB,KAAK,YAAY,KAAM,KAAK,KAAK,MAAM,EAAe;AAIxF,MAAM,aAAa,SAAqD;CACtE,MAAM,OAAuB;EAC3B,MAAM;EACN,SAAS,KAAK;EACd,MAAM,SAAS,KAAK;EACpB,WAAW,KAAK,UAAU,KAAK,SAAS,EAAE,CAAC;EAC5C;AACD,KAAI,KAAK,UAAU,mBAAoB,QAAO,CAAC,KAAK;CACpD,MAAM,SAAS,OAAO,KAAK,WAAW,WAAW,KAAK,SAAS,KAAK,UAAU,KAAK,OAAO;AAC1F,QAAO,CAAC,MAAM,MAAM,eAAe,KAAK,YAAY,OAAO,CAAC;;AAG9D,MAAM,kBAAkB,YAAyD;CAC/E,MAAM,OAAO,OAAO,QAAQ;AAC5B,QAAO,CACL,GAAI,KAAK,SAAS,IAAI,CAAC,MAAM,cAAc,KAAK,CAAC,GAAG,EAAE,EACtD,GAAG,QAAQ,MAAM,OAAO,OAAO,CAAC,QAAQ,UAAU,CACnD;;AAGH,MAAM,iBAAiB,YACrB,MAAM,MAAM,QAAQ,KAAK,CAAC,KACxB,MAAM,KAAK,cAAc,CAAC,YAAY,QAAQ,CAAC,CAAC,EAChD,MAAM,KAAK,gBAAgB,CAAC,MAAM,WAAW,OAAO,QAAQ,CAAC,CAAC,CAAC,EAC/D,MAAM,KAAK,mBAAmB,eAAe,QAAQ,CAAC,EACtD,MAAM,WACP;AAEH,MAAa,kBACX,aACqC,SAAS,QAAQ,cAAc"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { Stream } from "effect";
|
|
2
|
+
import * as Turn from "@effect-uai/core/Turn";
|
|
3
|
+
import * as SSE from "@effect-uai/core/SSE";
|
|
4
|
+
|
|
5
|
+
//#region src/UIMessageStream.d.ts
|
|
6
|
+
declare namespace UIMessageStream_d_exports {
|
|
7
|
+
export { DataEmission, Emission, MetadataEmission, Part, dataPart, messageMetadata, responseHeaders, toUIMessageStream };
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* The stream parts `useChat` consumes. Names and shapes are the wire
|
|
11
|
+
* contract with the AI SDK client; keep them literal.
|
|
12
|
+
*/
|
|
13
|
+
type Part = {
|
|
14
|
+
readonly type: "start";
|
|
15
|
+
readonly messageId: string;
|
|
16
|
+
} | {
|
|
17
|
+
readonly type: "text-start";
|
|
18
|
+
readonly id: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly type: "text-delta";
|
|
21
|
+
readonly id: string;
|
|
22
|
+
readonly delta: string;
|
|
23
|
+
} | {
|
|
24
|
+
readonly type: "text-end";
|
|
25
|
+
readonly id: string;
|
|
26
|
+
} | {
|
|
27
|
+
readonly type: "reasoning-start";
|
|
28
|
+
readonly id: string;
|
|
29
|
+
} | {
|
|
30
|
+
readonly type: "reasoning-delta";
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly delta: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly type: "reasoning-end";
|
|
35
|
+
readonly id: string;
|
|
36
|
+
} | {
|
|
37
|
+
readonly type: "tool-input-start";
|
|
38
|
+
readonly toolCallId: string;
|
|
39
|
+
readonly toolName: string;
|
|
40
|
+
} | {
|
|
41
|
+
readonly type: "tool-input-delta";
|
|
42
|
+
readonly toolCallId: string;
|
|
43
|
+
readonly inputTextDelta: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly type: "tool-input-available";
|
|
46
|
+
readonly toolCallId: string;
|
|
47
|
+
readonly toolName: string;
|
|
48
|
+
readonly input: unknown;
|
|
49
|
+
} | {
|
|
50
|
+
readonly type: "tool-output-available";
|
|
51
|
+
readonly toolCallId: string;
|
|
52
|
+
readonly output: unknown;
|
|
53
|
+
} | {
|
|
54
|
+
readonly type: `data-${string}`;
|
|
55
|
+
readonly id?: string;
|
|
56
|
+
readonly data: unknown;
|
|
57
|
+
readonly transient?: boolean;
|
|
58
|
+
} | {
|
|
59
|
+
readonly type: "message-metadata";
|
|
60
|
+
readonly messageMetadata: unknown;
|
|
61
|
+
} | {
|
|
62
|
+
readonly type: "error";
|
|
63
|
+
readonly errorText: string;
|
|
64
|
+
} | {
|
|
65
|
+
readonly type: "finish";
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* A custom typed data part (`data-<name>`). `transient: true` delivers it to
|
|
69
|
+
* the client's `onData` callback only (not persisted into `message.parts`);
|
|
70
|
+
* reuse an `id` across emissions to reconcile/update one part in place.
|
|
71
|
+
*/
|
|
72
|
+
type DataEmission = {
|
|
73
|
+
readonly kind: "data";
|
|
74
|
+
readonly name: string;
|
|
75
|
+
readonly data: unknown;
|
|
76
|
+
readonly id?: string;
|
|
77
|
+
readonly transient?: boolean;
|
|
78
|
+
};
|
|
79
|
+
/** Settled per-message metadata, surfaced as `message.metadata` on the client. */
|
|
80
|
+
type MetadataEmission = {
|
|
81
|
+
readonly kind: "metadata";
|
|
82
|
+
readonly metadata: unknown;
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What `toUIMessageStream` encodes: loop `InteractionEvent`s plus optional
|
|
86
|
+
* data / metadata emissions a caller interleaves into the stream.
|
|
87
|
+
*/
|
|
88
|
+
type Emission = Turn.InteractionEvent | DataEmission | MetadataEmission;
|
|
89
|
+
/** Construct a `data-<name>` emission. See {@link DataEmission}. */
|
|
90
|
+
declare const dataPart: (name: string, data: unknown, options?: {
|
|
91
|
+
readonly id?: string;
|
|
92
|
+
readonly transient?: boolean;
|
|
93
|
+
}) => DataEmission;
|
|
94
|
+
/** Construct a message-metadata emission. See {@link MetadataEmission}. */
|
|
95
|
+
declare const messageMetadata: (metadata: unknown) => MetadataEmission;
|
|
96
|
+
/**
|
|
97
|
+
* Headers the UI Message Stream protocol requires on the HTTP response.
|
|
98
|
+
* `x-vercel-ai-ui-message-stream: v1` is mandatory; without it the client
|
|
99
|
+
* falls back to the plain text-stream protocol and ignores non-text parts.
|
|
100
|
+
* Pure data, so it composes with any server (web `Response`, `@effect/platform`
|
|
101
|
+
* `HttpServerResponse`, raw Node) - the package deliberately owns no HTTP layer.
|
|
102
|
+
*/
|
|
103
|
+
declare const responseHeaders: Record<string, string>;
|
|
104
|
+
/**
|
|
105
|
+
* Encode an `InteractionEvent` stream as UI Message Stream `SSE.Event`s,
|
|
106
|
+
* bracketed by the protocol's `start` and terminal `finish` / `[DONE]`
|
|
107
|
+
* markers. `messageId` identifies the assistant message on the client.
|
|
108
|
+
*/
|
|
109
|
+
declare const toUIMessageStream: (messageId: string) => <E, R>(self: Stream.Stream<Emission, E, R>) => Stream.Stream<SSE.Event, E, R>;
|
|
110
|
+
//#endregion
|
|
111
|
+
export { DataEmission, Emission, MetadataEmission, Part, dataPart, messageMetadata, responseHeaders, UIMessageStream_d_exports as t, toUIMessageStream };
|
|
112
|
+
//# sourceMappingURL=UIMessageStream.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UIMessageStream.d.mts","names":[],"sources":["../src/UIMessageStream.ts"],"mappings":";;;;;;;;;;;;KAyBY,IAAA;EAAA,SACG,IAAA;EAAA,SAAwB,SAAA;AAAA;EAAA,SACxB,IAAA;EAAA,SAA6B,EAAA;AAAA;EAAA,SAC7B,IAAA;EAAA,SAA6B,EAAA;EAAA,SAAqB,KAAA;AAAA;EAAA,SAClD,IAAA;EAAA,SAA2B,EAAA;AAAA;EAAA,SAC3B,IAAA;EAAA,SAAkC,EAAA;AAAA;EAAA,SAClC,IAAA;EAAA,SAAkC,EAAA;EAAA,SAAqB,KAAA;AAAA;EAAA,SACvD,IAAA;EAAA,SAAgC,EAAA;AAAA;EAAA,SAChC,IAAA;EAAA,SAAmC,UAAA;EAAA,SAA6B,QAAA;AAAA;EAAA,SAEhE,IAAA;EAAA,SACA,UAAA;EAAA,SACA,cAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,UAAA;EAAA,SACA,QAAA;EAAA,SACA,KAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,UAAA;EAAA,SACA,MAAA;AAAA;EAAA,SAGA,IAAA;EAAA,SACA,EAAA;EAAA,SACA,IAAA;EAAA,SACA,SAAA;AAAA;EAAA,SAEA,IAAA;EAAA,SAAmC,eAAA;AAAA;EAAA,SACnC,IAAA;EAAA,SAAwB,SAAA;AAAA;EAAA,SACxB,IAAA;AAAA;;;;;;KAOH,YAAA;EAAA,SACD,IAAA;EAAA,SACA,IAAA;EAAA,SACA,IAAA;EAAA,SACA,EAAA;EAAA,SACA,SAAA;AAAA;AAgBX;AAAA,KAZY,gBAAA;EAAA,SACD,IAAA;EAAA,SACA,QAAA;AAAA;;;;;KAOC,QAAA,GAAW,IAAA,CAAK,gBAAA,GAAmB,YAAA,GAAe,gBAAA;;cAGjD,QAAA,GACX,IAAA,UACA,IAAA,WACA,OAAA;EAAA,SAAqB,EAAA;EAAA,SAAsB,SAAA;AAAA,MAC1C,YAAA;;cAGU,eAAA,GAAmB,QAAA,cAAoB,gBAAA;AAYpD;;;;;AAgHA;;AAhHA,cAAa,eAAA,EAAiB,MAAA;;;;;;cAgHjB,iBAAA,GACV,SAAA,oBACM,IAAA,EAAM,MAAA,CAAO,MAAA,CAAO,QAAA,EAAU,CAAA,EAAG,CAAA,MAAK,MAAA,CAAO,MAAA,CAAO,GAAA,CAAI,KAAA,EAAO,CAAA,EAAG,CAAA"}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import { t as __exportAll } from "./chunk-CfYAbeIz.mjs";
|
|
2
|
+
import { Match, Option, Stream } from "effect";
|
|
3
|
+
import * as Turn from "@effect-uai/core/Turn";
|
|
4
|
+
//#region src/UIMessageStream.ts
|
|
5
|
+
var UIMessageStream_exports = /* @__PURE__ */ __exportAll({
|
|
6
|
+
dataPart: () => dataPart,
|
|
7
|
+
messageMetadata: () => messageMetadata,
|
|
8
|
+
responseHeaders: () => responseHeaders,
|
|
9
|
+
toUIMessageStream: () => toUIMessageStream
|
|
10
|
+
});
|
|
11
|
+
/** Construct a `data-<name>` emission. See {@link DataEmission}. */
|
|
12
|
+
const dataPart = (name, data, options) => ({
|
|
13
|
+
kind: "data",
|
|
14
|
+
name,
|
|
15
|
+
data,
|
|
16
|
+
...options
|
|
17
|
+
});
|
|
18
|
+
/** Construct a message-metadata emission. See {@link MetadataEmission}. */
|
|
19
|
+
const messageMetadata = (metadata) => ({
|
|
20
|
+
kind: "metadata",
|
|
21
|
+
metadata
|
|
22
|
+
});
|
|
23
|
+
/**
|
|
24
|
+
* Headers the UI Message Stream protocol requires on the HTTP response.
|
|
25
|
+
* `x-vercel-ai-ui-message-stream: v1` is mandatory; without it the client
|
|
26
|
+
* falls back to the plain text-stream protocol and ignores non-text parts.
|
|
27
|
+
* Pure data, so it composes with any server (web `Response`, `@effect/platform`
|
|
28
|
+
* `HttpServerResponse`, raw Node) - the package deliberately owns no HTTP layer.
|
|
29
|
+
*/
|
|
30
|
+
const responseHeaders = {
|
|
31
|
+
"content-type": "text/event-stream",
|
|
32
|
+
"cache-control": "no-cache",
|
|
33
|
+
connection: "keep-alive",
|
|
34
|
+
"x-vercel-ai-ui-message-stream": "v1"
|
|
35
|
+
};
|
|
36
|
+
const parseJson = Option.liftThrowable(JSON.parse);
|
|
37
|
+
const decode = (raw) => Option.getOrElse(parseJson(raw), () => raw);
|
|
38
|
+
const event = (part) => ({ data: part === "[DONE]" ? "[DONE]" : JSON.stringify(part) });
|
|
39
|
+
const toDataPart = (d) => {
|
|
40
|
+
const base = {
|
|
41
|
+
type: `data-${d.name}`,
|
|
42
|
+
data: d.data
|
|
43
|
+
};
|
|
44
|
+
const withId = d.id !== void 0 ? {
|
|
45
|
+
...base,
|
|
46
|
+
id: d.id
|
|
47
|
+
} : base;
|
|
48
|
+
return d.transient !== void 0 ? {
|
|
49
|
+
...withId,
|
|
50
|
+
transient: d.transient
|
|
51
|
+
} : withId;
|
|
52
|
+
};
|
|
53
|
+
const step = (s, ev) => Match.value(ev).pipe(Match.tags({
|
|
54
|
+
TextDelta: (e) => {
|
|
55
|
+
if (s.textId !== null) return [s, [{
|
|
56
|
+
type: "text-delta",
|
|
57
|
+
id: s.textId,
|
|
58
|
+
delta: e.text
|
|
59
|
+
}]];
|
|
60
|
+
const id = `${s.messageId}:t${s.seq}`;
|
|
61
|
+
return [{
|
|
62
|
+
...s,
|
|
63
|
+
textId: id,
|
|
64
|
+
seq: s.seq + 1
|
|
65
|
+
}, [{
|
|
66
|
+
type: "text-start",
|
|
67
|
+
id
|
|
68
|
+
}, {
|
|
69
|
+
type: "text-delta",
|
|
70
|
+
id,
|
|
71
|
+
delta: e.text
|
|
72
|
+
}]];
|
|
73
|
+
},
|
|
74
|
+
ReasoningDelta: (e) => {
|
|
75
|
+
if (s.reasoningId !== null) return [s, [{
|
|
76
|
+
type: "reasoning-delta",
|
|
77
|
+
id: s.reasoningId,
|
|
78
|
+
delta: e.text
|
|
79
|
+
}]];
|
|
80
|
+
const id = `${s.messageId}:r${s.seq}`;
|
|
81
|
+
return [{
|
|
82
|
+
...s,
|
|
83
|
+
reasoningId: id,
|
|
84
|
+
seq: s.seq + 1
|
|
85
|
+
}, [{
|
|
86
|
+
type: "reasoning-start",
|
|
87
|
+
id
|
|
88
|
+
}, {
|
|
89
|
+
type: "reasoning-delta",
|
|
90
|
+
id,
|
|
91
|
+
delta: e.text
|
|
92
|
+
}]];
|
|
93
|
+
},
|
|
94
|
+
ToolCallStart: (e) => [s, [{
|
|
95
|
+
type: "tool-input-start",
|
|
96
|
+
toolCallId: e.call_id,
|
|
97
|
+
toolName: e.name
|
|
98
|
+
}]],
|
|
99
|
+
ToolCallArgsDelta: (e) => [s, [{
|
|
100
|
+
type: "tool-input-delta",
|
|
101
|
+
toolCallId: e.call_id,
|
|
102
|
+
inputTextDelta: e.delta
|
|
103
|
+
}]],
|
|
104
|
+
RefusalDelta: (e) => [s, [{
|
|
105
|
+
type: "error",
|
|
106
|
+
errorText: e.text
|
|
107
|
+
}]],
|
|
108
|
+
UsageUpdate: () => [s, []],
|
|
109
|
+
TurnComplete: (e) => {
|
|
110
|
+
const closing = [
|
|
111
|
+
...s.textId !== null ? [{
|
|
112
|
+
type: "text-end",
|
|
113
|
+
id: s.textId
|
|
114
|
+
}] : [],
|
|
115
|
+
...s.reasoningId !== null ? [{
|
|
116
|
+
type: "reasoning-end",
|
|
117
|
+
id: s.reasoningId
|
|
118
|
+
}] : [],
|
|
119
|
+
...Turn.getToolCalls(e.turn).map((call) => ({
|
|
120
|
+
type: "tool-input-available",
|
|
121
|
+
toolCallId: call.call_id,
|
|
122
|
+
toolName: call.name,
|
|
123
|
+
input: decode(call.arguments)
|
|
124
|
+
}))
|
|
125
|
+
];
|
|
126
|
+
return [{
|
|
127
|
+
...s,
|
|
128
|
+
textId: null,
|
|
129
|
+
reasoningId: null
|
|
130
|
+
}, closing];
|
|
131
|
+
}
|
|
132
|
+
}), Match.when({ type: "function_call_output" }, (o) => [s, [{
|
|
133
|
+
type: "tool-output-available",
|
|
134
|
+
toolCallId: o.call_id,
|
|
135
|
+
output: decode(o.output)
|
|
136
|
+
}]]), Match.when({ kind: "data" }, (d) => [s, [toDataPart(d)]]), Match.when({ kind: "metadata" }, (m) => [s, [{
|
|
137
|
+
type: "message-metadata",
|
|
138
|
+
messageMetadata: m.metadata
|
|
139
|
+
}]]), Match.exhaustive);
|
|
140
|
+
/**
|
|
141
|
+
* Encode an `InteractionEvent` stream as UI Message Stream `SSE.Event`s,
|
|
142
|
+
* bracketed by the protocol's `start` and terminal `finish` / `[DONE]`
|
|
143
|
+
* markers. `messageId` identifies the assistant message on the client.
|
|
144
|
+
*/
|
|
145
|
+
const toUIMessageStream = (messageId) => (self) => Stream.make(event({
|
|
146
|
+
type: "start",
|
|
147
|
+
messageId
|
|
148
|
+
})).pipe(Stream.concat(Stream.mapAccum(self, () => ({
|
|
149
|
+
messageId,
|
|
150
|
+
textId: null,
|
|
151
|
+
reasoningId: null,
|
|
152
|
+
seq: 0
|
|
153
|
+
}), (s, ev) => {
|
|
154
|
+
const [next, parts] = step(s, ev);
|
|
155
|
+
return [next, parts.map(event)];
|
|
156
|
+
})), Stream.concat(Stream.make(event({ type: "finish" }), event("[DONE]"))));
|
|
157
|
+
//#endregion
|
|
158
|
+
export { dataPart, messageMetadata, responseHeaders, UIMessageStream_exports as t, toUIMessageStream };
|
|
159
|
+
|
|
160
|
+
//# sourceMappingURL=UIMessageStream.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UIMessageStream.mjs","names":[],"sources":["../src/UIMessageStream.ts"],"sourcesContent":["/**\n * Project an effect-uai loop onto the Vercel AI SDK \"UI Message Stream\"\n * protocol (the `v1` wire format, stable across AI SDK v5–v7). The output is a\n * `Stream<SSE.Event>` ready for `SSE.toBytes`; a `useChat` client on the other\n * end consumes it with no frontend changes.\n *\n * The encoder input is an `Emission`: every `InteractionEvent` the loop\n * produces, plus optional `dataPart` and `messageMetadata` emissions a caller\n * interleaves to stream custom typed data (e.g. live metrics) and settled\n * per-message metadata (e.g. usage). A plain `Stream<InteractionEvent>` is a\n * valid `Stream<Emission>`, so existing callers are unaffected.\n *\n * The protocol wants a stable id and explicit start/end lifecycle around each\n * text and reasoning block, which effect-uai's delta events don't carry. This\n * module synthesizes them: one block id per turn, opened on the first delta\n * and closed on `TurnComplete`.\n */\nimport type * as SSE from \"@effect-uai/core/SSE\"\nimport * as Turn from \"@effect-uai/core/Turn\"\nimport { Match, Option, Stream } from \"effect\"\n\n/**\n * The stream parts `useChat` consumes. Names and shapes are the wire\n * contract with the AI SDK client; keep them literal.\n */\nexport type Part =\n | { readonly type: \"start\"; readonly messageId: string }\n | { readonly type: \"text-start\"; readonly id: string }\n | { readonly type: \"text-delta\"; readonly id: string; readonly delta: string }\n | { readonly type: \"text-end\"; readonly id: string }\n | { readonly type: \"reasoning-start\"; readonly id: string }\n | { readonly type: \"reasoning-delta\"; readonly id: string; readonly delta: string }\n | { readonly type: \"reasoning-end\"; readonly id: string }\n | { readonly type: \"tool-input-start\"; readonly toolCallId: string; readonly toolName: string }\n | {\n readonly type: \"tool-input-delta\"\n readonly toolCallId: string\n readonly inputTextDelta: string\n }\n | {\n readonly type: \"tool-input-available\"\n readonly toolCallId: string\n readonly toolName: string\n readonly input: unknown\n }\n | {\n readonly type: \"tool-output-available\"\n readonly toolCallId: string\n readonly output: unknown\n }\n | {\n readonly type: `data-${string}`\n readonly id?: string\n readonly data: unknown\n readonly transient?: boolean\n }\n | { readonly type: \"message-metadata\"; readonly messageMetadata: unknown }\n | { readonly type: \"error\"; readonly errorText: string }\n | { readonly type: \"finish\" }\n\n/**\n * A custom typed data part (`data-<name>`). `transient: true` delivers it to\n * the client's `onData` callback only (not persisted into `message.parts`);\n * reuse an `id` across emissions to reconcile/update one part in place.\n */\nexport type DataEmission = {\n readonly kind: \"data\"\n readonly name: string\n readonly data: unknown\n readonly id?: string\n readonly transient?: boolean\n}\n\n/** Settled per-message metadata, surfaced as `message.metadata` on the client. */\nexport type MetadataEmission = {\n readonly kind: \"metadata\"\n readonly metadata: unknown\n}\n\n/**\n * What `toUIMessageStream` encodes: loop `InteractionEvent`s plus optional\n * data / metadata emissions a caller interleaves into the stream.\n */\nexport type Emission = Turn.InteractionEvent | DataEmission | MetadataEmission\n\n/** Construct a `data-<name>` emission. See {@link DataEmission}. */\nexport const dataPart = (\n name: string,\n data: unknown,\n options?: { readonly id?: string; readonly transient?: boolean },\n): DataEmission => ({ kind: \"data\", name, data, ...options })\n\n/** Construct a message-metadata emission. See {@link MetadataEmission}. */\nexport const messageMetadata = (metadata: unknown): MetadataEmission => ({\n kind: \"metadata\",\n metadata,\n})\n\n/**\n * Headers the UI Message Stream protocol requires on the HTTP response.\n * `x-vercel-ai-ui-message-stream: v1` is mandatory; without it the client\n * falls back to the plain text-stream protocol and ignores non-text parts.\n * Pure data, so it composes with any server (web `Response`, `@effect/platform`\n * `HttpServerResponse`, raw Node) - the package deliberately owns no HTTP layer.\n */\nexport const responseHeaders: Record<string, string> = {\n \"content-type\": \"text/event-stream\",\n \"cache-control\": \"no-cache\",\n connection: \"keep-alive\",\n \"x-vercel-ai-ui-message-stream\": \"v1\",\n}\n\nconst parseJson = Option.liftThrowable(JSON.parse)\n\n// Tool arguments/output cross effect-uai as JSON strings; hand the client the\n// decoded value, falling back to the raw string when it isn't JSON.\nconst decode = (raw: string): unknown => Option.getOrElse(parseJson(raw), () => raw)\n\nconst event = (part: Part | \"[DONE]\"): SSE.Event => ({\n data: part === \"[DONE]\" ? \"[DONE]\" : JSON.stringify(part),\n})\n\ntype State = {\n readonly messageId: string\n readonly textId: string | null\n readonly reasoningId: string | null\n readonly seq: number\n}\n\ntype Step = readonly [State, ReadonlyArray<Part>]\n\nconst toDataPart = (d: DataEmission): Part => {\n const base: Part = { type: `data-${d.name}`, data: d.data }\n const withId = d.id !== undefined ? { ...base, id: d.id } : base\n return d.transient !== undefined ? { ...withId, transient: d.transient } : withId\n}\n\n// One emission fans out to zero or more parts (a first TextDelta opens a block\n// *and* emits its delta), so each step yields an array.\nconst step = (s: State, ev: Emission): Step =>\n Match.value(ev).pipe(\n Match.tags({\n TextDelta: (e): Step => {\n if (s.textId !== null) return [s, [{ type: \"text-delta\", id: s.textId, delta: e.text }]]\n const id = `${s.messageId}:t${s.seq}`\n return [\n { ...s, textId: id, seq: s.seq + 1 },\n [\n { type: \"text-start\", id },\n { type: \"text-delta\", id, delta: e.text },\n ],\n ]\n },\n ReasoningDelta: (e): Step => {\n if (s.reasoningId !== null)\n return [s, [{ type: \"reasoning-delta\", id: s.reasoningId, delta: e.text }]]\n const id = `${s.messageId}:r${s.seq}`\n return [\n { ...s, reasoningId: id, seq: s.seq + 1 },\n [\n { type: \"reasoning-start\", id },\n { type: \"reasoning-delta\", id, delta: e.text },\n ],\n ]\n },\n ToolCallStart: (e): Step => [\n s,\n [{ type: \"tool-input-start\", toolCallId: e.call_id, toolName: e.name }],\n ],\n ToolCallArgsDelta: (e): Step => [\n s,\n [{ type: \"tool-input-delta\", toolCallId: e.call_id, inputTextDelta: e.delta }],\n ],\n RefusalDelta: (e): Step => [s, [{ type: \"error\", errorText: e.text }]],\n UsageUpdate: (): Step => [s, []],\n TurnComplete: (e): Step => {\n const closing: ReadonlyArray<Part> = [\n ...(s.textId !== null ? [{ type: \"text-end\", id: s.textId } as const] : []),\n ...(s.reasoningId !== null\n ? [{ type: \"reasoning-end\", id: s.reasoningId } as const]\n : []),\n ...Turn.getToolCalls(e.turn).map(\n (call): Part => ({\n type: \"tool-input-available\",\n toolCallId: call.call_id,\n toolName: call.name,\n input: decode(call.arguments),\n }),\n ),\n ]\n // Reset block ids; the loop may run further turns after tools resolve.\n return [{ ...s, textId: null, reasoningId: null }, closing]\n },\n }),\n // The non-`_tag` arm of InteractionEvent: a resolved tool result the loop\n // appended after running the tool.\n Match.when(\n { type: \"function_call_output\" },\n (o): Step => [\n s,\n [{ type: \"tool-output-available\", toolCallId: o.call_id, output: decode(o.output) }],\n ],\n ),\n // Caller-interleaved emissions.\n Match.when({ kind: \"data\" }, (d): Step => [s, [toDataPart(d)]]),\n Match.when(\n { kind: \"metadata\" },\n (m): Step => [s, [{ type: \"message-metadata\", messageMetadata: m.metadata }]],\n ),\n Match.exhaustive,\n )\n\n/**\n * Encode an `InteractionEvent` stream as UI Message Stream `SSE.Event`s,\n * bracketed by the protocol's `start` and terminal `finish` / `[DONE]`\n * markers. `messageId` identifies the assistant message on the client.\n */\nexport const toUIMessageStream =\n (messageId: string) =>\n <E, R>(self: Stream.Stream<Emission, E, R>): Stream.Stream<SSE.Event, E, R> =>\n Stream.make(event({ type: \"start\", messageId })).pipe(\n Stream.concat(\n Stream.mapAccum(\n self,\n (): State => ({ messageId, textId: null, reasoningId: null, seq: 0 }),\n (s, ev: Emission): readonly [State, ReadonlyArray<SSE.Event>] => {\n const [next, parts] = step(s, ev)\n return [next, parts.map(event)]\n },\n ),\n ),\n Stream.concat(Stream.make(event({ type: \"finish\" }), event(\"[DONE]\"))),\n )\n"],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|