@effect-uai/core 0.18.0 → 0.19.0-dev-20261110065930

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 (162) hide show
  1. package/README.md +4 -4
  2. package/dist/Content-D_Qfn8Ja.d.mts +48 -0
  3. package/dist/Content-D_Qfn8Ja.d.mts.map +1 -0
  4. package/dist/File-B923KvZ9.d.mts +46 -0
  5. package/dist/File-B923KvZ9.d.mts.map +1 -0
  6. package/dist/{Image-Cv6iBLTy.d.mts → Image-Bzc1H_UK.d.mts} +19 -8
  7. package/dist/Image-Bzc1H_UK.d.mts.map +1 -0
  8. package/dist/{Items-C7MyrYPX.d.mts → Items-Bvx8s9QH.d.mts} +109 -16
  9. package/dist/Items-Bvx8s9QH.d.mts.map +1 -0
  10. package/dist/{Loop-ChpaO-ya.d.mts → Loop-CeA0AelN.d.mts} +7 -6
  11. package/dist/Loop-CeA0AelN.d.mts.map +1 -0
  12. package/dist/Tool-Bkryw6-h.d.mts +206 -0
  13. package/dist/Tool-Bkryw6-h.d.mts.map +1 -0
  14. package/dist/ToolEvent-9inLx5cI.d.mts +22 -0
  15. package/dist/ToolEvent-9inLx5cI.d.mts.map +1 -0
  16. package/dist/ToolOutput-BNTIQgGB.d.mts +151 -0
  17. package/dist/ToolOutput-BNTIQgGB.d.mts.map +1 -0
  18. package/dist/Toolkit-D2VGeD5R.d.mts +212 -0
  19. package/dist/Toolkit-D2VGeD5R.d.mts.map +1 -0
  20. package/dist/browser/BrowserTool.d.mts +84 -22
  21. package/dist/browser/BrowserTool.d.mts.map +1 -1
  22. package/dist/browser/BrowserTool.mjs +5 -0
  23. package/dist/browser/BrowserTool.mjs.map +1 -1
  24. package/dist/capabilities/Capabilities.d.mts +1 -1
  25. package/dist/decision-model/Decision.test.mjs +1 -1
  26. package/dist/{dist-C8RmtoR8.mjs → dist-B5K-y4Be.mjs} +51 -32
  27. package/dist/dist-B5K-y4Be.mjs.map +1 -0
  28. package/dist/domain/File.d.mts +2 -0
  29. package/dist/domain/File.mjs +51 -0
  30. package/dist/domain/File.mjs.map +1 -0
  31. package/dist/domain/Image.d.mts +2 -2
  32. package/dist/domain/Image.mjs +12 -2
  33. package/dist/domain/Image.mjs.map +1 -1
  34. package/dist/domain/Items.d.mts +2 -2
  35. package/dist/domain/Items.mjs +65 -4
  36. package/dist/domain/Items.mjs.map +1 -1
  37. package/dist/domain/Realtime.d.mts +72 -490
  38. package/dist/domain/Realtime.d.mts.map +1 -1
  39. package/dist/domain/Research.d.mts +1 -1
  40. package/dist/domain/Transcript.test.mjs +1 -1
  41. package/dist/domain/Turn.d.mts +57 -262
  42. package/dist/domain/Turn.d.mts.map +1 -1
  43. package/dist/domain/Turn.mjs +1 -1
  44. package/dist/embedding-model/Embedding.d.mts +1 -1
  45. package/dist/image-generator/ImageGenerator.d.mts +10 -66
  46. package/dist/image-generator/ImageGenerator.d.mts.map +1 -1
  47. package/dist/index.d.mts +10 -9
  48. package/dist/index.mjs +7 -6
  49. package/dist/language-model/LanguageModel.d.mts +2 -2
  50. package/dist/loop/Loop.d.mts +1 -1
  51. package/dist/loop/Loop.mjs.map +1 -1
  52. package/dist/messenger/Messenger.d.mts +43 -289
  53. package/dist/messenger/Messenger.d.mts.map +1 -1
  54. package/dist/messenger/MessengerAdapter.test.mjs +1 -1
  55. package/dist/observability/Metrics.d.mts +1 -1
  56. package/dist/observability/Metrics.test.mjs +1 -1
  57. package/dist/observability/Telemetry.test.mjs +1 -1
  58. package/dist/realtime/RealtimeSession.d.mts +1 -1
  59. package/dist/realtime/RealtimeSession.test.mjs +1 -1
  60. package/dist/sandbox/Sandbox.d.mts +32 -237
  61. package/dist/sandbox/Sandbox.d.mts.map +1 -1
  62. package/dist/streaming/Multipart.mjs +2 -1
  63. package/dist/streaming/Multipart.mjs.map +1 -1
  64. package/dist/streaming/Multipart.test.mjs +28 -0
  65. package/dist/streaming/Multipart.test.mjs.map +1 -0
  66. package/dist/streaming/Settle.test.mjs +1 -1
  67. package/dist/streaming/WebSocketSession.test.mjs +1 -1
  68. package/dist/testing/MockMessenger.d.mts +19 -134
  69. package/dist/testing/MockMessenger.d.mts.map +1 -1
  70. package/dist/testing/MockProvider.d.mts +1 -1
  71. package/dist/testing/MockSandbox.d.mts +65 -446
  72. package/dist/testing/MockSandbox.d.mts.map +1 -1
  73. package/dist/tool/Content.d.mts +2 -0
  74. package/dist/tool/Content.mjs +46 -0
  75. package/dist/tool/Content.mjs.map +1 -0
  76. package/dist/tool/Content.test.d.mts +1 -0
  77. package/dist/tool/Content.test.mjs +48 -0
  78. package/dist/tool/Content.test.mjs.map +1 -0
  79. package/dist/tool/HistoryCheck.d.mts +6 -15
  80. package/dist/tool/HistoryCheck.d.mts.map +1 -1
  81. package/dist/tool/HistoryCheck.mjs +10 -24
  82. package/dist/tool/HistoryCheck.mjs.map +1 -1
  83. package/dist/tool/Tool.d.mts +2 -2
  84. package/dist/tool/Tool.mjs +55 -130
  85. package/dist/tool/Tool.mjs.map +1 -1
  86. package/dist/tool/Tool.test.mjs +220 -176
  87. package/dist/tool/Tool.test.mjs.map +1 -1
  88. package/dist/tool/ToolDecision.d.mts +164 -0
  89. package/dist/tool/ToolDecision.d.mts.map +1 -0
  90. package/dist/tool/ToolDecision.mjs +146 -0
  91. package/dist/tool/ToolDecision.mjs.map +1 -0
  92. package/dist/tool/ToolDecision.test.d.mts +1 -0
  93. package/dist/tool/ToolDecision.test.mjs +330 -0
  94. package/dist/tool/ToolDecision.test.mjs.map +1 -0
  95. package/dist/tool/ToolEvent.d.mts +2 -2
  96. package/dist/tool/ToolEvent.mjs +2 -34
  97. package/dist/tool/ToolEvent.mjs.map +1 -1
  98. package/dist/tool/ToolOutput.d.mts +2 -0
  99. package/dist/tool/ToolOutput.mjs +128 -0
  100. package/dist/tool/ToolOutput.mjs.map +1 -0
  101. package/dist/tool/ToolOutput.test.d.mts +1 -0
  102. package/dist/tool/ToolOutput.test.mjs +153 -0
  103. package/dist/tool/ToolOutput.test.mjs.map +1 -0
  104. package/dist/tool/Toolkit.d.mts +2 -2
  105. package/dist/tool/Toolkit.mjs +230 -165
  106. package/dist/tool/Toolkit.mjs.map +1 -1
  107. package/dist/tool/Toolkit.test.mjs +431 -389
  108. package/dist/tool/Toolkit.test.mjs.map +1 -1
  109. package/dist/web-read/WebReadTool.d.mts +2 -2
  110. package/dist/web-read/WebReadTool.mjs +1 -0
  111. package/dist/web-read/WebReadTool.mjs.map +1 -1
  112. package/dist/web-search/WebSearchTool.d.mts +2 -2
  113. package/dist/web-search/WebSearchTool.mjs +1 -0
  114. package/dist/web-search/WebSearchTool.mjs.map +1 -1
  115. package/package.json +17 -9
  116. package/src/browser/BrowserTool.ts +5 -0
  117. package/src/domain/File.ts +79 -0
  118. package/src/domain/Image.ts +14 -4
  119. package/src/domain/Items.ts +39 -3
  120. package/src/index.ts +3 -2
  121. package/src/loop/Loop.ts +24 -13
  122. package/src/streaming/Multipart.test.ts +26 -0
  123. package/src/streaming/Multipart.ts +5 -4
  124. package/src/tool/Content.test.ts +40 -0
  125. package/src/tool/Content.ts +38 -0
  126. package/src/tool/HistoryCheck.ts +21 -26
  127. package/src/tool/Tool.test.ts +240 -241
  128. package/src/tool/Tool.ts +203 -308
  129. package/src/tool/ToolDecision.test.ts +301 -0
  130. package/src/tool/ToolDecision.ts +360 -0
  131. package/src/tool/ToolEvent.ts +21 -41
  132. package/src/tool/ToolOutput.test.ts +132 -0
  133. package/src/tool/ToolOutput.ts +198 -0
  134. package/src/tool/Toolkit.test.ts +594 -454
  135. package/src/tool/Toolkit.ts +631 -356
  136. package/src/web-read/WebReadTool.ts +2 -1
  137. package/src/web-search/WebSearchTool.ts +2 -1
  138. package/dist/Image-Cv6iBLTy.d.mts.map +0 -1
  139. package/dist/Items-C7MyrYPX.d.mts.map +0 -1
  140. package/dist/Loop-ChpaO-ya.d.mts.map +0 -1
  141. package/dist/Tool-BF78QWtn.d.mts +0 -314
  142. package/dist/Tool-BF78QWtn.d.mts.map +0 -1
  143. package/dist/ToolEvent-Di4a723B.d.mts +0 -144
  144. package/dist/ToolEvent-Di4a723B.d.mts.map +0 -1
  145. package/dist/ToolResult-BsYQRjmW.d.mts +0 -152
  146. package/dist/ToolResult-BsYQRjmW.d.mts.map +0 -1
  147. package/dist/Toolkit-tlL0F2YR.d.mts +0 -193
  148. package/dist/Toolkit-tlL0F2YR.d.mts.map +0 -1
  149. package/dist/dist-C8RmtoR8.mjs.map +0 -1
  150. package/dist/tool/Approval.d.mts +0 -113
  151. package/dist/tool/Approval.d.mts.map +0 -1
  152. package/dist/tool/Approval.mjs +0 -80
  153. package/dist/tool/Approval.mjs.map +0 -1
  154. package/dist/tool/Approval.test.mjs +0 -371
  155. package/dist/tool/Approval.test.mjs.map +0 -1
  156. package/dist/tool/ToolResult.d.mts +0 -2
  157. package/dist/tool/ToolResult.mjs +0 -85
  158. package/dist/tool/ToolResult.mjs.map +0 -1
  159. package/src/tool/Approval.test.ts +0 -400
  160. package/src/tool/Approval.ts +0 -181
  161. package/src/tool/ToolResult.ts +0 -114
  162. /package/dist/{tool/Approval.test.d.mts → streaming/Multipart.test.d.mts} +0 -0
