@effect-uai/responses 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.
@@ -0,0 +1,199 @@
1
+ import { Match, Schema } from "effect"
2
+ import { matchType } from "@effect-uai/core/Match"
3
+ import type { TurnEvent } from "@effect-uai/core/Turn"
4
+ import { WireOutputItem, WireResponseCompleted, turnFromCompleted } from "./codec.js"
5
+
6
+ // ---------------------------------------------------------------------------
7
+ // Schemas for the SSE event payloads we care about. The Responses API ships
8
+ // many event types; we model only the ones that map to a `TurnEvent`. All
9
+ // others are silently ignored at the dispatch site.
10
+ // ---------------------------------------------------------------------------
11
+
12
+ const OutputItemAdded = Schema.Struct({
13
+ type: Schema.Literal("response.output_item.added"),
14
+ item: WireOutputItem,
15
+ })
16
+
17
+ const OutputTextDelta = Schema.Struct({
18
+ type: Schema.Literal("response.output_text.delta"),
19
+ delta: Schema.String,
20
+ })
21
+
22
+ const FunctionArgsDelta = Schema.Struct({
23
+ type: Schema.Literal("response.function_call_arguments.delta"),
24
+ item_id: Schema.String,
25
+ delta: Schema.String,
26
+ })
27
+
28
+ const ReasoningTextDelta = Schema.Struct({
29
+ type: Schema.Literal("response.reasoning_text.delta"),
30
+ delta: Schema.String,
31
+ })
32
+
33
+ const ReasoningSummaryDelta = Schema.Struct({
34
+ type: Schema.Literal("response.reasoning_summary_text.delta"),
35
+ delta: Schema.String,
36
+ })
37
+
38
+ const RefusalDelta = Schema.Struct({
39
+ type: Schema.Literal("response.refusal.delta"),
40
+ delta: Schema.String,
41
+ })
42
+
43
+ const RefusalDone = Schema.Struct({
44
+ type: Schema.Literal("response.refusal.done"),
45
+ refusal: Schema.optional(Schema.String),
46
+ })
47
+
48
+ const Completed = Schema.Struct({
49
+ type: Schema.Literal("response.completed"),
50
+ response: WireResponseCompleted,
51
+ })
52
+
53
+ const Incomplete = Schema.Struct({
54
+ type: Schema.Literal("response.incomplete"),
55
+ response: WireResponseCompleted,
56
+ })
57
+
58
+ const Failed = Schema.Struct({
59
+ type: Schema.Literal("response.failed"),
60
+ response: WireResponseCompleted,
61
+ })
62
+
63
+ const ErrorEvent = Schema.Struct({
64
+ type: Schema.Literal("error"),
65
+ message: Schema.optional(Schema.String),
66
+ code: Schema.optional(Schema.String),
67
+ })
68
+
69
+ /**
70
+ * Catch-all variant for wire events that fail to decode against any known
71
+ * schema, plus events that fail to JSON-parse. The decoder never produces
72
+ * this directly - it's synthesized by `sseEventToProviderEvent` when
73
+ * `decodeKnown` fails.
74
+ */
75
+ const Unknown = Schema.Struct({
76
+ type: Schema.Literal("_unknown"),
77
+ raw: Schema.Unknown,
78
+ })
79
+
80
+ /**
81
+ * Internal: union of variants we actually know how to decode from the wire.
82
+ * Used as the decode target; failures are caught and re-emitted as `Unknown`.
83
+ */
84
+ export const KnownProviderEvent = Schema.Union([
85
+ OutputItemAdded,
86
+ OutputTextDelta,
87
+ FunctionArgsDelta,
88
+ ReasoningTextDelta,
89
+ ReasoningSummaryDelta,
90
+ RefusalDelta,
91
+ RefusalDone,
92
+ Completed,
93
+ Incomplete,
94
+ Failed,
95
+ ErrorEvent,
96
+ ])
97
+
98
+ /**
99
+ * Public: every event the native stream can emit. Discriminated on `type`.
100
+ * The `_unknown` branch closes the cardinality so downstream `Match.exhaustive`
101
+ * cannot silently miss a wire event we didn't model.
102
+ */
103
+ export const ProviderEvent = Schema.Union([
104
+ OutputItemAdded,
105
+ OutputTextDelta,
106
+ FunctionArgsDelta,
107
+ ReasoningTextDelta,
108
+ ReasoningSummaryDelta,
109
+ RefusalDelta,
110
+ RefusalDone,
111
+ Completed,
112
+ Incomplete,
113
+ Failed,
114
+ ErrorEvent,
115
+ Unknown,
116
+ ])
117
+ export type ProviderEvent = typeof ProviderEvent.Type
118
+
119
+ // `call_id` lookup for `function_call_arguments.delta`. The SSE event
120
+ // references the *item id*, not the call id; we keep a small map populated
121
+ // from the `output_item.added` events.
122
+ export interface CallIdLookup {
123
+ readonly resolve: (itemId: string) => string | undefined
124
+ readonly remember: (itemId: string, callId: string) => void
125
+ }
126
+
127
+ export const makeCallIdLookup = (): CallIdLookup => {
128
+ const map = new Map<string, string>()
129
+ return {
130
+ resolve: (id) => map.get(id),
131
+ remember: (id, callId) => {
132
+ map.set(id, callId)
133
+ },
134
+ }
135
+ }
136
+
137
+ /**
138
+ * Translate a decoded provider event into zero-or-more `TurnEvent`s.
139
+ * Mutates `lookup` to record `(item_id → call_id)` from `output_item.added`
140
+ * so subsequent `function_call_arguments.delta` events can be tagged with
141
+ * the right call id.
142
+ */
143
+ export const eventToDeltas = (
144
+ event: ProviderEvent,
145
+ lookup: CallIdLookup,
146
+ ): ReadonlyArray<TurnEvent> =>
147
+ Match.value(event).pipe(
148
+ matchType("response.output_item.added", ({ item }) => {
149
+ if (item.type !== "function_call") return []
150
+ if (item.id !== undefined) lookup.remember(item.id, item.call_id)
151
+ return [
152
+ {
153
+ type: "tool_call_start" as const,
154
+ call_id: item.call_id,
155
+ name: item.name,
156
+ },
157
+ ]
158
+ }),
159
+ matchType("response.output_text.delta", ({ delta }) => [
160
+ { type: "text_delta" as const, text: delta },
161
+ ]),
162
+ matchType("response.function_call_arguments.delta", ({ item_id, delta }) => {
163
+ const call_id = lookup.resolve(item_id)
164
+ return call_id === undefined
165
+ ? []
166
+ : [{ type: "tool_call_args_delta" as const, call_id, delta }]
167
+ }),
168
+ matchType("response.reasoning_text.delta", ({ delta }) => [
169
+ { type: "reasoning_delta" as const, text: delta, kind: "trace" as const },
170
+ ]),
171
+ matchType("response.reasoning_summary_text.delta", ({ delta }) => [
172
+ { type: "reasoning_delta" as const, text: delta, kind: "summary" as const },
173
+ ]),
174
+ matchType("response.refusal.delta", ({ delta }) => [
175
+ { type: "refusal_delta" as const, text: delta },
176
+ ]),
177
+ // Terminal marker for the refusal stream; the deltas already covered
178
+ // the text, so no canonical event is emitted here.
179
+ matchType("response.refusal.done", () => []),
180
+ matchType("response.completed", ({ response }) => [
181
+ { type: "turn_complete" as const, turn: turnFromCompleted(response) },
182
+ ]),
183
+ // Incomplete = the model stopped early but produced a usable partial
184
+ // turn (max_tokens, content_filter, max_tool_calls, refusal). Same
185
+ // payload shape as `response.completed`; the stop_reason carries the
186
+ // reason.
187
+ matchType("response.incomplete", ({ response }) => [
188
+ { type: "turn_complete" as const, turn: turnFromCompleted(response) },
189
+ ]),
190
+ // Failed and error are surfaced separately as `AiError` by the stream
191
+ // pipeline, not as canonical events.
192
+ matchType("response.failed", () => []),
193
+ matchType("error", () => []),
194
+ // No silent drops: unknown wire events flow through `streamNative` but
195
+ // produce no canonical delta. Step 3 (canonical `other` event) replaces
196
+ // this with a forwarded `other` delta on `TurnEvent`.
197
+ matchType("_unknown", () => []),
198
+ Match.exhaustive,
199
+ )