@effect-uai/core 0.8.0 → 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/dist/{AiError-CAX_48RU.d.mts → AiError-Bf1pq4BB.d.mts} +9 -4
- package/dist/{AiError-CAX_48RU.d.mts.map → AiError-Bf1pq4BB.d.mts.map} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts → Audio-CR7CSljJ.d.mts} +1 -1
- package/dist/{Audio-DPlTsNdz.d.mts.map → Audio-CR7CSljJ.d.mts.map} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts → Image-BZmKfIdq.d.mts} +1 -1
- package/dist/{Image-HNmMpMTh.d.mts.map → Image-BZmKfIdq.d.mts.map} +1 -1
- package/dist/{Items-7ljaVxH_.d.mts → Items-BnIduEV2.d.mts} +7 -7
- package/dist/{Items-7ljaVxH_.d.mts.map → Items-BnIduEV2.d.mts.map} +1 -1
- package/dist/Loop-D8nWpowc.d.mts +173 -0
- package/dist/Loop-D8nWpowc.d.mts.map +1 -0
- package/dist/{SandboxError-BvKypkJm.d.mts → SandboxError-CIrVDOMV.d.mts} +2 -2
- package/dist/{SandboxError-BvKypkJm.d.mts.map → SandboxError-CIrVDOMV.d.mts.map} +1 -1
- package/dist/{StructuredFormat-BbN4dosH.d.mts → StructuredFormat-DHaUJBdd.d.mts} +2 -2
- package/dist/{StructuredFormat-BbN4dosH.d.mts.map → StructuredFormat-DHaUJBdd.d.mts.map} +1 -1
- package/dist/Tool-2lC_XMth.d.mts +284 -0
- package/dist/Tool-2lC_XMth.d.mts.map +1 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts +151 -0
- package/dist/ToolEvent-cWZ2BnbS.d.mts.map +1 -0
- package/dist/ToolResult-CL4hK_9z.d.mts +157 -0
- package/dist/ToolResult-CL4hK_9z.d.mts.map +1 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts +196 -0
- package/dist/Toolkit-Dr9l_hNy.d.mts.map +1 -0
- package/dist/browser/Browser.d.mts +190 -0
- package/dist/browser/Browser.d.mts.map +1 -0
- package/dist/browser/Browser.mjs +34 -0
- package/dist/browser/Browser.mjs.map +1 -0
- package/dist/browser/BrowserError.d.mts +141 -0
- package/dist/browser/BrowserError.d.mts.map +1 -0
- package/dist/browser/BrowserError.mjs +89 -0
- package/dist/browser/BrowserError.mjs.map +1 -0
- package/dist/browser/BrowserTool.d.mts +58 -0
- package/dist/browser/BrowserTool.d.mts.map +1 -0
- package/dist/browser/BrowserTool.mjs +86 -0
- package/dist/browser/BrowserTool.mjs.map +1 -0
- package/dist/{dist-DV5ISja1.mjs → dist-B04dlFHg.mjs} +3 -3
- package/dist/{dist-DV5ISja1.mjs.map → dist-B04dlFHg.mjs.map} +1 -1
- package/dist/dist-DWgRbDN3.mjs +140 -0
- package/dist/dist-DWgRbDN3.mjs.map +1 -0
- package/dist/domain/AiError.d.mts +2 -2
- package/dist/domain/AiError.mjs +22 -3
- package/dist/domain/AiError.mjs.map +1 -1
- package/dist/domain/Audio.d.mts +1 -1
- package/dist/domain/Image.d.mts +1 -1
- package/dist/domain/Items.d.mts +1 -1
- package/dist/domain/Music.d.mts +1 -1
- package/dist/domain/Turn.d.mts +4 -4
- package/dist/domain/Turn.test.mjs +1 -1
- package/dist/embedding-model/Embedding.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.d.mts +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs +1 -1
- package/dist/embedding-model/EmbeddingModel.test.mjs.map +1 -1
- package/dist/index.d.mts +19 -13
- package/dist/index.mjs +15 -9
- package/dist/language-model/LanguageModel.d.mts +10 -5
- package/dist/language-model/LanguageModel.d.mts.map +1 -1
- package/dist/language-model/LanguageModel.mjs.map +1 -1
- package/dist/language-model/LanguageModel.test.mjs +1 -1
- package/dist/loop/Loop.d.mts +2 -141
- package/dist/loop/Loop.mjs +16 -20
- package/dist/loop/Loop.mjs.map +1 -1
- package/dist/loop/Loop.test.mjs +18 -1
- package/dist/loop/Loop.test.mjs.map +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs → magic-string.es-Dk-MMA0F.mjs} +1 -1
- package/dist/{magic-string.es-BgIV5Mu3.mjs.map → magic-string.es-Dk-MMA0F.mjs.map} +1 -1
- package/dist/music-generator/MusicGenerator.d.mts +2 -2
- package/dist/music-generator/MusicGenerator.test.mjs +1 -1
- package/dist/observability/Metrics.d.mts +116 -28
- package/dist/observability/Metrics.d.mts.map +1 -1
- package/dist/observability/Metrics.mjs +250 -38
- package/dist/observability/Metrics.mjs.map +1 -1
- package/dist/observability/Metrics.test.d.mts +1 -0
- package/dist/observability/Metrics.test.mjs +187 -0
- package/dist/observability/Metrics.test.mjs.map +1 -0
- package/dist/observability/Telemetry.d.mts +37 -0
- package/dist/observability/Telemetry.d.mts.map +1 -0
- package/dist/observability/Telemetry.mjs +57 -0
- package/dist/observability/Telemetry.mjs.map +1 -0
- package/dist/observability/Telemetry.test.d.mts +1 -0
- package/dist/observability/Telemetry.test.mjs +92 -0
- package/dist/observability/Telemetry.test.mjs.map +1 -0
- package/dist/retry/Retry.d.mts +2 -2
- package/dist/retry/Retry.test.mjs +1 -1
- package/dist/sandbox/Sandbox.d.mts +1 -1
- package/dist/sandbox/SandboxError.d.mts +1 -1
- package/dist/speech-synthesizer/SpeechSynthesizer.d.mts +2 -2
- package/dist/speech-synthesizer/SpeechSynthesizer.test.mjs +1 -1
- package/dist/streaming/JSONL.d.mts +1 -1
- package/dist/streaming/JSONL.test.mjs +1 -1
- package/dist/streaming/SSE.test.mjs +1 -1
- package/dist/structured-format/StructuredFormat.d.mts +1 -1
- package/dist/structured-format/StructuredFormat.mjs.map +1 -1
- package/dist/structured-format/StructuredFormat.test.mjs +1 -1
- package/dist/testing/MockMusicGenerator.d.mts +1 -1
- package/dist/testing/MockProvider.d.mts +1 -1
- package/dist/testing/MockSpeechSynthesizer.d.mts +1 -1
- package/dist/tool/Approval.d.mts +19 -7
- package/dist/tool/Approval.d.mts.map +1 -1
- package/dist/tool/Approval.mjs +18 -11
- package/dist/tool/Approval.mjs.map +1 -1
- package/dist/tool/Approval.test.mjs +80 -40
- package/dist/tool/Approval.test.mjs.map +1 -1
- package/dist/tool/HistoryCheck.d.mts +2 -2
- package/dist/tool/Tool.d.mts +2 -2
- package/dist/tool/Tool.mjs +190 -36
- package/dist/tool/Tool.mjs.map +1 -1
- package/dist/tool/Tool.test.mjs +41 -3
- package/dist/tool/Tool.test.mjs.map +1 -1
- package/dist/tool/ToolEvent.d.mts +2 -151
- package/dist/tool/ToolEvent.mjs +5 -0
- package/dist/tool/ToolEvent.mjs.map +1 -1
- package/dist/tool/ToolResult.d.mts +2 -138
- package/dist/tool/ToolResult.mjs +32 -8
- package/dist/tool/ToolResult.mjs.map +1 -1
- package/dist/tool/Toolkit.d.mts +2 -73
- package/dist/tool/Toolkit.mjs +153 -35
- package/dist/tool/Toolkit.mjs.map +1 -1
- package/dist/tool/Toolkit.test.mjs +271 -11
- package/dist/tool/Toolkit.test.mjs.map +1 -1
- package/dist/transcriber/Transcriber.d.mts +2 -2
- package/dist/transcriber/Transcriber.test.mjs +1 -1
- package/dist/web-read/WebRead.d.mts +44 -0
- package/dist/web-read/WebRead.d.mts.map +1 -0
- package/dist/web-read/WebRead.mjs +18 -0
- package/dist/web-read/WebRead.mjs.map +1 -0
- package/dist/web-read/WebReadTool.d.mts +57 -0
- package/dist/web-read/WebReadTool.d.mts.map +1 -0
- package/dist/web-read/WebReadTool.mjs +59 -0
- package/dist/web-read/WebReadTool.mjs.map +1 -0
- package/dist/web-search/WebSearch.d.mts +1 -1
- package/dist/web-search/WebSearchTool.d.mts +12 -6
- package/dist/web-search/WebSearchTool.d.mts.map +1 -1
- package/dist/web-search/WebSearchTool.mjs +10 -5
- package/dist/web-search/WebSearchTool.mjs.map +1 -1
- package/package.json +21 -1
- package/src/browser/Browser.ts +268 -0
- package/src/browser/BrowserError.ts +145 -0
- package/src/browser/BrowserTool.ts +161 -0
- package/src/domain/AiError.ts +27 -1
- package/src/embedding-model/EmbeddingModel.test.ts +4 -0
- package/src/index.ts +6 -0
- package/src/language-model/LanguageModel.ts +7 -2
- package/src/loop/Loop.test.ts +22 -0
- package/src/loop/Loop.ts +27 -4
- package/src/observability/Metrics.test.ts +266 -0
- package/src/observability/Metrics.ts +505 -65
- package/src/observability/Telemetry.test.ts +123 -0
- package/src/observability/Telemetry.ts +96 -0
- package/src/structured-format/StructuredFormat.ts +4 -0
- package/src/tool/Approval.test.ts +77 -26
- package/src/tool/Approval.ts +72 -27
- package/src/tool/Tool.test.ts +55 -2
- package/src/tool/Tool.ts +335 -70
- package/src/tool/ToolEvent.ts +5 -0
- package/src/tool/ToolResult.ts +36 -9
- package/src/tool/Toolkit.test.ts +339 -9
- package/src/tool/Toolkit.ts +470 -120
- package/src/web-read/WebRead.ts +70 -0
- package/src/web-read/WebReadTool.ts +96 -0
- package/src/web-search/WebSearchTool.ts +11 -5
- package/dist/Tool-BKiieFXk.d.mts +0 -102
- package/dist/Tool-BKiieFXk.d.mts.map +0 -1
- package/dist/loop/Loop.d.mts.map +0 -1
- package/dist/tool/ToolEvent.d.mts.map +0 -1
- package/dist/tool/ToolResult.d.mts.map +0 -1
- package/dist/tool/Toolkit.d.mts.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,
|
|
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",
|
|
@@ -13,6 +14,59 @@ export class ToolError extends Schema.TaggedErrorClass<ToolError>("@betalyra/eff
|
|
|
13
14
|
},
|
|
14
15
|
) {}
|
|
15
16
|
|
|
17
|
+
/**
|
|
18
|
+
* 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.
|
|
21
|
+
*/
|
|
22
|
+
const ToolValidationIssue = Schema.Struct({
|
|
23
|
+
message: Schema.String,
|
|
24
|
+
path: Schema.optional(Schema.Array(Schema.Unknown)),
|
|
25
|
+
})
|
|
26
|
+
|
|
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
|
+
*/
|
|
33
|
+
export class ToolValidationError extends Schema.TaggedErrorClass<ToolValidationError>(
|
|
34
|
+
"@betalyra/effect-uai/ToolValidationError",
|
|
35
|
+
)("ToolValidationError", {
|
|
36
|
+
call_id: Schema.String,
|
|
37
|
+
tool: Schema.String,
|
|
38
|
+
issues: Schema.Array(ToolValidationIssue),
|
|
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
|
+
|
|
16
70
|
/**
|
|
17
71
|
* Schemas accepted on `Tool.inputSchema`. Must implement both Standard
|
|
18
72
|
* Schema validation and JSON Schema conversion (for rendering tool
|
|
@@ -33,8 +87,7 @@ export type ToolInputSchema<Input = unknown> = StandardSchemaV1<unknown, Input>
|
|
|
33
87
|
export const fromEffectSchema = <S extends Schema.Codec<any, any, never, any>>(
|
|
34
88
|
schema: S,
|
|
35
89
|
): S & ToolInputSchema<S["Type"]> =>
|
|
36
|
-
Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))
|
|
37
|
-
ToolInputSchema<S["Type"]>
|
|
90
|
+
Schema.toStandardJSONSchemaV1(Schema.toStandardSchemaV1(schema))
|
|
38
91
|
|
|
39
92
|
/**
|
|
40
93
|
* Use any schema library that implements both Standard Schema (validation)
|
|
@@ -56,11 +109,56 @@ export const fromStandardSchema = <S extends StandardSchemaV1 & StandardJSONSche
|
|
|
56
109
|
): S & ToolInputSchema<StandardSchemaV1.InferOutput<S>> =>
|
|
57
110
|
schema as S & ToolInputSchema<StandardSchemaV1.InferOutput<S>>
|
|
58
111
|
|
|
59
|
-
|
|
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
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Emit one intermediate `Event` from inside a tool's `run`. Backed by the
|
|
133
|
+
* executor's per-call queue, so it returns an `Effect` (backpressure) and
|
|
134
|
+
* composes with `Stream.runForEach(emit)`.
|
|
135
|
+
*/
|
|
136
|
+
export type Emit<Event> = (event: Event) => Effect.Effect<void>
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* A model-visible tool with a local `Effect` executor. `run` computes the
|
|
140
|
+
* model-facing `Output`; emitting progress is a side channel via `emit`. A
|
|
141
|
+
* plain tool never calls `emit` (its `Event` is irrelevant); a streaming tool
|
|
142
|
+
* emits intermediate `Event`s that flow to the consumer as
|
|
143
|
+
* `ToolEvent.Progress` in real time.
|
|
144
|
+
*
|
|
145
|
+
* This is one of four tool *kinds*, discriminated by `_tag`. The other three
|
|
146
|
+
* (`ProviderTool`, `SignalTool`, `InteractionTool`) are model-visible but have
|
|
147
|
+
* no local executor — see their constructors below. `Tool` keeps its name (and
|
|
148
|
+
* the `Tool.make` constructor) because the local kind is by far the common one.
|
|
149
|
+
*/
|
|
150
|
+
export type Tool<Name extends string, Input, Event, Output, E = unknown, R = never> = {
|
|
151
|
+
readonly _tag: "LocalTool"
|
|
60
152
|
readonly name: Name
|
|
61
153
|
readonly description: string
|
|
62
154
|
readonly inputSchema: ToolInputSchema<Input>
|
|
63
|
-
readonly run: (input: Input) => Effect.Effect<Output,
|
|
155
|
+
readonly run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output, E, R>
|
|
156
|
+
/**
|
|
157
|
+
* Bound on this tool's emit queue. Unbounded when omitted. Per-tool because
|
|
158
|
+
* backpressure depends on how a given tool emits, unlike `concurrency`
|
|
159
|
+
* (cross-tool, on `Toolkit.run`'s options).
|
|
160
|
+
*/
|
|
161
|
+
readonly emitBufferSize?: number
|
|
64
162
|
/**
|
|
65
163
|
* Whether the provider should render this tool with its strict-mode
|
|
66
164
|
* flag (OpenAI's `strict: true`, etc). Default: true. The framework
|
|
@@ -70,6 +168,70 @@ export type Tool<Name extends string, Input, Output, R = never> = {
|
|
|
70
168
|
readonly strict?: boolean
|
|
71
169
|
}
|
|
72
170
|
|
|
171
|
+
/**
|
|
172
|
+
* A provider-hosted tool: visible to the model and executed by the *provider*
|
|
173
|
+
* (native web search, code execution, RAG grounding), never by this process.
|
|
174
|
+
* It has no local `run`; `provider`/`config` tell the provider adapter how to
|
|
175
|
+
* render it. Passing it to `Toolkit.run` yields a `non_local_tool` result.
|
|
176
|
+
*/
|
|
177
|
+
export type ProviderTool<Name extends string, Input, Provider extends string, Config> = {
|
|
178
|
+
readonly _tag: "ProviderTool"
|
|
179
|
+
readonly name: Name
|
|
180
|
+
readonly description: string
|
|
181
|
+
readonly inputSchema: ToolInputSchema<Input>
|
|
182
|
+
readonly provider: Provider
|
|
183
|
+
readonly config: Config
|
|
184
|
+
readonly strict?: boolean
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* A typed request for the agent loop to change control flow (escalate, pause,
|
|
189
|
+
* schedule, hand off). Model-visible and decodable (`decodeArgs`) but never
|
|
190
|
+
* locally executed — the loop intercepts the call in `onTurnComplete` and acts
|
|
191
|
+
* on it, so there is no fake `run`. `Toolkit.run` reports it as `non_local_tool`.
|
|
192
|
+
*/
|
|
193
|
+
export type SignalTool<Name extends string, Input> = {
|
|
194
|
+
readonly _tag: "SignalTool"
|
|
195
|
+
readonly name: Name
|
|
196
|
+
readonly description: string
|
|
197
|
+
readonly inputSchema: ToolInputSchema<Input>
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* A typed request for an external actor (user, channel, frontend) to respond
|
|
202
|
+
* before the loop resumes. Like `SignalTool` it is decode-only, but its
|
|
203
|
+
* lifecycle differs: the loop usually stops/pauses and resumes later by
|
|
204
|
+
* appending a `function_call_output` once the actor replies.
|
|
205
|
+
*/
|
|
206
|
+
export type InteractionTool<Name extends string, Input> = {
|
|
207
|
+
readonly _tag: "InteractionTool"
|
|
208
|
+
readonly name: Name
|
|
209
|
+
readonly description: string
|
|
210
|
+
readonly inputSchema: ToolInputSchema<Input>
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** A local (executable) tool of any shape. Readability alias. */
|
|
214
|
+
export type AnyLocalTool<E = any, R = any> = Tool<string, any, any, any, E, R>
|
|
215
|
+
|
|
216
|
+
/** Any of the four model-visible tool kinds. */
|
|
217
|
+
export type AnyTool<E = any, R = any> =
|
|
218
|
+
| Tool<string, any, any, any, E, R>
|
|
219
|
+
| ProviderTool<string, any, string, any>
|
|
220
|
+
| SignalTool<string, any>
|
|
221
|
+
| InteractionTool<string, any>
|
|
222
|
+
|
|
223
|
+
/** A model-visible tool that carries a decodable input schema (every kind). */
|
|
224
|
+
export type DecodableTool<Input> = {
|
|
225
|
+
readonly name: string
|
|
226
|
+
readonly inputSchema: ToolInputSchema<Input>
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Extract the `R` of a local tool; non-local kinds contribute `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
|
|
234
|
+
|
|
73
235
|
/**
|
|
74
236
|
* Provider-agnostic tool descriptor. Each provider maps `inputSchema`
|
|
75
237
|
* to its own wire field (OpenAI → `parameters`, Anthropic →
|
|
@@ -82,87 +244,190 @@ export type ToolDescriptor = {
|
|
|
82
244
|
readonly strict?: boolean
|
|
83
245
|
}
|
|
84
246
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
// ---------------------------------------------------------------------------
|
|
90
|
-
// Streaming tools
|
|
91
|
-
//
|
|
92
|
-
// `run` returns a `Stream<Event>` instead of an `Effect<Output>`. Events
|
|
93
|
-
// flow through to the consumer as `ToolEvent.Progress`s in real time;
|
|
94
|
-
// at end-of-stream `finalize(events)` reduces them to the model-facing
|
|
95
|
-
// `Output`. Sub-agents, slow downloads with progress, recipe streamers.
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
|
|
98
|
-
export type StreamingTool<Name extends string, Input, Event, Output, R = never> = {
|
|
99
|
-
readonly _kind: "streaming"
|
|
100
|
-
readonly name: Name
|
|
101
|
-
readonly description: string
|
|
102
|
-
readonly inputSchema: ToolInputSchema<Input>
|
|
103
|
-
readonly run: (input: Input) => Stream.Stream<Event, unknown, R>
|
|
104
|
-
readonly finalize: (events: ReadonlyArray<Event>) => Output
|
|
105
|
-
readonly strict?: boolean
|
|
106
|
-
}
|
|
247
|
+
/** A model-visible tool with a local `Effect` executor. */
|
|
248
|
+
export const make = <Name extends string, Input, Event, Output, E = never, R = never>(
|
|
249
|
+
spec: Omit<Tool<Name, Input, Event, Output, E, R>, "_tag">,
|
|
250
|
+
): Tool<Name, Input, Event, Output, E, R> => ({ _tag: "LocalTool", ...spec })
|
|
107
251
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
252
|
+
/**
|
|
253
|
+
* A provider-hosted tool (native web search, code execution, RAG grounding).
|
|
254
|
+
* No local executor — `provider`/`config` drive the provider adapter's
|
|
255
|
+
* rendering. The model sees it; `Toolkit.run` reports it as `non_local_tool`.
|
|
256
|
+
*/
|
|
257
|
+
export const provider = <Name extends string, Input, Provider extends string, Config>(
|
|
258
|
+
spec: Omit<ProviderTool<Name, Input, Provider, Config>, "_tag">,
|
|
259
|
+
): ProviderTool<Name, Input, Provider, Config> => ({ _tag: "ProviderTool", ...spec })
|
|
111
260
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
261
|
+
/**
|
|
262
|
+
* A typed control-flow signal to the agent loop (escalate, pause, schedule,
|
|
263
|
+
* hand off). Decode-only: the loop interprets the call rather than executing a
|
|
264
|
+
* handler, so there is no `run`.
|
|
265
|
+
*/
|
|
266
|
+
export const signal = <Name extends string, Input>(
|
|
267
|
+
spec: Omit<SignalTool<Name, Input>, "_tag">,
|
|
268
|
+
): SignalTool<Name, Input> => ({ _tag: "SignalTool", ...spec })
|
|
115
269
|
|
|
116
|
-
|
|
117
|
-
|
|
270
|
+
/**
|
|
271
|
+
* A typed request for an external actor to respond before the loop resumes
|
|
272
|
+
* (frontend, Telegram, CLI prompt). Decode-only; the loop stops/pauses and
|
|
273
|
+
* resumes by appending the actor's `function_call_output`.
|
|
274
|
+
*/
|
|
275
|
+
export const interaction = <Name extends string, Input>(
|
|
276
|
+
spec: Omit<InteractionTool<Name, Input>, "_tag">,
|
|
277
|
+
): InteractionTool<Name, Input> => ({ _tag: "InteractionTool", ...spec })
|
|
118
278
|
|
|
119
279
|
/**
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
280
|
+
* Return a copy of a tool under a new `name`. Useful for resolving a name
|
|
281
|
+
* clash before combining tools into one `Toolkit`. Literal-preserving, so the
|
|
282
|
+
* renamed tool keys the toolkit record under the new name statically.
|
|
123
283
|
*/
|
|
124
|
-
export const
|
|
284
|
+
export const withName = <N extends string, T extends AnyTool>(
|
|
285
|
+
tool: T,
|
|
286
|
+
name: N,
|
|
287
|
+
): Omit<T, "name"> & { readonly name: N } => ({ ...tool, name })
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Replace a local tool's `run`, keeping its model-facing definition (name,
|
|
291
|
+
* description, schema, `_tag`) identical. The new `run`'s `input` is typed from
|
|
292
|
+
* the original tool, so override/mock need no annotation:
|
|
293
|
+
*
|
|
294
|
+
* const safe = { ...toolkit, send_email: Tool.withRun(toolkit.send_email,
|
|
295
|
+
* ({ to }) => Effect.succeed({ status: "dry-run", to })) }
|
|
296
|
+
*
|
|
297
|
+
* Descriptors stay identical to production, so a mocked toolkit can't drift
|
|
298
|
+
* from the contract the model sees.
|
|
299
|
+
*/
|
|
300
|
+
export const withRun = <
|
|
301
|
+
Name extends string,
|
|
302
|
+
Input,
|
|
303
|
+
Event,
|
|
304
|
+
Output,
|
|
305
|
+
E,
|
|
306
|
+
R,
|
|
307
|
+
Output2,
|
|
308
|
+
E2 = never,
|
|
309
|
+
R2 = never,
|
|
310
|
+
>(
|
|
311
|
+
tool: Tool<Name, Input, Event, Output, E, R>,
|
|
312
|
+
run: (input: Input, emit: Emit<Event>) => Effect.Effect<Output2, E2, R2>,
|
|
313
|
+
): Tool<Name, Input, Event, Output2, E2, R2> => ({ ...tool, run })
|
|
314
|
+
|
|
315
|
+
/** Render tools to provider-agnostic descriptors. Every kind is model-visible. */
|
|
316
|
+
export const toDescriptors = (tools: ReadonlyArray<AnyTool>): ReadonlyArray<ToolDescriptor> =>
|
|
125
317
|
tools.map((tool) => {
|
|
126
318
|
const inputSchema = tool.inputSchema["~standard"].jsonSchema.input({
|
|
127
319
|
target: "draft-2020-12",
|
|
128
320
|
})
|
|
129
|
-
|
|
130
|
-
|
|
321
|
+
// `strict` only exists on local/provider kinds; signals/interactions omit it.
|
|
322
|
+
const strict = "strict" in tool ? tool.strict : undefined
|
|
323
|
+
return strict !== undefined
|
|
324
|
+
? { name: tool.name, description: tool.description, inputSchema, strict }
|
|
131
325
|
: { name: tool.name, description: tool.description, inputSchema }
|
|
132
326
|
})
|
|
133
327
|
|
|
134
|
-
|
|
135
|
-
|
|
328
|
+
/**
|
|
329
|
+
* Render a `Toolkit` (a name-indexed record of tools) to descriptors, treating
|
|
330
|
+
* an absent toolkit as no tools. The normalization point the `LanguageModel`
|
|
331
|
+
* boundary uses so a request can carry the toolkit itself instead of a
|
|
332
|
+
* pre-rendered descriptor array. Lives here (not on `Toolkit`) to keep providers
|
|
333
|
+
* free of a dependency on the toolkit module — it only needs the tool values.
|
|
334
|
+
*/
|
|
335
|
+
export const descriptorsOf = (
|
|
336
|
+
toolkit?: Readonly<Record<string, AnyTool>>,
|
|
337
|
+
): ReadonlyArray<ToolDescriptor> =>
|
|
338
|
+
toolkit === undefined ? [] : toDescriptors(Object.values(toolkit))
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Outcome of decoding a call's `arguments` against a tool's `inputSchema`,
|
|
342
|
+
* as data so both the typed (`decodeArgs`) and result-synthesizing
|
|
343
|
+
* (`Toolkit.run`) callers can translate it their own way.
|
|
344
|
+
*/
|
|
345
|
+
export type DecodeResult<Input> =
|
|
346
|
+
| { readonly _tag: "ok"; readonly input: Input }
|
|
347
|
+
| { readonly _tag: "parseError" }
|
|
348
|
+
| { readonly _tag: "invalid"; readonly issues: ReadonlyArray<StandardSchemaV1.Issue> }
|
|
349
|
+
|
|
350
|
+
const VALIDATION_THREW: ReadonlyArray<StandardSchemaV1.Issue> = [
|
|
351
|
+
{ message: "input schema validation threw" },
|
|
352
|
+
]
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Parse and validate a call's `arguments`. Empty arguments normalize to
|
|
356
|
+
* `{}` (some models emit `""` for zero-arg tools); a throwing validator is
|
|
357
|
+
* captured, not propagated. Never fails.
|
|
358
|
+
*/
|
|
359
|
+
export const decodeCallInput = <Input>(
|
|
360
|
+
tool: DecodableTool<Input>,
|
|
361
|
+
call: ToolCall,
|
|
362
|
+
): Effect.Effect<DecodeResult<Input>> => {
|
|
363
|
+
const raw = call.arguments.trim() === "" ? "{}" : call.arguments
|
|
364
|
+
return Effect.try({
|
|
365
|
+
// @effect-diagnostics-next-line effect/preferSchemaOverJson:off
|
|
366
|
+
try: () => JSON.parse(raw) as unknown,
|
|
367
|
+
catch: (): DecodeResult<Input> => ({ _tag: "parseError" }),
|
|
368
|
+
}).pipe(
|
|
369
|
+
Effect.flatMap((parsed) =>
|
|
370
|
+
Effect.tryPromise({
|
|
371
|
+
try: () => Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
372
|
+
catch: (): DecodeResult<Input> => ({ _tag: "invalid", issues: VALIDATION_THREW }),
|
|
373
|
+
}).pipe(
|
|
374
|
+
Effect.map(
|
|
375
|
+
(result): DecodeResult<Input> =>
|
|
376
|
+
result.issues !== undefined
|
|
377
|
+
? { _tag: "invalid", issues: result.issues }
|
|
378
|
+
: { _tag: "ok", input: result.value },
|
|
379
|
+
),
|
|
380
|
+
),
|
|
381
|
+
),
|
|
382
|
+
Effect.catch((captured: DecodeResult<Input>) => Effect.succeed(captured)),
|
|
383
|
+
)
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Decode and validate a function_call's JSON `arguments` against the tool's
|
|
388
|
+
* own `inputSchema`, yielding the typed input. The decode-only half of
|
|
389
|
+
* `execute` - reach for it when you intercept a call to translate it into a
|
|
390
|
+
* control-flow decision rather than running the tool's `run`.
|
|
391
|
+
*
|
|
392
|
+
* Malformed JSON fails with `ToolError`; a parsed body that violates
|
|
393
|
+
* `inputSchema` fails with `ToolValidationError` carrying the issues.
|
|
394
|
+
*/
|
|
395
|
+
export const decodeArgs = <Input>(
|
|
396
|
+
tool: DecodableTool<Input>,
|
|
397
|
+
call: ToolCall,
|
|
398
|
+
): Effect.Effect<Input, ToolError | ToolValidationError> =>
|
|
399
|
+
decodeCallInput(tool, call).pipe(
|
|
400
|
+
Effect.flatMap(
|
|
401
|
+
Match.type<DecodeResult<Input>>().pipe(
|
|
402
|
+
Match.tag("ok", ({ input }) => Effect.succeed(input)),
|
|
403
|
+
Match.tag("parseError", () =>
|
|
404
|
+
Effect.fail(
|
|
405
|
+
new ToolError({
|
|
406
|
+
call_id: call.call_id,
|
|
407
|
+
tool: tool.name,
|
|
408
|
+
message: "Failed to parse JSON arguments",
|
|
409
|
+
}),
|
|
410
|
+
),
|
|
411
|
+
),
|
|
412
|
+
Match.tag("invalid", ({ issues }) =>
|
|
413
|
+
Effect.fail(new ToolValidationError({ call_id: call.call_id, tool: tool.name, issues })),
|
|
414
|
+
),
|
|
415
|
+
Match.exhaustive,
|
|
416
|
+
),
|
|
417
|
+
),
|
|
418
|
+
)
|
|
136
419
|
|
|
137
420
|
/**
|
|
138
421
|
* Decode and validate the JSON arguments of a function_call against the
|
|
139
422
|
* tool's input schema, run the tool, and serialize the output into a
|
|
140
|
-
* function_call_output item.
|
|
423
|
+
* function_call_output item. Single-shot: intermediate events emitted by
|
|
424
|
+
* `run` are discarded (use `Toolkit.run` to stream them).
|
|
141
425
|
*/
|
|
142
|
-
export const execute = <Name extends string, Input, Output, R>(
|
|
143
|
-
tool: Tool<Name, Input, Output, R>,
|
|
426
|
+
export const execute = <Name extends string, Input, Event, Output, E, R>(
|
|
427
|
+
tool: Tool<Name, Input, Event, Output, E, R>,
|
|
144
428
|
call: ToolCall,
|
|
145
|
-
): Effect.Effect<ToolCallOutput, ToolError, R> =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
})
|
|
151
|
-
|
|
152
|
-
const result = yield* Effect.promise(() =>
|
|
153
|
-
Promise.resolve(tool.inputSchema["~standard"].validate(parsed)),
|
|
154
|
-
)
|
|
155
|
-
if (result.issues !== undefined) {
|
|
156
|
-
return yield* new ToolError({
|
|
157
|
-
call_id: call.call_id,
|
|
158
|
-
tool: tool.name,
|
|
159
|
-
message: "Tool input failed schema validation",
|
|
160
|
-
cause: result.issues,
|
|
161
|
-
})
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const output = yield* tool
|
|
165
|
-
.run(result.value)
|
|
166
|
-
.pipe(Effect.mapError(toToolError(call, tool.name, "Tool execution failed")))
|
|
167
|
-
return toolCallOutput(call.call_id, JSON.stringify(output))
|
|
168
|
-
})
|
|
429
|
+
): Effect.Effect<ToolCallOutput, E | ToolError | ToolValidationError, R> =>
|
|
430
|
+
decodeArgs(tool, call).pipe(
|
|
431
|
+
Effect.flatMap((input) => tool.run(input, () => Effect.void)),
|
|
432
|
+
Effect.map((output) => toolCallOutput(call.call_id, serializeValue(output))),
|
|
433
|
+
)
|
package/src/tool/ToolEvent.ts
CHANGED
|
@@ -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"
|
package/src/tool/ToolResult.ts
CHANGED
|
@@ -66,22 +66,49 @@ export const denied = (call: ToolCall, reason?: string): ToolResult =>
|
|
|
66
66
|
export const cancelled = (call: ToolCall, reason?: string): ToolResult =>
|
|
67
67
|
failed(call, "cancelled", reason)
|
|
68
68
|
|
|
69
|
-
/** Tool's own execution failed (parse error,
|
|
69
|
+
/** Tool's own execution failed (JSON parse error, runtime crash). */
|
|
70
70
|
export const executionError = (call: ToolCall, reason: string): ToolResult =>
|
|
71
71
|
failed(call, "execution_error", reason)
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* The model called a tool that is visible to it but has no local executor — a
|
|
75
|
+
* provider-hosted, signal, or interaction tool routed through `Toolkit.run`.
|
|
76
|
+
* Distinct from `unknown_tool` (no such tool at all): the loop is expected to
|
|
77
|
+
* intercept these kinds before execution, so this kind flags a missing handler.
|
|
78
|
+
*/
|
|
79
|
+
export const nonLocalTool = (call: ToolCall, reason?: string): ToolResult =>
|
|
80
|
+
failed(
|
|
81
|
+
call,
|
|
82
|
+
"non_local_tool",
|
|
83
|
+
reason ?? `Tool "${call.name}" is visible to the model but has no local executor`,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The model's `arguments` failed the tool's input schema. Distinct kind from
|
|
88
|
+
* `execution_error` so recipes can tell a contract violation apart from a
|
|
89
|
+
* runtime crash.
|
|
90
|
+
*/
|
|
91
|
+
export const validationError = (call: ToolCall, reason?: string): ToolResult =>
|
|
92
|
+
failed(call, "input_validation_error", reason ?? "Tool input failed schema validation")
|
|
93
|
+
|
|
73
94
|
// ---------------------------------------------------------------------------
|
|
74
95
|
// Wire conversion - the one place structured → string happens.
|
|
75
96
|
// ---------------------------------------------------------------------------
|
|
76
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
|
+
|
|
77
110
|
export const toToolCallOutput = (r: ToolResult): ToolCallOutput =>
|
|
78
111
|
ToolResult.$match(r, {
|
|
79
|
-
Ok: (v) => toolCallOutput(v.call_id,
|
|
80
|
-
Failure: (f) =>
|
|
81
|
-
toolCallOutput(
|
|
82
|
-
f.call_id,
|
|
83
|
-
JSON.stringify(
|
|
84
|
-
f.reason !== undefined ? { kind: f.kind, reason: f.reason } : { kind: f.kind },
|
|
85
|
-
),
|
|
86
|
-
),
|
|
112
|
+
Ok: (v) => toolCallOutput(v.call_id, serializeValue(v.value)),
|
|
113
|
+
Failure: (f) => toolCallOutput(f.call_id, failureBody(f.kind, f.reason)),
|
|
87
114
|
})
|