@effect-uai/core 0.9.0 → 0.11.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.
Files changed (196) hide show
  1. package/dist/{AiError-CrdR1EQi.d.mts → AiError-Bf1pq4BB.d.mts} +8 -3
  2. package/dist/{AiError-CrdR1EQi.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
  3. package/dist/{Audio-DDSKCkSe.d.mts → Audio-CR7CSljJ.d.mts} +2 -2
  4. package/dist/{Audio-DDSKCkSe.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
  5. package/dist/{Image-C_Zjpwhc.d.mts → Image-C4vuBmu0.d.mts} +12 -12
  6. package/dist/{Image-C_Zjpwhc.d.mts.map → Image-C4vuBmu0.d.mts.map} +1 -1
  7. package/dist/{Items-CgKD6Npw.d.mts → Items-BWNu6VGp.d.mts} +118 -92
  8. package/dist/Items-BWNu6VGp.d.mts.map +1 -0
  9. package/dist/{Loop-B5_cqumi.d.mts → Loop-D8nWpowc.d.mts} +2 -2
  10. package/dist/{Loop-B5_cqumi.d.mts.map → Loop-D8nWpowc.d.mts.map} +1 -1
  11. package/dist/{Media-DOLyaygI.d.mts → Media-D_CpcM1Z.d.mts} +1 -1
  12. package/dist/{Media-DOLyaygI.d.mts.map → Media-D_CpcM1Z.d.mts.map} +1 -1
  13. package/dist/{StructuredFormat-CMJFD3sn.d.mts → StructuredFormat-DHaUJBdd.d.mts} +1 -1
  14. package/dist/{StructuredFormat-CMJFD3sn.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
  15. package/dist/{Tool-0XUIT2A2.d.mts → Tool-CXFHCQrT.d.mts} +101 -16
  16. package/dist/Tool-CXFHCQrT.d.mts.map +1 -0
  17. package/dist/{ToolEvent-DYrAUJGs.d.mts → ToolEvent-nVijCrvm.d.mts} +2 -2
  18. package/dist/ToolEvent-nVijCrvm.d.mts.map +1 -0
  19. package/dist/{ToolResult-BZd-cEWW.d.mts → ToolResult-CQqKkcDa.d.mts} +10 -4
  20. package/dist/ToolResult-CQqKkcDa.d.mts.map +1 -0
  21. package/dist/{Toolkit-BKC8b8Xo.d.mts → Toolkit-BYPwwu-j.d.mts} +65 -24
  22. package/dist/Toolkit-BYPwwu-j.d.mts.map +1 -0
  23. package/dist/browser/Browser.d.mts +190 -0
  24. package/dist/browser/Browser.d.mts.map +1 -0
  25. package/dist/browser/Browser.mjs +34 -0
  26. package/dist/browser/Browser.mjs.map +1 -0
  27. package/dist/browser/BrowserError.d.mts +141 -0
  28. package/dist/browser/BrowserError.d.mts.map +1 -0
  29. package/dist/browser/BrowserError.mjs +89 -0
  30. package/dist/browser/BrowserError.mjs.map +1 -0
  31. package/dist/browser/BrowserTool.d.mts +59 -0
  32. package/dist/browser/BrowserTool.d.mts.map +1 -0
  33. package/dist/browser/BrowserTool.mjs +86 -0
  34. package/dist/browser/BrowserTool.mjs.map +1 -0
  35. package/dist/{dist-DWgRbDN3.mjs → dist-C0m0QnUP.mjs} +71 -30
  36. package/dist/dist-C0m0QnUP.mjs.map +1 -0
  37. package/dist/{dist-B04dlFHg.mjs → dist-CcRV3-A5.mjs} +8 -8
  38. package/dist/{dist-B04dlFHg.mjs.map → dist-CcRV3-A5.mjs.map} +1 -1
  39. package/dist/domain/AiError.d.mts +2 -2
  40. package/dist/domain/AiError.mjs +22 -3
  41. package/dist/domain/AiError.mjs.map +1 -1
  42. package/dist/domain/Audio.d.mts +1 -1
  43. package/dist/domain/Citation.d.mts +60 -0
  44. package/dist/domain/Citation.d.mts.map +1 -0
  45. package/dist/domain/Citation.mjs +7 -0
  46. package/dist/domain/Citation.mjs.map +1 -0
  47. package/dist/domain/Image.d.mts +1 -1
  48. package/dist/domain/Items.d.mts +1 -1
  49. package/dist/domain/Items.mjs +13 -3
  50. package/dist/domain/Items.mjs.map +1 -1
  51. package/dist/domain/Media.d.mts +1 -1
  52. package/dist/domain/Music.d.mts +1 -1
  53. package/dist/domain/Research.d.mts +37 -0
  54. package/dist/domain/Research.d.mts.map +1 -0
  55. package/dist/domain/Research.mjs +7 -0
  56. package/dist/domain/Research.mjs.map +1 -0
  57. package/dist/domain/Turn.d.mts +92 -10
  58. package/dist/domain/Turn.d.mts.map +1 -1
  59. package/dist/domain/Turn.mjs +8 -1
  60. package/dist/domain/Turn.mjs.map +1 -1
  61. package/dist/domain/Turn.test.mjs +1 -1
  62. package/dist/embedding-model/Embedding.d.mts +1 -1
  63. package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
  64. package/dist/embedding-model/EmbeddingModel.mjs +2 -2
  65. package/dist/embedding-model/EmbeddingModel.mjs.map +1 -1
  66. package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
  67. package/dist/index.d.mts +22 -13
  68. package/dist/index.mjs +18 -9
  69. package/dist/job/Job.d.mts +80 -0
  70. package/dist/job/Job.d.mts.map +1 -0
  71. package/dist/job/Job.mjs +49 -0
  72. package/dist/job/Job.mjs.map +1 -0
  73. package/dist/language-model/LanguageModel.d.mts +4 -4
  74. package/dist/language-model/LanguageModel.mjs +2 -2
  75. package/dist/language-model/LanguageModel.mjs.map +1 -1
  76. package/dist/language-model/LanguageModel.test.mjs +1 -1
  77. package/dist/loop/Loop.d.mts +1 -1
  78. package/dist/loop/Loop.test.mjs +1 -1
  79. package/dist/music-generator/MusicGenerator.d.mts +2 -2
  80. package/dist/music-generator/MusicGenerator.mjs +4 -4
  81. package/dist/music-generator/MusicGenerator.mjs.map +1 -1
  82. package/dist/music-generator/MusicGenerator.test.mjs +1 -1
  83. package/dist/observability/Metrics.d.mts +1 -1
  84. package/dist/observability/Metrics.test.mjs +2 -2
  85. package/dist/observability/Telemetry.test.mjs +2 -2
  86. package/dist/research/DeepResearch.d.mts +71 -0
  87. package/dist/research/DeepResearch.d.mts.map +1 -0
  88. package/dist/research/DeepResearch.mjs +61 -0
  89. package/dist/research/DeepResearch.mjs.map +1 -0
  90. package/dist/retry/Retry.d.mts +1 -1
  91. package/dist/retry/Retry.test.mjs +1 -1
  92. package/dist/sandbox/Sandbox.mjs +18 -18
  93. package/dist/sandbox/Sandbox.mjs.map +1 -1
  94. package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
  95. package/dist/speech-synthesizer/SpeechSynthesizer.mjs +8 -8
  96. package/dist/speech-synthesizer/SpeechSynthesizer.mjs.map +1 -1
  97. package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
  98. package/dist/streaming/JSONL.d.mts +3 -1
  99. package/dist/streaming/JSONL.d.mts.map +1 -1
  100. package/dist/streaming/JSONL.mjs +5 -11
  101. package/dist/streaming/JSONL.mjs.map +1 -1
  102. package/dist/streaming/JSONL.test.mjs +10 -1
  103. package/dist/streaming/JSONL.test.mjs.map +1 -1
  104. package/dist/streaming/SSE.d.mts +5 -3
  105. package/dist/streaming/SSE.d.mts.map +1 -1
  106. package/dist/streaming/SSE.mjs +28 -38
  107. package/dist/streaming/SSE.mjs.map +1 -1
  108. package/dist/streaming/SSE.test.mjs +54 -4
  109. package/dist/streaming/SSE.test.mjs.map +1 -1
  110. package/dist/structured-format/StructuredFormat.d.mts +1 -1
  111. package/dist/structured-format/StructuredFormat.test.mjs +1 -1
  112. package/dist/testing/MockMusicGenerator.d.mts +1 -1
  113. package/dist/testing/MockProvider.d.mts +1 -1
  114. package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
  115. package/dist/tool/Approval.d.mts +19 -7
  116. package/dist/tool/Approval.d.mts.map +1 -1
  117. package/dist/tool/Approval.mjs +18 -11
  118. package/dist/tool/Approval.mjs.map +1 -1
  119. package/dist/tool/Approval.test.mjs +47 -8
  120. package/dist/tool/Approval.test.mjs.map +1 -1
  121. package/dist/tool/HistoryCheck.d.mts +2 -2
  122. package/dist/tool/Tool.d.mts +2 -2
  123. package/dist/tool/Tool.mjs +112 -28
  124. package/dist/tool/Tool.mjs.map +1 -1
  125. package/dist/tool/Tool.test.mjs +127 -3
  126. package/dist/tool/Tool.test.mjs.map +1 -1
  127. package/dist/tool/ToolEvent.d.mts +1 -1
  128. package/dist/tool/ToolEvent.mjs +5 -0
  129. package/dist/tool/ToolEvent.mjs.map +1 -1
  130. package/dist/tool/ToolResult.d.mts +2 -2
  131. package/dist/tool/ToolResult.mjs +15 -6
  132. package/dist/tool/ToolResult.mjs.map +1 -1
  133. package/dist/tool/Toolkit.d.mts +2 -2
  134. package/dist/tool/Toolkit.mjs +62 -22
  135. package/dist/tool/Toolkit.mjs.map +1 -1
  136. package/dist/tool/Toolkit.test.mjs +207 -9
  137. package/dist/tool/Toolkit.test.mjs.map +1 -1
  138. package/dist/transcriber/Transcriber.d.mts +2 -2
  139. package/dist/transcriber/Transcriber.mjs +3 -3
  140. package/dist/transcriber/Transcriber.mjs.map +1 -1
  141. package/dist/transcriber/Transcriber.test.mjs +1 -1
  142. package/dist/web-read/WebRead.d.mts +44 -0
  143. package/dist/web-read/WebRead.d.mts.map +1 -0
  144. package/dist/web-read/WebRead.mjs +18 -0
  145. package/dist/web-read/WebRead.mjs.map +1 -0
  146. package/dist/web-read/WebReadTool.d.mts +57 -0
  147. package/dist/web-read/WebReadTool.d.mts.map +1 -0
  148. package/dist/web-read/WebReadTool.mjs +59 -0
  149. package/dist/web-read/WebReadTool.mjs.map +1 -0
  150. package/dist/web-search/WebSearch.d.mts +1 -1
  151. package/dist/web-search/WebSearch.mjs +1 -1
  152. package/dist/web-search/WebSearch.mjs.map +1 -1
  153. package/dist/web-search/WebSearchTool.d.mts +12 -6
  154. package/dist/web-search/WebSearchTool.d.mts.map +1 -1
  155. package/dist/web-search/WebSearchTool.mjs +10 -5
  156. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  157. package/package.json +39 -3
  158. package/src/browser/Browser.ts +268 -0
  159. package/src/browser/BrowserError.ts +145 -0
  160. package/src/browser/BrowserTool.ts +161 -0
  161. package/src/domain/AiError.ts +27 -1
  162. package/src/domain/Citation.ts +64 -0
  163. package/src/domain/Items.ts +14 -4
  164. package/src/domain/Research.ts +42 -0
  165. package/src/domain/Turn.ts +26 -0
  166. package/src/embedding-model/EmbeddingModel.ts +2 -2
  167. package/src/index.ts +9 -0
  168. package/src/job/Job.ts +125 -0
  169. package/src/language-model/LanguageModel.ts +2 -2
  170. package/src/music-generator/MusicGenerator.ts +4 -4
  171. package/src/research/DeepResearch.ts +137 -0
  172. package/src/sandbox/Sandbox.ts +18 -18
  173. package/src/speech-synthesizer/SpeechSynthesizer.ts +8 -8
  174. package/src/streaming/JSONL.test.ts +19 -0
  175. package/src/streaming/JSONL.ts +6 -38
  176. package/src/streaming/SSE.test.ts +86 -3
  177. package/src/streaming/SSE.ts +36 -80
  178. package/src/tool/Approval.test.ts +55 -5
  179. package/src/tool/Approval.ts +72 -27
  180. package/src/tool/Tool.test.ts +151 -1
  181. package/src/tool/Tool.ts +263 -66
  182. package/src/tool/ToolEvent.ts +5 -0
  183. package/src/tool/ToolResult.ts +14 -8
  184. package/src/tool/Toolkit.test.ts +231 -2
  185. package/src/tool/Toolkit.ts +175 -62
  186. package/src/transcriber/Transcriber.ts +3 -3
  187. package/src/web-read/WebRead.ts +70 -0
  188. package/src/web-read/WebReadTool.ts +96 -0
  189. package/src/web-search/WebSearch.ts +1 -1
  190. package/src/web-search/WebSearchTool.ts +11 -5
  191. package/dist/Items-CgKD6Npw.d.mts.map +0 -1
  192. package/dist/Tool-0XUIT2A2.d.mts.map +0 -1
  193. package/dist/ToolEvent-DYrAUJGs.d.mts.map +0 -1
  194. package/dist/ToolResult-BZd-cEWW.d.mts.map +0 -1
  195. package/dist/Toolkit-BKC8b8Xo.d.mts.map +0 -1
  196. package/dist/dist-DWgRbDN3.mjs.map +0 -1
package/src/tool/Tool.ts CHANGED
@@ -1,7 +1,8 @@
1
1
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
2
- import { Effect, Schema } from "effect"
2
+ import { Effect, Match, Schema } from "effect"
3
3
  import type { ToolCall, ToolCallOutput } from "../domain/Items.js"
4
4
  import { toolCallOutput } from "../domain/Items.js"
5
+ import { serializeValue } from "./ToolResult.js"
5
6
 
6
7
  export class ToolError extends Schema.TaggedErrorClass<ToolError>("@betalyra/effect-uai/ToolError")(
7
8
  "ToolError",
@@ -37,6 +38,35 @@ export class ToolValidationError extends Schema.TaggedErrorClass<ToolValidationE
37
38
  issues: Schema.Array(ToolValidationIssue),
38
39
  }) {}
39
40
 
41
+ /**
42
+ * The deliberate "speak to the model" failure sentinel. A tool's `run` that
43
+ * fails with a `ToolFailed` (or a bare `string`, its sugar) is absorbed by
44
+ * `Toolkit.run` into a `ToolResult.Failure` the model reads and adapts to,
45
+ * instead of ending the run. Every other error type propagates typed on the
46
+ * executor's stream. `kind` overrides the synthesized `Failure.kind`
47
+ * (default `"tool_failed"`) so a tool can hand the model a distinguishable
48
+ * category (`"not_found"`, `"rate_limited"`) without a new result variant.
49
+ */
50
+ export class ToolFailed extends Schema.TaggedErrorClass<ToolFailed>(
51
+ "@betalyra/effect-uai/ToolFailed",
52
+ )("ToolFailed", {
53
+ message: Schema.String,
54
+ kind: Schema.optional(Schema.String),
55
+ }) {}
56
+
57
+ /**
58
+ * Fail a tool's `run` with a model-visible message (optionally a `kind`).
59
+ * `yield* Tool.fail("page not found", { kind: "not_found" })` inside a
60
+ * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.
61
+ */
62
+ export const fail = (
63
+ message: string,
64
+ options?: { readonly kind?: string },
65
+ ): Effect.Effect<never, ToolFailed> =>
66
+ Effect.fail(
67
+ new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),
68
+ )
69
+
40
70
  /**
41
71
  * Schemas accepted on `Tool.inputSchema`. Must implement both Standard
42
72
  * Schema validation and JSON Schema conversion (for rendering tool
@@ -79,6 +109,25 @@ export const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSche
79
109
  ): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>
80
110
  schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>
81
111
 
112
+ /**
113
+ * Input schema for a tool that takes no arguments. Renders a clean
114
+ * `{ type: "object", properties: {} }`; validation accepts anything and
115
+ * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,
116
+ * which renders a degenerate `anyOf` (object-or-array) that strict
117
+ * providers like Gemini reject.
118
+ */
119
+ export const noInput: ToolInputSchema<Record<string, never>> = {
120
+ "~standard": {
121
+ version: 1,
122
+ vendor: "effect-uai",
123
+ validate: () => ({ value: {} }),
124
+ jsonSchema: {
125
+ input: () => ({ type: "object", properties: {} }),
126
+ output: () => ({ type: "object", properties: {} }),
127
+ },
128
+ },
129
+ }
130
+
82
131
  /**
83
132
  * Emit one intermediate `Event` from inside a tool's `run`. Backed by the
84
133
  * executor's per-call queue, so it returns an `Effect` (backpressure) and
@@ -98,12 +147,12 @@ export type Emit<Event> = (event: Event) => Effect.Effect<void>
98
147
  * no local executor — see their constructors below. `Tool` keeps its name (and
99
148
  * the `Tool.make` constructor) because the local kind is by far the common one.
100
149
  */
101
- export type Tool<Name extends string, Input, Event, Output, R = never> = {
150
+ export type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {
102
151
  readonly _tag: "LocalTool"
103
152
  readonly name: Name
104
153
  readonly description: string
105
154
  readonly inputSchema: ToolInputSchema<Input>
106
- readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, unknown, R>
155
+ readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>
107
156
  /**
108
157
  * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because
109
158
  * backpressure depends on how a given tool emits, unlike `concurrency`
@@ -162,11 +211,11 @@ export type InteractionTool<Name extends string, Input> = {
162
211
  }
163
212
 
164
213
  /** A local (executable) tool of any shape. Readability alias. */
165
- export type AnyLocalTool<R = any> = Tool<string, any, any, any, R>
214
+ export type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>
166
215
 
167
216
  /** Any of the four model-visible tool kinds. */
168
- export type AnyTool<R = any> =
169
- | Tool<string, any, any, any, R>
217
+ export type AnyTool<E = any, R = any> =
218
+ | Tool<string, any, any, any, E, R>
170
219
  | ProviderTool<string, any, string, any>
171
220
  | SignalTool<string, any>
172
221
  | InteractionTool<string, any>
@@ -178,7 +227,10 @@ export type DecodableTool<Input> = {
178
227
  }
179
228
 
180
229
  /** Extract the `R` of a local tool; non-local kinds contribute `never`. */
181
- export type ToolR<T> = T extends Tool<string, any, any, any, infer R> ? R : never
230
+ export type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never
231
+
232
+ /** Extract the `E` of a local tool; non-local kinds contribute `never`. */
233
+ export type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never
182
234
 
183
235
  /**
184
236
  * Provider-agnostic tool descriptor. Each provider maps `inputSchema`
@@ -192,37 +244,114 @@ export type ToolDescriptor = {
192
244
  readonly strict?: boolean
193
245
  }
194
246
 
247
+ const resolveInputSchema = (
248
+ schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,
249
+ ): ToolInputSchema<any> =>
250
+ Schema.isSchema(schema)
251
+ ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)
252
+ : (schema as ToolInputSchema<any>)
253
+
195
254
  /** A model-visible tool with a local `Effect` executor. */
196
- export const make = <Name extends string, Input, Event, Output, R = never>(
197
- spec: Omit<Tool<Name, Input, Event, Output, R>, "_tag">,
198
- ): Tool<Name, Input, Event, Output, R> => ({ _tag: "LocalTool", ...spec })
255
+ export const make: {
256
+ <
257
+ Name extends string,
258
+ S extends Schema.Codec<any, any, never, any>,
259
+ Event,
260
+ Output,
261
+ E = never,
262
+ R = never,
263
+ >(
264
+ spec: Omit<Tool<Name, S["Type"], Event, Output, E, R>, "_tag" | "inputSchema"> & {
265
+ readonly inputSchema: S
266
+ },
267
+ ): Tool<Name, S["Type"], Event, Output, E, R>
268
+ <Name extends string, Input, Event, Output, E = never, R = never>(
269
+ spec: Omit<Tool<Name, Input, Event, Output, E, R>, "_tag">,
270
+ ): Tool<Name, Input, Event, Output, E, R>
271
+ } = (
272
+ spec: Omit<AnyLocalTool, "_tag" | "inputSchema"> & {
273
+ readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
274
+ },
275
+ ): AnyLocalTool => ({
276
+ _tag: "LocalTool",
277
+ ...spec,
278
+ inputSchema: resolveInputSchema(spec.inputSchema),
279
+ })
199
280
 
200
281
  /**
201
282
  * A provider-hosted tool (native web search, code execution, RAG grounding).
202
283
  * No local executor — `provider`/`config` drive the provider adapter's
203
284
  * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.
204
285
  */
205
- export const provider = <Name extends string, Input, Provider extends string, Config>(
206
- spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag">,
207
- ): ProviderTool<Name, Input, Provider, Config> => ({ _tag: "ProviderTool", ...spec })
286
+ export const provider: {
287
+ <
288
+ Name extends string,
289
+ S extends Schema.Codec<any, any, never, any>,
290
+ Provider extends string,
291
+ Config,
292
+ >(
293
+ spec: Omit<ProviderTool<Name, S["Type"], Provider, Config>, "_tag" | "inputSchema"> & {
294
+ readonly inputSchema: S
295
+ },
296
+ ): ProviderTool<Name, S["Type"], Provider, Config>
297
+ <Name extends string, Input, Provider extends string, Config>(
298
+ spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag">,
299
+ ): ProviderTool<Name, Input, Provider, Config>
300
+ } = (
301
+ spec: Omit<ProviderTool<string, any, string, any>, "_tag" | "inputSchema"> & {
302
+ readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
303
+ },
304
+ ): ProviderTool<string, any, string, any> => ({
305
+ _tag: "ProviderTool",
306
+ ...spec,
307
+ inputSchema: resolveInputSchema(spec.inputSchema),
308
+ })
208
309
 
209
310
  /**
210
311
  * A typed control-flow signal to the agent loop (escalate, pause, schedule,
211
312
  * hand off). Decode-only: the loop interprets the call rather than executing a
212
313
  * handler, so there is no `run`.
213
314
  */
214
- export const signal = <Name extends string, Input>(
215
- spec: Omit<SignalTool<Name, Input>, "_tag">,
216
- ): SignalTool<Name, Input> => ({ _tag: "SignalTool", ...spec })
315
+ export const signal: {
316
+ <Name extends string, S extends Schema.Codec<any, any, never, any>>(
317
+ spec: Omit<SignalTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
318
+ readonly inputSchema: S
319
+ },
320
+ ): SignalTool<Name, S["Type"]>
321
+ <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, "_tag">): SignalTool<Name, Input>
322
+ } = (
323
+ spec: Omit<SignalTool<string, any>, "_tag" | "inputSchema"> & {
324
+ readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
325
+ },
326
+ ): SignalTool<string, any> => ({
327
+ _tag: "SignalTool",
328
+ ...spec,
329
+ inputSchema: resolveInputSchema(spec.inputSchema),
330
+ })
217
331
 
218
332
  /**
219
333
  * A typed request for an external actor to respond before the loop resumes
220
334
  * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and
221
335
  * resumes by appending the actor's `function_call_output`.
222
336
  */
223
- export const interaction = <Name extends string, Input>(
224
- spec: Omit<InteractionTool<Name, Input>, "_tag">,
225
- ): InteractionTool<Name, Input> => ({ _tag: "InteractionTool", ...spec })
337
+ export const interaction: {
338
+ <Name extends string, S extends Schema.Codec<any, any, never, any>>(
339
+ spec: Omit<InteractionTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
340
+ readonly inputSchema: S
341
+ },
342
+ ): InteractionTool<Name, S["Type"]>
343
+ <Name extends string, Input>(
344
+ spec: Omit<InteractionTool<Name, Input>, "_tag">,
345
+ ): InteractionTool<Name, Input>
346
+ } = (
347
+ spec: Omit<InteractionTool<string, any>, "_tag" | "inputSchema"> & {
348
+ readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
349
+ },
350
+ ): InteractionTool<string, any> => ({
351
+ _tag: "InteractionTool",
352
+ ...spec,
353
+ inputSchema: resolveInputSchema(spec.inputSchema),
354
+ })
226
355
 