package/src/tool/Tool.ts CHANGED
@@ -1,8 +1,6 @@
1
1
  import type { StandardJSONSchemaV1, StandardSchemaV1 } from "@standard-schema/spec"
2
- import { Effect, Match, Schema } from "effect"
3
- import type { ToolCall, ToolCallOutput } from "../domain/Items.js"
4
- import { toolCallOutput } from "../domain/Items.js"
5
- import { serializeValue } from "./ToolResult.js"
2
+ import { Effect, Function, Match, Schema, Stream } from "effect"
3
+ import type { ToolCall } from "../domain/Items.js"
6
4
 
7
5
  export class ToolError extends Schema.TaggedError<ToolError>("@betalyra/effect-uai/ToolError")(
8
6
  "ToolError",
@@ -16,20 +14,14 @@ export class ToolError extends Schema.TaggedError<ToolError>("@betalyra/effect-u
16
14
 
17
15
  /**
18
16
  * One Standard Schema validation issue, carried on `ToolValidationError`.
19
- * Mirrors `StandardSchemaV1.Issue`; `path` is left as `unknown` elements
20
- * because Standard Schema path segments may be symbols or `{ key }` objects.
17
+ * `path` elements stay `unknown`: Standard Schema segments may be symbols or `{ key }`.
21
18
  */
22
19
  const ToolValidationIssue = Schema.Struct({
23
20
  message: Schema.String,
24
21
  path: Schema.optional(Schema.Array(Schema.Unknown)),
25
22
  })
26
23
 
27
- /**
28
- * The model's `arguments` for a call failed the tool's `inputSchema`. Distinct
29
- * from `ToolError` (malformed JSON, runtime crash) so callers can tell a
30
- * contract violation apart from a parse or execution failure, and so the
31
- * structured `issues` survive instead of being flattened into a message string.
32
- */
24
+ /** A call's `arguments` failed the tool's `inputSchema` (from `decodeArgs`). */
33
25
  export class ToolValidationError extends Schema.TaggedError<ToolValidationError>(
34
26
  "@betalyra/effect-uai/ToolValidationError",
35
27
  )("ToolValidationError", {
@@ -39,71 +31,43 @@ export class ToolValidationError extends Schema.TaggedError<ToolValidationError>
39
31
  }) {}
40
32
 
41
33
  /**
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.
34
+ * A ready-made model-visible error for one-off messages. It reaches the model
35
+ * only when listed in the tool's `failureSchema` (or via `Toolkit.describeFailures`).
49
36
  */
50
37
  export class ToolFailed extends Schema.TaggedError<ToolFailed>("@betalyra/effect-uai/ToolFailed")(
51
38
  "ToolFailed",
52
- {
53
- message: Schema.String,
54
- kind: Schema.optional(Schema.String),
55
- },
39
+ { message: Schema.String },
56
40
  ) {}
57
41
 
58
- /**
59
- * Fail a tool's `run` with a model-visible message (optionally a `kind`).
60
- * `yield* Tool.fail("page not found", { kind: "not_found" })` inside a
61
- * `run`'s `Effect.gen`; the executor absorbs it as `ToolResult.Failure`.
62
- */
63
- export const fail = (
64
- message: string,
65
- options?: { readonly kind?: string },
66
- ): Effect.Effect<never, ToolFailed> =>
67
- Effect.fail(
68
- new ToolFailed({ message, ...(options?.kind !== undefined ? { kind: options.kind } : {}) }),
69
- )
42
+ /** `yield* Tool.fail("page not found")` inside a `run`. */
43
+ export const fail = (message: string): Effect.Effect<never, ToolFailed> =>
44
+ Effect.fail(new ToolFailed({ message }))
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // Input schemas
48
+ // ---------------------------------------------------------------------------
70
49
 
71
50
  /**
72
- * Schemas accepted on `Tool.inputSchema`. Must implement both Standard
73
- * Schema validation and JSON Schema conversion (for rendering tool
74
- * descriptors to provider request bodies).
75
- *
76
- * Any Standard-Schema-compliant library that exposes both interfaces
77
- * works directly: Zod 4+, Valibot, ArkType, Effect Schema (after
78
- * `fromEffectSchema`), etc.
51
+ * Standard Schema with JSON Schema conversion (Zod 4+, Valibot, ArkType, or an
52
+ * Effect Schema via `fromEffectSchema`). Validates the model's arguments and
53
+ * renders the provider descriptor.
79
54
  */
80
55
  export type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input> &
81
56
  StandardJSONSchemaV1<unknown, Input>
82
57
 
83
- /**
84
- * Convenience wrapper for Effect Schema users - adds both the
85
- * `validate` and `jsonSchema` extensions to a plain Effect Schema so it
86
- * can be used as a `Tool.inputSchema`.
87
- */
58
+ /** What `inputSchema` accepts: an Effect Schema directly, or a `ToolInputSchema`. */
59
+ export type InputSchema<Input> = Schema.Codec<Input, any, never, any> | ToolInputSchema<Input>
60
+
61
+ /** Adds the Standard Schema and Standard JSON Schema interfaces to an Effect Schema. */
88
62
  export const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(
89
63
  schema: S,
90
64
  ): S & ToolInputSchema<S["Type"]> =>
91
65
  Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))
