@effect-uai/ai-sdk 0.18.0 → 0.19.0-dev-20261109183959
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/dist/Messages.d.mts.map +1 -1
- package/dist/Messages.mjs +11 -3
- package/dist/Messages.mjs.map +1 -1
- package/dist/UIMessageStream.d.mts +4 -0
- package/dist/UIMessageStream.d.mts.map +1 -1
- package/dist/UIMessageStream.mjs +34 -5
- package/dist/UIMessageStream.mjs.map +1 -1
- package/dist/UIMessageStream.test.mjs +46 -3
- package/dist/UIMessageStream.test.mjs.map +1 -1
- package/dist/conformance.test.mjs +6 -2
- package/dist/conformance.test.mjs.map +1 -1
- package/dist/{dist-Cu2gIMHV.mjs → dist-DvYz71Uo.mjs} +51 -32
- package/dist/{dist-Cu2gIMHV.mjs.map → dist-DvYz71Uo.mjs.map} +1 -1
- package/package.json +4 -4
- package/src/Messages.ts +14 -3
- package/src/UIMessageStream.test.ts +36 -2
- package/src/UIMessageStream.ts +43 -5
- package/src/conformance.test.ts +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@effect-uai/ai-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0-dev-20261109183959",
|
|
4
4
|
"description": "Vercel AI SDK (useChat) UI Message Stream compatibility for @effect-uai/core.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agents",
|
|
@@ -51,12 +51,12 @@
|
|
|
51
51
|
"dependencies": {},
|
|
52
52
|
"devDependencies": {
|
|
53
53
|
"ai": "^7.0.3",
|
|
54
|
-
"effect": "4.
|
|
54
|
+
"effect": "4.1.2",
|
|
55
55
|
"typescript": "^7.0.2",
|
|
56
|
-
"@effect-uai/core": "0.
|
|
56
|
+
"@effect-uai/core": "0.19.0-dev-20261109183959"
|
|
57
57
|
},
|
|
58
58
|
"peerDependencies": {
|
|
59
|
-
"@effect-uai/core": "
|
|
59
|
+
"@effect-uai/core": "0.19.0-dev-20261109183959",
|
|
60
60
|
"effect": ">=4.0.1 <5.0.0"
|
|
61
61
|
},
|
|
62
62
|
"scripts": {
|
package/src/Messages.ts
CHANGED
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import * as Image from "@effect-uai/core/Image"
|
|
13
13
|
import * as Items from "@effect-uai/core/Items"
|
|
14
|
-
import { Match, Schema } from "effect"
|
|
14
|
+
import { Match, Option, Predicate, Schema } from "effect"
|
|
15
|
+
|
|
16
|
+
const asJson = Schema.decodeUnknownOption(Schema.Json)
|
|
15
17
|
|
|
16
18
|
// The inbound part variants we understand. Tool parts are identified
|
|
17
19
|
// structurally by `toolCallId` - their wire `type` is a dynamic `tool-<name>`
|
|
@@ -97,8 +99,17 @@ const toolItems = (part: ToolPart): ReadonlyArray<Items.HistoryItem> => {
|
|
|
97
99
|
arguments: JSON.stringify(part.input ?? {}),
|
|
98
100
|
}
|
|
99
101
|
if (part.state !== "output-available") return [call]
|
|
100
|
-
|
|
101
|
-
|
|
102
|
+
// Text stays text; any other JSON stays a value (the provider codec serializes it).
|
|
103
|
+
const output: Items.ToolOutputContent = Match.value(part.output).pipe(
|
|
104
|
+
Match.when(Predicate.isString, (text) => Items.text(text)),
|
|
105
|
+
Match.orElse((value) =>
|
|
106
|
+
Option.match(asJson(value), {
|
|
107
|
+
onNone: () => Items.text(String(value)),
|
|
108
|
+
onSome: (json) => Items.json(json),
|
|
109
|
+
}),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
return [call, { type: "function_call_output", call_id: part.toolCallId, output: [output] }]
|
|
102
113
|
}
|
|
103
114
|
|
|
104
115
|
const assistantItems = (message: UIMessage): ReadonlyArray<Items.HistoryItem> => {
|
|
@@ -63,7 +63,7 @@ describe("toUIMessageStream", () => {
|
|
|
63
63
|
Turn.TurnEvent.ToolCallStart({ call_id: "c1", name: "search" }),
|
|
64
64
|
Turn.TurnEvent.ToolCallArgsDelta({ call_id: "c1", delta: `{"q":"lisbon"}` }),
|
|
65
65
|
Turn.TurnEvent.TurnComplete({ turn: turn([call], "tool_calls") }),
|
|
66
|
-
|
|
66
|
+
{ type: "function_call_output", call_id: "c1", output: [Items.json({ hits: 3 })] },
|
|
67
67
|
])
|
|
68
68
|
|
|
69
69
|
expect(types(parts)).toEqual([
|
|
@@ -90,6 +90,39 @@ describe("toUIMessageStream", () => {
|
|
|
90
90
|
}),
|
|
91
91
|
)
|
|
92
92
|
|
|
93
|
+
it.effect("maps a failed call to tool-output-error and media to a data URL", () =>
|
|
94
|
+
Effect.gen(function* () {
|
|
95
|
+
const parts = yield* encode([
|
|
96
|
+
{
|
|
97
|
+
type: "function_call_output",
|
|
98
|
+
call_id: "c1",
|
|
99
|
+
output: [Items.json({ _tag: "CityNotFound", city: "Atlantis" })],
|
|
100
|
+
isError: true,
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
type: "function_call_output",
|
|
104
|
+
call_id: "c2",
|
|
105
|
+
output: [
|
|
106
|
+
Items.text("Home"),
|
|
107
|
+
Items.image({ _tag: "bytes", bytes: new Uint8Array([1, 2, 3]), mimeType: "image/png" }),
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
])
|
|
111
|
+
expect(parts.slice(1, 3)).toEqual([
|
|
112
|
+
{
|
|
113
|
+
type: "tool-output-error",
|
|
114
|
+
toolCallId: "c1",
|
|
115
|
+
errorText: '{"_tag":"CityNotFound","city":"Atlantis"}',
|
|
116
|
+
},
|
|
117
|
+
{
|
|
118
|
+
type: "tool-output-available",
|
|
119
|
+
toolCallId: "c2",
|
|
120
|
+
output: ["Home", { type: "image", url: "data:image/png;base64,AQID" }],
|
|
121
|
+
},
|
|
122
|
+
])
|
|
123
|
+
}),
|
|
124
|
+
)
|
|
125
|
+
|
|
93
126
|
it.effect("surfaces a refusal as an error part", () =>
|
|
94
127
|
Effect.gen(function* () {
|
|
95
128
|
const parts = yield* encode([Turn.TurnEvent.RefusalDelta({ text: "cannot help" })])
|
|
@@ -192,7 +225,8 @@ describe("decodeMessages", () => {
|
|
|
192
225
|
expect(items).toEqual([
|
|
193
226
|
Items.assistantText("Here"),
|
|
194
227
|
{ type: "function_call", call_id: "c1", name: "search", arguments: `{"q":"lisbon"}` },
|
|
195
|
-
|
|
228
|
+
// The output stays a value; the provider codec serializes it.
|
|
229
|
+
{ type: "function_call_output", call_id: "c1", output: [Items.json({ hits: 3 })] },
|
|
196
230
|
])
|
|
197
231
|
})
|
|
198
232
|
|
package/src/UIMessageStream.ts
CHANGED
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
* module synthesizes them: one block id per turn, opened on the first delta
|
|
16
16
|
* and closed on `TurnComplete`.
|
|
17
17
|
*/
|
|
18
|
+
import type { FileSource } from "@effect-uai/core/File"
|
|
19
|
+
import type { ImageSource } from "@effect-uai/core/Image"
|
|
20
|
+
import type * as Items from "@effect-uai/core/Items"
|
|
18
21
|
import type * as SSE from "@effect-uai/core/SSE"
|
|
22
|
+
import * as ToolOutput from "@effect-uai/core/ToolOutput"
|
|
19
23
|
import * as Turn from "@effect-uai/core/Turn"
|
|
20
24
|
import { Match, Option, Stream } from "effect"
|
|
25
|
+
import { Base64 } from "effect/encoding"
|
|
21
26
|
|
|
22
27
|
/**
|
|
23
28
|
* The stream parts `useChat` consumes. Names and shapes are the wire
|
|
@@ -48,6 +53,7 @@ export type Part =
|
|
|
48
53
|
readonly toolCallId: string
|
|
49
54
|
readonly output: unknown
|
|
50
55
|
}
|
|
56
|
+
| { readonly type: "tool-output-error"; readonly toolCallId: string; readonly errorText: string }
|
|
51
57
|
| {
|
|
52
58
|
readonly type: `data-${string}`
|
|
53
59
|
readonly id?: string
|
|
@@ -112,10 +118,45 @@ export const responseHeaders: Record<string, string> = {
|
|
|
112
118
|
|
|
113
119
|
const parseJson = Option.liftThrowable(JSON.parse)
|
|
114
120
|
|
|
115
|
-
// Tool arguments
|
|
121
|
+
// Tool arguments cross effect-uai as a JSON string; hand the client the
|
|
116
122
|
// decoded value, falling back to the raw string when it isn't JSON.
|
|
117
123
|
const decode = (raw: string): unknown => Option.getOrElse(parseJson(raw), () => raw)
|
|
118
124
|
|
|
125
|
+
// Media goes to the client as a URL; inline bytes as a data URL.
|
|
126
|
+
const sourceUrl = Match.type<ImageSource | FileSource>().pipe(
|
|
127
|
+
Match.tagsExhaustive({
|
|
128
|
+
url: (s) => s.url,
|
|
129
|
+
base64: (s) => `data:${s.mimeType};base64,${s.base64}`,
|
|
130
|
+
bytes: (s) => `data:${s.mimeType};base64,${Base64.encode(s.bytes)}`,
|
|
131
|
+
}),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
const partJson = Match.type<Items.ToolOutputContent>().pipe(
|
|
135
|
+
Match.discriminatorsExhaustive("type")({
|
|
136
|
+
input_text: (p): unknown => p.text,
|
|
137
|
+
input_json: (p) => p.value,
|
|
138
|
+
input_image: (p) => ({ type: "image", url: sourceUrl(p.source) }),
|
|
139
|
+
input_file: (p) => ({
|
|
140
|
+
type: "file",
|
|
141
|
+
url: sourceUrl(p.source),
|
|
142
|
+
...(p.filename !== undefined && { filename: p.filename }),
|
|
143
|
+
}),
|
|
144
|
+
}),
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
const outputJson = (o: Items.ToolCallOutput): unknown => ToolOutput.single(o.output.map(partJson))
|
|
148
|
+
|
|
149
|
+
const toolOutputPart = (o: Items.ToolCallOutput): Part => {
|
|
150
|
+
const output = outputJson(o)
|
|
151
|
+
return o.isError === true
|
|
152
|
+
? {
|
|
153
|
+
type: "tool-output-error",
|
|
154
|
+
toolCallId: o.call_id,
|
|
155
|
+
errorText: typeof output === "string" ? output : JSON.stringify(output),
|
|
156
|
+
}
|
|
157
|
+
: { type: "tool-output-available", toolCallId: o.call_id, output }
|
|
158
|
+
}
|
|
159
|
+
|
|
119
160
|
const event = (part: Part | "[DONE]"): SSE.Event => ({
|
|
120
161
|
data: part === "[DONE]" ? "[DONE]" : JSON.stringify(part),
|
|
121
162
|
})
|
|
@@ -199,10 +240,7 @@ const step = (s: State, ev: Emission): Step =>
|
|
|
199
240
|
}),
|
|
200
241
|
// The non-`_tag` arm of InteractionEvent: a resolved tool result the loop
|
|
201
242
|
// appended after running the tool.
|
|
202
|
-
Match.when({ type: "function_call_output" }, (o): Step => [
|
|
203
|
-
s,
|
|
204
|
-
[{ type: "tool-output-available", toolCallId: o.call_id, output: decode(o.output) }],
|
|
205
|
-
]),
|
|
243
|
+
Match.when({ type: "function_call_output" }, (o): Step => [s, [toolOutputPart(o)]]),
|
|
206
244
|
// Caller-interleaved emissions.
|
|
207
245
|
Match.when({ kind: "data" }, (d): Step => [s, [toDataPart(d)]]),
|
|
208
246
|
Match.when({ kind: "metadata" }, (m): Step => [
|
package/src/conformance.test.ts
CHANGED
|
@@ -76,7 +76,7 @@ describe("AI SDK conformance (readUIMessageStream)", () => {
|
|
|
76
76
|
Turn.TurnEvent.ToolCallStart({ call_id: "c1", name: "search" }),
|
|
77
77
|
Turn.TurnEvent.ToolCallArgsDelta({ call_id: "c1", delta: `{"q":"lisbon"}` }),
|
|
78
78
|
Turn.TurnEvent.TurnComplete({ turn: turn([call], "tool_calls") }),
|
|
79
|
-
|
|
79
|
+
{ type: "function_call_output", call_id: "c1", output: [Items.json({ hits: 3 })] },
|
|
80
80
|
])
|
|
81
81
|
|
|
82
82
|
const tool = Option.flatMap(message, (m) => Arr.findFirst(m.parts, isToolUIPart))
|