227
356
  /**
228
357
  * Return a copy of a tool under a new `name`. Useful for resolving a name
@@ -245,23 +374,54 @@ export const withName = <N extends string, T extends AnyTool>(
245
374
  * Descriptors stay identical to production, so a mocked toolkit can't drift
246
375
  * from the contract the model sees.
247
376
  */
248
- export const withRun = <Name extends string, Input, Event, Output, R, Output2, R2 = never>(
249
- tool: Tool<Name, Input, Event, Output, R>,
250
- run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, unknown, R2>,
251
- ): Tool<Name, Input, Event, Output2, R2> => ({ ...tool, run })
377
+ export const withRun = <
378
+ Name extends string,
379
+ Input,
380
+ Event,
381
+ Output,
382
+ E,
383
+ R,
384
+ Output2,
385
+ E2 = never,
386
+ R2 = never,
387
+ >(
388
+ tool: Tool<Name, Input, Event, Output, E, R>,
389
+ run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,
390
+ ): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })
391
+
392
+ /** True for provider-hosted tools — the kind a provider renders natively. */
393
+ export const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>
394
+ tool._tag === "ProviderTool"
252
395
 
253
- /** Render tools to provider-agnostic descriptors. Every kind is model-visible. */
396
+ /**
397
+ * The `ProviderTool`s in a toolkit, for a provider adapter to render as its
398
+ * native hosted-tool entries (web search, code execution, grounding). The other
399
+ * kinds render as function declarations via `descriptorsOf`.
400
+ */
401
+ export const providerToolsOf = (
402
+ toolkit?: Readonly<Record<string, AnyTool>>,
403
+ ): ReadonlyArray<ProviderTool<string, any, string, any>> =>
404
+ toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)
405
+
406
+ /**
407
+ * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:
408
+ * they are provider-hosted and must never leak into the function-declaration
409
+ * list — each adapter renders them natively via `providerToolsOf`. Every other
410
+ * kind is model-visible as a declaration.
411
+ */
254
412
  export const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>