92
66
 
93
67
  /**
94
- * Use any schema library that implements both Standard Schema (validation)
95
- * and Standard JSON Schema (JSON Schema generation) as a `Tool.inputSchema`.
96
- * Covers Zod 4.2+, Valibot 1.2+, and ArkType 2.1.28+ in one helper.
97
- *
98
- * Effect Schema doesn't implement Standard JSON Schema natively — use
99
- * `fromEffectSchema` for those.
100
- *
101
- * The intersection constraint catches missing interfaces at compile time:
102
- * a Zod v3 schema (no Standard JSON Schema) produces a precise type error
103
- * pointing at the missing interface rather than a runtime surprise. The
104
- * helper itself is a thin type-narrowing identity — schemas that satisfy
105
- * both standards already structurally satisfy `ToolInputSchema`; the
106
- * helper makes the input type inference explicit at the call site.
68
+ * Types a schema that implements both Standard Schema and Standard JSON Schema
69
+ * (Zod 4.2+, Valibot 1.2+, ArkType 2.1.28+). A schema missing either half is a
70
+ * compile error here rather than a runtime surprise.
107
71
  */
108
72
  export const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSchemaV1>(
109
73
  schema: S,
@@ -111,11 +75,8 @@ export const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSche
111
75
  schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>
112
76
 
113
77
  /**
114
- * Input schema for a tool that takes no arguments. Renders a clean
115
- * `{ type: "object", properties: {} }`; validation accepts anything and
116
- * yields `{}`. Use it instead of `fromEffectSchema(Schema.Struct({}))`,
117
- * which renders a degenerate `anyOf` (object-or-array) that strict
118
- * providers like Gemini reject.
78
+ * Input for a tool without arguments: renders `{ type: "object", properties: {} }`
79
+ * (an empty `Schema.Struct` renders an `anyOf` that strict providers reject).
119
80
  */
120
81
  export const noInput: ToolInputSchema<Record<string, never>> = {
121
82
  "~standard": {
@@ -129,287 +90,250 @@ export const noInput: ToolInputSchema<Record<string, never>> = {
129
90
  },
130
91
  }
131
92
 
132
- /**
133
- * Emit one intermediate `Event` from inside a tool's `run`. Backed by the
134
- * executor's per-call queue, so it returns an `Effect` (backpressure) and
135
- * composes with `Stream.runForEach(emit)`.
136
- */
137
- export type Emit<Event> = (event: Event) => Effect.Effect<void>
93
+ const resolveInputSchema = <Input>(schema: InputSchema<Input>): ToolInputSchema<Input> =>
94
+ Schema.isSchema(schema) ? fromEffectSchema(schema) : schema
138
95
 
139
- /**
140
- * A model-visible tool with a local `Effect` executor. `run` computes the
141
- * model-facing `Output`; emitting progress is a side channel via `emit`. A
142
- * plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool
143
- * emits intermediate `Event`s that flow to the consumer as
144
- * `ToolEvent.Progress` in real time.
145
- *
146
- * This is one of four tool *kinds*, discriminated by `_tag`. The other three
147
- * (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have
148
- * no local executor — see their constructors below. `Tool` keeps its name (and
149
- * the `Tool.make` constructor) because the local kind is by far the common one.
150
- */
151
- export type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {
152
- readonly _tag: "LocalTool"
96
+ // ---------------------------------------------------------------------------
97
+ // Tool kinds
98
+ // ---------------------------------------------------------------------------
99
+
100
+ /** One result (`Effect`) or partial results whose last element is the result (`Stream`). */
101
+ export type Run<Input, Output, E, R> = (
102
+ input: Input,
103
+ ) => Effect.Effect<Output, E, R> | Stream.Stream<Output, E, R>
104
+
105
+ /** Whether `run` returned a `Stream` (values as partials) rather than an `Effect` (one value). */
106
+ export const isStreamed = <A, E, R>(
107
+ out: Effect.Effect<A, E, R> | Stream.Stream<A, E, R>,
108
+ ): out is Stream.Stream<A, E, R> => Stream.isStream(out)
109
+
110
+ /** An `Effect` is a one-element stream. */
111
+ export const toStream = <A, E, R>(
112
+ out: Effect.Effect<A, E, R> | Stream.Stream<A, E, R>,
113
+ ): Stream.Stream<A, E, R> => (Effect.isEffect(out) ? Stream.fromEffect(out) : out)
114
+
115
+ type Described<Name extends string, Input, Output, Failure> = {
153
116
  readonly name: Name
154
117
  readonly description: string
155
118
  readonly inputSchema: ToolInputSchema<Input>
156
- readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>
119
+ /** Checks every value and encodes it for history and the UI. A rejected value is `OutputInvalid`. */
120
+ readonly outputSchema?: Schema.Codec<Output, any>
157
121
  /**
158
- * Bound on this tool's emit queue. Unbounded when omitted. Per-tool because
159
- * backpressure depends on how a given tool emits, unlike `concurrency`
160
- * (cross-tool, on `Toolkit.run`'s options).
161
- */
162
- readonly emitBufferSize?: number
163
- /**
164
- * Whether the provider should render this tool with its strict-mode
165
- * flag (OpenAI's `strict: true`, etc). Default: true. The framework
166
- * never rewrites the schema; if the rendered JSON Schema isn't
167
- * compatible, the provider returns an error.
122
+ * Every error a call may end with that the model sees: errors `run` raises and
123
+ * errors the app closes calls with (`ToolDecision.Denied`, see `Toolkit.close`).
168
124
  */
125
+ readonly failureSchema?: Schema.Codec<Failure, any>
126
+ /** Render with the provider's strict-mode flag. Default: true. */
169
127
  readonly strict?: boolean
170
128
  }
171
129
 
172
130
  /**
173
- * A provider-hosted tool: visible to the model and executed by the *provider*
174
- * (native web search, code execution, RAG grounding), never by this process.
175
- * It has no local `run`; `provider`/`config` tell the provider adapter how to
176
- * render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.
131
+ * A model-visible tool that `Toolkit.run` executes: errors matching
132
+ * `failureSchema` reach the model, any other error fails the stream.
177
133
  */
178
- export type ProviderTool<Name extends string, Input, Provider extends string, Config> = {
179
- readonly _tag: "ProviderTool"
180
- readonly name: Name
181
- readonly description: string
182
- readonly inputSchema: ToolInputSchema<Input>
183
- readonly provider: Provider
184
- readonly config: Config
185
- readonly strict?: boolean
134
+ export type Tool<
135
+ Name extends string,
136
+ Input,
137
+ Output,
138
+ Failure = never,
139
+ E = never,
140
+ R = never,
141
+ > = Described<Name, Input, Output, Failure> & {
142
+ readonly _tag: "LocalTool"
143
+ readonly run: Run<Input, Output, E, R>
186
144
  }
187
145
 
188
146
  /**
189
- * A typed request for the agent loop to change control flow (escalate, pause,
190
- * schedule, hand off). Model-visible and decodable (`decodeArgs`) but never
191
- * locally executed — the loop intercepts the call in `onTurnComplete` and acts
192
- * on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.
147
+ * A model-visible tool without `run`: the app handles its calls. A signal to
148
+ * the loop (`escalate`), or a call answered elsewhere (`ask_user`, typed by
149
+ * `outputSchema`). `Tool.withRun` turns it into a `Tool`.
193
150
  */
194
- export type SignalTool<Name extends string, Input> = {
195
- readonly _tag: "SignalTool"
196
- readonly name: Name
197
- readonly description: string
198
- readonly inputSchema: ToolInputSchema<Input>
151
+ export type Declared<Name extends string, Input, Output = never, Failure = never> = Described<
152
+ Name,
153
+ Input,
154
+ Output,
155
+ Failure
156
+ > & {
157
+ readonly _tag: "DeclaredTool"
199
158
  }
200
159
 
201
160
  /**
202
- * A typed request for an external actor (user, channel, frontend) to respond
203
- * before the loop resumes. Like `SignalTool` it is decode-only, but its
204
- * lifecycle differs: the loop usually stops/pauses and resumes later by
205
- * appending a `function_call_output` once the actor replies.
161
+ * A provider-hosted tool (native web search, code execution, grounding), executed
162
+ * by the provider. `provider` / `config` drive the adapter's rendering.
206
163
  */
207
- export type InteractionTool<Name extends string, Input> = {
208
- readonly _tag: "InteractionTool"
164
+ export type ProviderTool<Name extends string, Input, Provider extends string, Config> = {
165
+ readonly _tag: "ProviderTool"
209
166
  readonly name: Name
210
167
  readonly description: string
211
168
  readonly inputSchema: ToolInputSchema<Input>
169
+ readonly provider: Provider
170
+ readonly config: Config
171
+ readonly strict?: boolean
212
172
  }
213
173
 
214
- /** A local (executable) tool of any shape. Readability alias. */
215
174
  export type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>
216
175
 
217
- /** Any of the four model-visible tool kinds. */
176
+ export type AnyDeclared = Declared<string, any, any, any>
177
+
178
+ /** A tool with schemas the app sees: `Tool` or `Declared`. */
179
+ export type AnyDescribed = AnyLocalTool | AnyDeclared
180
+
218
181
  export type AnyTool<E = any, R = any> =
219
- | Tool<string, any, any, any, E, R>
182
+ | AnyLocalTool<E, R>
183
+ | AnyDeclared
220
184
  | ProviderTool<string, any, string, any>
221
- | SignalTool<string, any>
222
- | InteractionTool<string, any>
223
185
 
224
- /** A model-visible tool that carries a decodable input schema (every kind). */
186
+ /** Any tool kind with a decodable input schema. */
225
187
  export type DecodableTool<Input> = {
226
188
  readonly name: string
227
189
  readonly inputSchema: ToolInputSchema<Input>
228
190
  }
229
191
 
230
- /** Extract the `R` of a local tool; non-local kinds contribute `never`. */
231
- export type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never
192
+ type DescribedTag = { readonly _tag: "LocalTool" | "DeclaredTool" }
232
193
 
233
- /** Extract the `E` of a local tool; non-local kinds contribute `never`. */
194
+ export type ToolOutput<T> = T extends DescribedTag & Described<string, any, infer O, any>
195
+ ? O
196
+ : never
197
+ export type ToolFailure<T> = T extends DescribedTag & Described<string, any, any, infer F>
198
+ ? F
199
+ : never
234
200
  export type ToolE<T> = T extends Tool<string, any, any, any, infer E, any> ? E : never
201
+ export type ToolR<T> = T extends Tool<string, any, any, any, any, infer R> ? R : never
235
202
 
236
- /**
237
- * Provider-agnostic tool descriptor. Each provider maps `inputSchema`
238
- * to its own wire field (OpenAI → `parameters`, Anthropic →
239
- * `input_schema`). Built from a `Tool` by `Tool.toDescriptors`.
240
- */
241
- export type ToolDescriptor = {
242
- readonly name: string
203
+ /** The errors a tool did not declare: these fail `Toolkit.run`'s stream. */
204
+ export type ToolUnhandled<T> =
205
+ T extends Tool<string, any, any, infer F, infer E, any> ? Exclude<E, F> : never
206
+
207
+ type Spec<Name extends string, Input, Failure, FailureEncoded> = {
208
+ readonly name: Name
243
209
  readonly description: string
244
- readonly inputSchema: Record<string, unknown>
210
+ readonly inputSchema: InputSchema<Input>
211
+ readonly failureSchema?: Schema.Codec<Failure, FailureEncoded>
245
212
  readonly strict?: boolean
246
213
  }
247
214
 
248
- const resolveInputSchema = (
249
- schema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>,
250
- ): ToolInputSchema<any> =>
251
- Schema.isSchema(schema)
252
- ? fromEffectSchema(schema as Schema.Codec<any, any, never, any>)
253
- : (schema as ToolInputSchema<any>)
254
-
255
- /** A model-visible tool with a local `Effect` executor. */
256
- export const make: {
257
- <
258
- Name extends string,
259
- S extends Schema.Codec<any, any, never, any>,
260
- Event,
261
- Output,
262
- E = never,
263
- R = never,
264
- >(
265
- spec: Omit<Tool<Name, S["Type"], Event, Output, E, R>, "_tag" | "inputSchema"> & {
266
- readonly inputSchema: S
267
- },
268
- ): Tool<Name, S["Type"], Event, Output, E, R>
269
- <Name extends string, Input, Event, Output, E = never, R = never>(
270
- spec: Omit<Tool<Name, Input, Event, Output, E, R>, "_tag">,
271
- ): Tool<Name, Input, Event, Output, E, R>
272
- } = (
273
- spec: Omit<AnyLocalTool, "_tag" | "inputSchema"> & {
274
- readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
275
- },
276
- ): AnyLocalTool => ({
277
- _tag: "LocalTool",
278
- ...spec,
279
- inputSchema: resolveInputSchema(spec.inputSchema),
280
- })
281
-
282
215
  /**
283
- * A provider-hosted tool (native web search, code execution, RAG grounding).
284
- * No local executor — `provider`/`config` drive the provider adapter's
285
- * rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.
216
+ * A tool `Toolkit.run` executes. `outputSchema` says what `run` returns and its
217
+ * encoded side must be JSON. `Content.Value` is the explicit "anything": text,
218
+ * JSON or media.
286
219
  */
287
- export const provider: {
288
- <
289
- Name extends string,
290
- S extends Schema.Codec<any, any, never, any>,
291
- Provider extends string,
292
- Config,
293
- >(
294
- spec: Omit<ProviderTool<Name, S["Type"], Provider, Config>, "_tag" | "inputSchema"> & {
295
- readonly inputSchema: S
296
- },
297
- ): ProviderTool<Name, S["Type"], Provider, Config>
298
- <Name extends string, Input, Provider extends string, Config>(
299
- spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag">,
300
- ): ProviderTool<Name, Input, Provider, Config>
301
- } = (
302
- spec: Omit<ProviderTool<string, any, string, any>, "_tag" | "inputSchema"> & {
303
- readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
220
+ export const make = <
221
+ const Name extends string,
222
+ Input,
223
+ Output,
224
+ Encoded extends Schema.Json,
225
+ Failure = never,
226
+ FailureEncoded extends Schema.Json = never,
227
+ E = never,
228
+ R = never,
229
+ >(
230
+ spec: Spec<Name, Input, Failure, FailureEncoded> & {
231
+ readonly outputSchema: Schema.Codec<Output, Encoded>
232
+ readonly run: Run<Input, Output, E, R>
304
233
  },
305
- ): ProviderTool<string, any, string, any> => ({
306
- _tag: "ProviderTool",
234
+ ): Tool<Name, Input, Output, Failure, E, R> => ({
307
235
  ...spec,
236
+ _tag: "LocalTool",
308
237
  inputSchema: resolveInputSchema(spec.inputSchema),
309
238
  })
310
239
 
311
240
  /**
312
- * A typed control-flow signal to the agent loop (escalate, pause, schedule,
313
- * hand off). Decode-only: the loop interprets the call rather than executing a
314
- * handler, so there is no `run`.
241
+ * A tool without `run`: the app handles its calls. A signal to the loop (no
242
+ * `outputSchema`), or a call answered elsewhere (typed by `outputSchema`).
315
243
  */
316
- export const signal: {
317
- <Name extends string, S extends Schema.Codec<any, any, never, any>>(
318
- spec: Omit<SignalTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
319
- readonly inputSchema: S
320
- },
321
- ): SignalTool<Name, S["Type"]>
322
- <Name extends string, Input>(spec: Omit<SignalTool<Name, Input>, "_tag">): SignalTool<Name, Input>
323
- } = (
324
- spec: Omit<SignalTool<string, any>, "_tag" | "inputSchema"> & {
325
- readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
244
+ export const declare = <
245
+ const Name extends string,
246
+ Input,
247
+ Output = never,
248
+ Encoded extends Schema.Json = never,
249
+ Failure = never,
250
+ FailureEncoded extends Schema.Json = never,
251
+ >(
252
+ spec: Spec<Name, Input, Failure, FailureEncoded> & {
253
+ readonly outputSchema?: Schema.Codec<Output, Encoded>
326
254
  },
327
- ): SignalTool<string, any> => ({
328
- _tag: "SignalTool",
255
+ ): Declared<Name, Input, Output, Failure> => ({
329
256
  ...spec,
257
+ _tag: "DeclaredTool",
330
258
  inputSchema: resolveInputSchema(spec.inputSchema),
331
259
  })
332
260
 
333
- /**
334
- * A typed request for an external actor to respond before the loop resumes
335
- * (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and
336
- * resumes by appending the actor's `function_call_output`.
337
- */
338
- export const interaction: {
339
- <Name extends string, S extends Schema.Codec<any, any, never, any>>(
340
- spec: Omit<InteractionTool<Name, S["Type"]>, "_tag" | "inputSchema"> & {
341
- readonly inputSchema: S
342
- },
343
- ): InteractionTool<Name, S["Type"]>
344
- <Name extends string, Input>(
345
- spec: Omit<InteractionTool<Name, Input>, "_tag">,
346
- ): InteractionTool<Name, Input>
347
- } = (
348
- spec: Omit<InteractionTool<string, any>, "_tag" | "inputSchema"> & {
349
- readonly inputSchema: ToolInputSchema<any> | Schema.Codec<any, any, never, any>
261
+ /** A provider-hosted tool. */
262
+ export const provider = <const Name extends string, Input, Provider extends string, Config>(
263
+ spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag" | "inputSchema"> & {
264
+ readonly inputSchema: InputSchema<Input>
350
265
  },
351
- ): InteractionTool<string, any> => ({
352
- _tag: "InteractionTool",
266
+ ): ProviderTool<Name, Input, Provider, Config> => ({
267
+ _tag: "ProviderTool",
353
268
  ...spec,
354
269
  inputSchema: resolveInputSchema(spec.inputSchema),
355
270
  })
356
271
 
357
- /**
358
- * Return a copy of a tool under a new `name`. Useful for resolving a name
359
- * clash before combining tools into one `Toolkit`. Literal-preserving, so the
360
- * renamed tool keys the toolkit record under the new name statically.
361
- */
272
+ /** A copy of a tool under a new `name`, e.g. to resolve a clash before `Toolkit.compose`. */
362
273
  export const withName = <N extends string, T extends AnyTool>(
363
274
  tool: T,
364
275
  name: N,
365
276
  ): Omit<T, "name"> & { readonly name: N } => ({ ...tool, name })
366
277
 
367
278
  /**
368
- * Replace a local tool's `run`, keeping its model-facing definition (name,
369
- * description, schema, `_tag`) identical. The new `run`'s `input` is typed from
370
- * the original tool, so override/mock need no annotation:
371
- *
372
- * const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,
373
- * ({ to }) => Effect.succeed({ status: "dry-run", to })) }
374
- *
375
- * Descriptors stay identical to production, so a mocked toolkit can't drift
376
- * from the contract the model sees.
279
+ * Give a tool a `run`, keeping everything else (name, description, schemas):
280
+ * implement a declared tool (a spec shared with the browser), or mock a tool.
281
+ * The new `run` returns the same output type.
377
282
  */
378
- export const withRun = <
379
- Name extends string,
380
- Input,
381
- Event,
382
- Output,
383
- E,
384
- R,
385
- Output2,
386
- E2 = never,
387
- R2 = never,
388
- >(
389
- tool: Tool<Name, Input, Event, Output, E, R>,
390
- run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,
391
- ): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })
283
+ export const withRun = <Name extends string, Input, Output, Failure, E2 = never, R2 = never>(
284
+ tool: Tool<Name, Input, Output, Failure, any, any> | Declared<Name, Input, Output, Failure>,
285
+ run: Run<Input, Output, E2, R2>,
286
+ ): Tool<Name, Input, Output, Failure, E2, R2> => ({ ...tool, _tag: "LocalTool", run })
287
+
288
+ /** The same kind of tool, with more failures. */
289
+ export type WithFailures<T, F2> =
290
+ T extends Tool<infer N, infer I, infer O, infer F, infer E, infer R>
291
+ ? Tool<N, I, O, F | F2, E, R>
292
+ : T extends Declared<infer N, infer I, infer O, infer F>
293
+ ? Declared<N, I, O, F | F2>
294
+ : never
295
+
296
+ /**
297
+ * Add errors a call of this tool may end with, e.g. the app's `ToolDecision.Denied`
298
+ * for a tool that needs approval. Its `run` need not raise them; the app closes
299
+ * calls with them (`Toolkit.close`).
300
+ */
301
+ export const withFailures: {
302
+ <F2, F2Encoded extends Schema.Json>(
303
+ failures: Schema.Codec<F2, F2Encoded>,
304
+ ): <T extends AnyDescribed>(tool: T) => WithFailures<T, F2>
305
+ <T extends AnyDescribed, F2, F2Encoded extends Schema.Json>(
306
+ tool: T,
307
+ failures: Schema.Codec<F2, F2Encoded>,
308
+ ): WithFailures<T, F2>
309
+ } = Function.dual(2, (tool: AnyDescribed, failures: Schema.Codec<unknown, any>): AnyDescribed => ({
310
+ ...tool,
311
+ failureSchema:
312
+ tool.failureSchema === undefined ? failures : Schema.Union([tool.failureSchema, failures]),
313
+ }))
314
+
315
+ // ---------------------------------------------------------------------------
316
+ // Descriptors
317
+ // ---------------------------------------------------------------------------
318
+
319
+ /** Provider-agnostic descriptor; each provider maps `inputSchema` to its own field. */
320
+ export type ToolDescriptor = {
321
+ readonly name: string
322
+ readonly description: string
323
+ readonly inputSchema: Record<string, unknown>
324
+ readonly strict?: boolean
325
+ }
392
326
 
393
- /** True for provider-hosted tools — the kind a provider renders natively. */
394
327
  export const isProviderTool = (tool: AnyTool): tool is ProviderTool<string, any, string, any> =>
395
328
  tool._tag === "ProviderTool"
396
329
 
397
- /**
398
- * The `ProviderTool`s in a toolkit, for a provider adapter to render as its
399
- * native hosted-tool entries (web search, code execution, grounding). The other
400
- * kinds render as function declarations via `descriptorsOf`.
401
- */
330
+ /** The provider-hosted tools in a toolkit, for an adapter to render natively. */
402
331
  export const providerToolsOf = (
403
332
  toolkit?: Readonly<Record<string, AnyTool>>,
404
333
  ): ReadonlyArray<ProviderTool<string, any, string, any>> =>
405
334
  toolkit === undefined ? [] : Object.values(toolkit).filter(isProviderTool)
406
335
 
407
- /**
408
- * Render tools to provider-agnostic descriptors. `ProviderTool`s are excluded:
409
- * they are provider-hosted and must never leak into the function-declaration
410
- * list — each adapter renders them natively via `providerToolsOf`. Every other
411
- * kind is model-visible as a declaration.
412
- */
336
+ /** Function descriptors for every tool except provider-hosted ones. */
413
337
  export const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>
414
338
  tools
415
339
  .filter((tool) => !isProviderTool(tool))
@@ -417,30 +341,22 @@ export const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<Tool
417
341
  const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
418
342
  target: "draft-2020-12",
419
343
  })
420
- // `strict` only exists on local/provider kinds; signals/interactions omit it.
421
344
  const strict = "strict" in tool ? tool.strict : undefined
422
345
  return strict !== undefined
423
346
  ? { name: tool.name, description: tool.description, inputSchema, strict }
424
347
  : { name: tool.name, description: tool.description, inputSchema }
425
348
  })
426
349
 
427
- /**
428
- * Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating
429
- * an absent toolkit as no tools. The normalization point the `LanguageModel`
430
- * boundary uses so a request can carry the toolkit itself instead of a
431
- * pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers
432
- * free of a dependency on the toolkit module — it only needs the tool values.
433
- */
350
+ /** `toDescriptors` over a toolkit record; an absent toolkit has none. */
434
351
  export const descriptorsOf = (
435
352
  toolkit?: Readonly<Record<string, AnyTool>>,
436
353
  ): ReadonlyArray<ToolDescriptor> =>
437
354
  toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))
438
355
 
439
- /**
440
- * Outcome of decoding a call's `arguments` against a tool's `inputSchema`,
441
- * as data so both the typed (`decodeArgs`) and result-synthesizing
442
- * (`Toolkit.run`) callers can translate it their own way.
443
- */
356
+ // ---------------------------------------------------------------------------
357
+ // Decoding arguments
358
+ // ---------------------------------------------------------------------------
359
+
444
360
  export type DecodeResult<Input> =
445
361
  | { readonly _tag: "ok"; readonly input: Input }
446
362
  | { readonly _tag: "parseError" }
@@ -451,9 +367,8 @@ const VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [
451
367
  ]
452
368
 
453
369
  /**
454
- * Parse and validate a call's `arguments`. Empty arguments normalize to
455
- * `{}` (some models emit `""` for zero-arg tools); a throwing validator is
456
- * captured, not propagated. Never fails.
370
+ * Parse and validate a call's `arguments`, as data. Empty arguments read as `{}`
371
+ * (some models send `""` for tools without arguments). Never fails.
457
372
  */
458
373
  export const decodeCallInput = <Input>(
459
374
  tool: DecodableTool<Input>,
@@ -482,13 +397,8 @@ export const decodeCallInput = <Input>(
482
397
  }
483
398
 
484
399
  /**
485
- * Decode and validate a function_call's JSON `arguments` against the tool's
486
- * own `inputSchema`, yielding the typed input. The decode-only half of
487
- * `execute` - reach for it when you intercept a call to translate it into a
488
- * control-flow decision rather than running the tool's `run`.
489
- *
490
- * Malformed JSON fails with `ToolError`; a parsed body that violates
491
- * `inputSchema` fails with `ToolValidationError` carrying the issues.
400
+ * A call's typed input, e.g. for a tool without `run` the loop handles itself.
401
+ * Bad JSON fails with `ToolError`, a schema violation with `ToolValidationError`.
492
402
  */
493
403
  export const decodeArgs = <Input>(
494
404
  tool: DecodableTool<Input>,
@@ -514,18 +424,3 @@ export const decodeArgs = <Input>(
514
424
  ),
515
425
  ),
516
426
  )
517
-
518
- /**
519
- * Decode and validate the JSON arguments of a function_call against the
520
- * tool's input schema, run the tool, and serialize the output into a
521
- * function_call_output item. Single-shot: intermediate events emitted by
522
- * `run` are discarded (use `Toolkit.run` to stream them).
523
- */
524
- export const execute = <Name extends string, Input, Event, Output, E, R>(
525
- tool: Tool<Name, Input, Event, Output, E, R>,
526
- call: ToolCall,
527
- ): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>
528
- decodeArgs(tool, call).pipe(
529
- Effect.flatMap((input) => tool.run(input, () => Effect.void)),
530
- Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),
531
- )