255
- tools.map((tool) => {
256
- const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
257
- target: "draft-2020-12",
413
+ tools
414
+ .filter((tool) => !isProviderTool(tool))
415
+ .map((tool) => {
416
+ const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
417
+ target: "draft-2020-12",
418
+ })
419
+ // `strict` only exists on local/provider kinds; signals/interactions omit it.
420
+ const strict = "strict" in tool ? tool.strict : undefined
421
+ return strict !== undefined
422
+ ? { name: tool.name, description: tool.description, inputSchema, strict }
423
+ : { name: tool.name, description: tool.description, inputSchema }
258
424
  })
259
- // `strict` only exists on local/provider kinds; signals/interactions omit it.
260
- const strict = "strict" in tool ? tool.strict : undefined
261
- return strict !== undefined
262
- ? { name: tool.name, description: tool.description, inputSchema, strict }
263
- : { name: tool.name, description: tool.description, inputSchema }
264
- })
265
425
 
266
426
  /**
267
427
  * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating
@@ -275,8 +435,51 @@ export const descriptorsOf = (
275
435
  ): ReadonlyArray<ToolDescriptor> =>
276
436
  toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))
277
437
 
278
- const toToolError = (call: ToolCall, toolName: string, message: string) => (cause: unknown) =>
279
- new ToolError({ call_id: call.call_id, tool: toolName, message, cause })
438
+ /**
439
+ * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,
440
+ * as data so both the typed (`decodeArgs`) and result-synthesizing
441
+ * (`Toolkit.run`) callers can translate it their own way.
442
+ */
443
+ export type DecodeResult<Input> =
444
+ | { readonly _tag: "ok"; readonly input: Input }
445
+ | { readonly _tag: "parseError" }
446
+ | { readonly _tag: "invalid"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }
447
+
448
+ const VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [
449
+ { message: "input schema validation threw" },
450
+ ]
451
+
452
+ /**
453
+ * Parse and validate a call's `arguments`. Empty arguments normalize to
454
+ * `{}` (some models emit `""` for zero-arg tools); a throwing validator is
455
+ * captured, not propagated. Never fails.
456
+ */
457
+ export const decodeCallInput = <Input>(
458
+ tool: DecodableTool<Input>,
459
+ call: ToolCall,
460
+ ): Effect.Effect<DecodeResult<Input>> => {
461
+ const raw = call.arguments.trim() === "" ? "{}" : call.arguments
462
+ return Effect.try({
463
+ // @effect-diagnostics-next-line effect/preferSchemaOverJson:off
464
+ try: () => JSON.parse(raw) as unknown,
465
+ catch: (): DecodeResult<Input> => ({ _tag: "parseError" }),
466
+ }).pipe(
467
+ Effect.flatMap((parsed) =>
468
+ Effect.tryPromise({
469
+ try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
470
+ catch: (): DecodeResult<Input> => ({ _tag: "invalid", issues: VALIDATION_THREW }),
471
+ }).pipe(
472
+ Effect.map(
473
+ (result): DecodeResult<Input> =>
474
+ result.issues !== undefined
475
+ ? { _tag: "invalid", issues: result.issues }
476
+ : { _tag: "ok", input: result.value },
477
+ ),
478
+ ),
479
+ ),
480
+ Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),
481
+ )
482
+ }
280
483
 
281
484
  /**
282
485
  * Decode and validate a function_call's JSON `arguments` against the tool's
@@ -291,28 +494,26 @@ export const decodeArgs = <Input>(
291
494
  tool: DecodableTool<Input>,
292
495
  call: ToolCall,
293
496
  ): Effect.Effect<Input, ToolError | ToolValidationError> =>
294
- Effect.gen(function* () {
295
- const parsed = yield* Effect.try({
296
- // inputSchema validates through Standard Schema, not Effect Schema, so its
297
- // JSON codecs don't apply; the parse is separate to keep ToolError (bad
298
- // JSON) distinct from ToolValidationError (bad shape).
299
- // @effect-diagnostics-next-line effect/preferSchemaOverJson:off
300
- try: () => JSON.parse(call.arguments) as unknown,
301
- catch: toToolError(call, tool.name, "Failed to parse JSON arguments"),
302
- })
303
-
304
- const result = yield* Effect.promise(() =>
305
- Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
306
- )
307
- if (result.issues !== undefined) {
308
- return yield* new ToolValidationError({
309
- call_id: call.call_id,
310
- tool: tool.name,
311
- issues: result.issues,
312
- })
313
- }
314
- return result.value
315
- })
497
+ decodeCallInput(tool, call).pipe(
498
+ Effect.flatMap(
499
+ Match.type<DecodeResult<Input>>().pipe(
500
+ Match.tag("ok", ({ input }) => Effect.succeed(input)),
501
+ Match.tag("parseError", () =>
502
+ Effect.fail(
503
+ new ToolError({
504
+ call_id: call.call_id,
505
+ tool: tool.name,
506
+ message: "Failed to parse JSON arguments",
507
+ }),
508
+ ),
509
+ ),
510
+ Match.tag("invalid", ({ issues }) =>
511
+ Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),
512
+ ),
513
+ Match.exhaustive,
514
+ ),
515
+ ),
516
+ )
316
517
 
317
518
  /**
318
519
  * Decode and validate the JSON arguments of a function_call against the
@@ -320,15 +521,11 @@ export const decodeArgs = <Input>(
320
521
  * function_call_output item. Single-shot: intermediate events emitted by
321
522
  * `run` are discarded (use `Toolkit.run` to stream them).
322
523
  */
323
- export const execute = <Name extends string, Input, Event, Output, R>(
324
- tool: Tool<Name, Input, Event, Output, R>,
524
+ export const execute = <Name extends string, Input, Event, Output, E, R>(
525
+ tool: Tool<Name, Input, Event, Output, E, R>,
325
526
  call: ToolCall,
326
- ): Effect.Effect<ToolCallOutput, ToolError | ToolValidationError, R> =>
527
+ ): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>
327
528
  decodeArgs(tool, call).pipe(
328
- Effect.flatMap((input) =>
329
- tool
330
- .run(input, () => Effect.void)
331
- .pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed"))),
332
- ),
333
- Effect.map((output) => toolCallOutput(call.call_id, JSON.stringify(output))),
529
+ Effect.flatMap((input) => tool.run(input, () => Effect.void)),
530
+ Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),
334
531
  )
@@ -7,6 +7,11 @@
7
7
  *
8
8
  * Recipes thread `ToolEvent.Output.result` through `continueWithResults` and
9
9
  * apply `toToolCallOutput` when appending to history.
10
+ *
11
+ * Demux note: `Progress` carries `call_id` / `tool` at the top level, while
12
+ * `Output` nests them inside `result` (the `ToolResult` already carries both).
13
+ * A consumer grouping events by call reads `e.call_id` for `Progress` and
14
+ * `e.result.call_id` for `Output`.
10
15
  */
11
16
  import { Data } from "effect"
12
17
  import type { ToolResult } from "./ToolResult.js"
@@ -95,14 +95,20 @@ export const validationError = (call: ToolCall, reason?: string): ToolResult =>
95
95
  // Wire conversion - the one place structured → string happens.
96
96
  // ---------------------------------------------------------------------------
97
97
 
98
+ /**
99
+ * Serialize a success value for the wire. A `string` passes through raw
100
+ * (models read plain text best; JSON.stringify would quote and escape it);
101
+ * `undefined` becomes `"null"`; anything else is JSON.
102
+ */
103
+ export const serializeValue = (value: unknown): string =>
104
+ typeof value === "string" ? value : JSON.stringify(value ?? null)
105
+
106
+ /** The one failure shape the model sees: `{"error":{"kind","message"}}`. */
107
+ const failureBody = (kind: string, message?: string): string =>
108
+ JSON.stringify({ error: message !== undefined ? { kind, message } : { kind } })
109
+
98
110
  export const toToolCallOutput = (r: ToolResult): ToolCallOutput =>
99
111
  ToolResult.$match(r, {
100
- Ok: (v) => toolCallOutput(v.call_id, JSON.stringify(v.value)),
101
- Failure: (f) =>
102
- toolCallOutput(
103
- f.call_id,
104
- JSON.stringify(
105
- f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
106
- ),
107
- ),
112
+ Ok: (v) => toolCallOutput(v.call_id, serializeValue(v.value)),
113
+ Failure: (f) => toolCallOutput(f.call_id, failureBody(f.kind, f.reason)),
108
114
  })