1/**
2 * Defines the shared service for language model providers.
3 *
4 * The `LanguageModel` service lets application code ask for generated text,
5 * streamed text, or structured output without depending on a specific provider.
6 * Requests can include tools, and the service can resolve tool calls while the
7 * model is generating a response. This module contains the service contract,
8 * request and response types, structured-output support, and the constructor
9 * used by provider packages to adapt their own generate and stream functions to
10 * the shared interface.
11 *
12 * @stability unstable
13 * @since 4.0.0
14 */
15import type * as Cause from "../Cause.ts"
16import * as Context from "../Context.ts"
17import * as Effect from "../Effect.ts"
18import * as FiberSet from "../FiberSet.ts"
19import { constFalse, identity, pipe } from "../Function.ts"
20import type * as JsonSchema from "../JsonSchema.ts"
21import * as Option from "../Option.ts"
22import * as Predicate from "../Predicate.ts"
23import * as Queue from "../Queue.ts"
24import * as Schema from "../Schema.ts"
25import * as SchemaAST from "../SchemaAST.ts"
26import * as Semaphore from "../Semaphore.ts"
27import * as Sink from "../Sink.ts"
28import * as Stream from "../Stream.ts"
29import type { Span } from "../Tracer.ts"
30import type { Concurrency, Mutable, NoExcessProperties } from "../Types.ts"
31import * as AiError from "./AiError.ts"
32import { defaultIdGenerator, IdGenerator } from "./IdGenerator.ts"
33import * as InternalCodecTransformer from "./internal/codec-transformer.ts"
34import * as Prompt from "./Prompt.ts"
35import * as Response from "./Response.ts"
36import * as ResponseIdTracker from "./ResponseIdTracker.ts"
37import type { SpanTransformer } from "./Telemetry.ts"
38import { CurrentSpanTransformer } from "./Telemetry.ts"
39import type * as Tool from "./Tool.ts"
40import * as Toolkit from "./Toolkit.ts"
41
42/**
43 * Service key for text generation, structured output, and tool calls.
44 *
45 * **Example** (Accessing the language model service)
46 *
47 * ```ts import.meta.vitest
48 * import { Effect, Layer, Stream } from "effect"
49 * import { LanguageModel } from "effect/ai"
50 *
51 * const FakeLanguageModel = Layer.effect(
52 * LanguageModel.LanguageModel,
53 * LanguageModel.make({
54 * generateText: () =>
55 * Effect.succeed([{
56 * type: "text",
57 * text: "Machine learning finds patterns in data."
58 * }]),
59 * streamText: () => Stream.empty
60 * })
61 * )
62 *
63 * const program = Effect.gen(function*() {
64 * const model = yield* LanguageModel.LanguageModel
65 * const response = yield* model.generateText({
66 * prompt: "What is machine learning?"
67 * })
68 * return response.text
69 * })
70 *
71 * await Effect.runPromise(program.pipe(Effect.provide(FakeLanguageModel))) // => "Machine learning finds patterns in data."
72 * ```
73 *
74 * @stability unstable
75 * @category services
76 * @since 4.0.0
77 */
78export const LanguageModel: Context.Service<LanguageModel, LanguageModel> = Context.Service(
79 "effect/ai/LanguageModel"
80)
81
82/**
83 * Brand type for `LanguageModel`.
84 *
85 * @stability unstable
86 * @category type IDs
87 * @since 4.0.0
88 */
89export type TypeId = "~effect/ai/LanguageModel"
90
91/**
92 * Brand for `LanguageModel` implementations.
93 *
94 * @stability unstable
95 * @category type IDs
96 * @since 4.0.0
97 */
98export const TypeId: TypeId = "~effect/ai/LanguageModel"
99
100/**
101 * Text generation, streaming, and structured output operations.
102 *
103 * @stability unstable
104 * @category models
105 * @since 4.0.0
106 */
107export interface LanguageModel {
108 readonly [TypeId]: TypeId
109
110 /**
111 * Whether the effective provider configuration supports system messages in
112 * history. Evaluate in the request's context to respect scoped overrides.
113 * Placement restrictions may apply; an absent capability means unknown support.
114 */
115 readonly supportsSystemMessagesInHistory?: Effect.Effect<boolean> | undefined
116
117 /**
118 * Generate text using the language model.
119 */
120 readonly generateText: {
121 // No toolkit: force `{}` instead of falling back to `Record<string, Tool.Any>`.
122 <Options extends NoExcessProperties<GenerateTextOptionsWithoutToolkit, Options>>(
123 options: Options & GenerateTextOptionsWithoutToolkit
124 ): Effect.Effect<
125 GenerateTextResponse<{}>,
126 ExtractError<Options>,
127 ExtractServices<Options>
128 >
129 // Generic toolkit: preserve caller-supplied `Tools` in helpers like `<Tools>(toolkit: WithHandler<Tools>) => ...`.
130 <
131 Tools extends Record<string, Tool.Any>,
132 Options extends NoExcessProperties<
133 GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> },
134 Options
135 >
136 >(
137 options: Options & GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }
138 ): Effect.Effect<
139 GenerateTextResponse<Tools, ExtractToolParametersMode<Options>>,
140 ExtractError<Options>,
141 ExtractServices<Options>
142 >
143 // Toolkit unions: recover distributive `ExtractTools<Options>` inference for `toolkitA | toolkitB` call sites.
144 <
145 Options extends {
146 readonly toolkit: ToolkitOption<any>
147 } & NoExcessProperties<GenerateTextOptions<any>, Options>
148 >(
149 options: Options & GenerateTextOptions<ExtractTools<Options>> & { readonly toolkit: Options["toolkit"] }
150 ): Effect.Effect<
151 GenerateTextResponse<ExtractTools<Options>, ExtractToolParametersMode<Options>>,
152 ExtractError<Options>,
153 ExtractServices<Options>
154 >
155 }
156
157 /**
158 * Generate a structured object from a schema using the language model.
159 */
160 readonly generateObject: <
161 ObjectEncoded extends Record<string, any>,
162 StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
163 Options extends NoExcessProperties<
164 GenerateObjectOptions<any, StructuredOutputSchema>,
165 Options
166 >,
167 Tools extends Record<string, Tool.Any> = {}
168 >(
169 options: Options & GenerateObjectOptions<Tools, StructuredOutputSchema>
170 ) => Effect.Effect<
171 GenerateObjectResponse<Tools, StructuredOutputSchema["Type"], ExtractToolParametersMode<Options>>,
172 ExtractError<Options>,
173 ExtractServices<Options> | StructuredOutputSchema["DecodingServices"]
174 >
175
176 /**
177 * Generate text using the language model with streaming output.
178 */
179 readonly streamText: {
180 // No toolkit: force `{}` instead of falling back to `Record<string, Tool.Any>`.
181 <Options extends NoExcessProperties<GenerateTextOptionsWithoutToolkit, Options>>(
182 options: Options & GenerateTextOptionsWithoutToolkit
183 ): Stream.Stream<
184 Response.StreamPart<{}>,
185 ExtractError<Options>,
186 ExtractServices<Options>
187 >
188 // Generic toolkit: preserve caller-supplied `Tools` in helpers like `<Tools>(toolkit: WithHandler<Tools>) => ...`.
189 <
190 Tools extends Record<string, Tool.Any>,
191 Options extends NoExcessProperties<
192 GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> },
193 Options
194 >
195 >(
196 options: Options & GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }
197 ): Stream.Stream<
198 Response.StreamPart<Tools, ExtractToolParametersMode<Options>>,
199 ExtractError<Options>,
200 ExtractServices<Options>
201 >
202 // Toolkit unions: recover distributive `ExtractTools<Options>` inference for `toolkitA | toolkitB` call sites.
203 <
204 Options extends {
205 readonly toolkit: ToolkitOption<any>
206 } & NoExcessProperties<GenerateTextOptions<any>, Options>
207 >(
208 options: Options & GenerateTextOptions<ExtractTools<Options>> & { readonly toolkit: Options["toolkit"] }
209 ): Stream.Stream<
210 Response.StreamPart<ExtractTools<Options>, ExtractToolParametersMode<Options>>,
211 ExtractError<Options>,
212 ExtractServices<Options>
213 >
214 }
215}
216
217/**
218 * A function that transforms a `Schema.Codec` into a provider-compatible form for structured output generation.
219 *
220 * **Details**
221 *
222 * Different language model providers have varying constraints on the JSON
223 * schemas they accept. A `CodecTransformer` rewrites a codec's encoded side to
224 * satisfy those constraints while preserving the decoded type. A provider
225 * schema may be less restrictive than the codec when the provider cannot
226 * express every constraint; the returned codec remains authoritative for
227 * validating model output.
228 *
229 * @stability unstable
230 * @category utility types
231 * @since 4.0.0
232 */
233export type CodecTransformer = <T, E, RD, RE>(schema: Schema.ConstraintCodec<T, E, RD, RE>) => {
234 readonly codec: Schema.ConstraintCodec<T, unknown, RD, RE>
235 readonly jsonSchema: JsonSchema.JsonSchema
236}
237
238/**
239 * The default codec transformer that passes schemas through without
240 * provider-specific rewrites.
241 *
242 * **When to use**
243 *
244 * Use as the codec transformer for provider implementations when the provider
245 * accepts the JSON Schema generated from an `Effect` Schema codec without
246 * provider-specific rewrites.
247 *
248 * **Details**
249 *
250 * The transformer returns the original codec, resolves a top-level `$ref`, and
251 * copies schema definitions into `$defs`.
252 *
253 * @see {@link CodecTransformer} for the structured-output transformer contract
254 * @see {@link make} for where this transformer is used as the default
255 *
256 * @stability unstable
257 * @category services
258 * @since 4.0.0
259 */
260export const defaultCodecTransformer: CodecTransformer = InternalCodecTransformer.defaultCodecTransformer
261
262/**
263 * Configuration options for text generation.
264 *
265 * @stability unstable
266 * @category options
267 * @since 4.0.0
268 */
269export interface GenerateTextOptions<Tools extends Record<string, Tool.Any>> {
270 /**
271 * The prompt input to use to generate text.
272 */
273 readonly prompt: Prompt.RawInput
274
275 /**
276 * A toolkit containing both the tools and the tool call handler to use to
277 * augment text generation.
278 */
279 readonly toolkit?: ToolkitInput<Tools> | undefined
280
281 /**
282 * The tool choice mode for the language model.
283 * - `auto` (default): The model can decide whether or not to call tools, as
284 * well as which tools to call.
285 * - `required`: The model **must** call a tool but can decide which tool will
286 * be called.
287 * - `none`: The model **must not** call a tool.
288 * - `{ tool: <tool_name> }`: The model must call the specified tool.
289 * - `{ mode?: "auto" (default) | "required", "oneOf": [<tool-names>] }`: The
290 * model is restricted to the subset of tools specified by `oneOf`. When
291 * `mode` is `"auto"` or omitted, the model can decide whether or not a tool
292 * from the allowed subset of tools can be called. When `mode` is
293 * `"required"`, the model **must** call one tool from the allowed subset of
294 * tools.
295 */
296 readonly toolChoice?:
297 | ToolChoice<{ [Name in keyof Tools]: Tools[Name]["name"] }[keyof Tools]>
298 | undefined
299
300 /**
301 * The concurrency level for resolving tool calls.
302 */
303 readonly concurrency?: Concurrency | undefined
304
305 /**
306 * When set to `true`, tool calls requested by the large language model are not auto-resolved by the framework.
307 *
308 * **When to use**
309 *
310 * Use when you want to include tool call definitions from an `AiToolkit`
311 * in requests to the large language model, while controlling tool call
312 * resolver execution yourself.
313 */
314 readonly disableToolCallResolution?: boolean | undefined
315}
316
317type GenerateTextOptionsWithoutToolkit = Omit<GenerateTextOptions<{}>, "toolkit"> & {
318 readonly toolkit?: undefined
319}
320
321/**
322 * Configuration options for structured object generation.
323 *
324 * @stability unstable
325 * @category options
326 * @since 4.0.0
327 */
328export interface GenerateObjectOptions<
329 Tools extends Record<string, Tool.Any>,
330 StructuredOutputSchema extends Schema.Top
331> extends GenerateTextOptions<Tools> {
332 /**
333 * The name of the structured output that should be generated. Used by some
334 * large language model providers to provide additional guidance to the model.
335 */
336 readonly objectName?: string | undefined
337
338 /**
339 * The schema to be used to specify the structure of the object to generate.
340 */
341 readonly schema: StructuredOutputSchema
342}
343
344/**
345 * The tool choice mode for the language model.
346 * - `auto` (default): The model can decide whether or not to call tools, as
347 * well as which tools to call.
348 * - `required`: The model **must** call a tool but can decide which tool will
349 * be called.
350 * - `none`: The model **must not** call a tool.
351 * - `{ tool: <tool_name> }`: The model must call the specified tool.
352 * - `{ mode?: "auto" (default) | "required", "oneOf": [<tool-names>] }`: The
353 * model is restricted to the subset of tools specified by `oneOf`. When
354 * `mode` is `"auto"` or omitted, the model can decide whether or not a tool
355 * from the allowed subset of tools can be called. When `mode` is
356 * `"required"`, the model **must** call one tool from the allowed subset of
357 * tools.
358 *
359 * @stability unstable
360 * @category models
361 * @since 4.0.0
362 */
363export type ToolChoice<ToolName extends string> =
364 | "auto"
365 | "none"
366 | "required"
367 | {
368 readonly tool: ToolName
369 }
370 | {
371 readonly mode?: "auto" | "required"
372 readonly oneOf: ReadonlyArray<ToolName>
373 }
374
375/**
376 * Response class for text generation operations, with accessors for extracting text, tool calls, usage information, and other response parts from generated content.
377 *
378 * **Example** (Inspecting a text response)
379 *
380 * ```ts import.meta.vitest
381 * import { LanguageModel, Response } from "effect/ai"
382 *
383 * const response = new LanguageModel.GenerateTextResponse([
384 * Response.makePart("text", { text: "Plants convert light into energy." })
385 * ])
386 *
387 * const result = [response.text, response.finishReason] // => ["Plants convert light into energy.", "unknown"]
388 * ```
389 *
390 * @stability unstable
391 * @category models
392 * @since 4.0.0
393 */
394export class GenerateTextResponse<
395 Tools extends Record<string, Tool.Any>,
396 ParametersMode extends Response.ToolParametersMode = "decoded"
397> {
398 readonly content: Array<Response.Part<Tools, ParametersMode>>
399
400 constructor(content: Array<Response.Part<Tools, ParametersMode>>) {
401 this.content = content
402 }
403
404 /**
405 * Extracts and concatenates all text parts from the response.
406 */
407 get text(): string {
408 const text: Array<string> = []
409 for (const part of this.content) {
410 if (part.type === "text") {
411 text.push(part.text)
412 }
413 }
414 return text.join("")
415 }
416
417 /**
418 * Returns all reasoning parts from the response.
419 */
420 get reasoning(): Array<Response.ReasoningPart> {
421 return this.content.filter((part) => part.type === "reasoning")
422 }
423
424 /**
425 * Extracts and concatenates all reasoning text, or undefined if none exists.
426 */
427 get reasoningText(): string | undefined {
428 const text: Array<string> = []
429 for (const part of this.content) {
430 if (part.type === "reasoning") {
431 text.push(part.text)
432 }
433 }
434 return text.length === 0 ? undefined : text.join("")
435 }
436
437 /**
438 * Returns all tool call parts from the response.
439 */
440 get toolCalls(): Array<Response.ToolCallParts<Tools, ParametersMode>> {
441 return this.content.filter((part) => part.type === "tool-call")
442 }
443
444 /**
445 * Returns all tool result parts from the response.
446 */
447 get toolResults(): Array<Response.ToolResultParts<Tools>> {
448 return this.content.filter((part) => part.type === "tool-result")
449 }
450
451 /**
452 * The reason why text generation finished.
453 */
454 get finishReason(): Response.FinishReason {
455 const finishPart = this.content.find((part) => part.type === "finish")
456 return Predicate.isUndefined(finishPart) ? "unknown" : finishPart.reason
457 }
458
459 /**
460 * Token usage statistics for the generation request.
461 */
462 get usage(): Response.Usage {
463 const finishPart = this.content.find((part) => part.type === "finish")
464 if (Predicate.isUndefined(finishPart)) {
465 return new Response.Usage({
466 inputTokens: {
467 uncached: undefined,
468 total: undefined,
469 cacheRead: undefined,
470 cacheWrite: undefined
471 },
472 outputTokens: {
473 total: undefined,
474 text: undefined,
475 reasoning: undefined
476 }
477 })
478 }
479 return finishPart.usage
480 }
481}
482
483/**
484 * Response class for structured object generation operations.
485 *
486 * **Example** (Inspecting an object response)
487 *
488 * ```ts import.meta.vitest
489 * import { LanguageModel, Response } from "effect/ai"
490 *
491 * const response = new LanguageModel.GenerateObjectResponse(
492 * { name: "John Doe", email: "john@example.com" },
493 * [Response.makePart("text", { text: '{"name":"John Doe","email":"john@example.com"}' })]
494 * )
495 *
496 * response.value // => { name: "John Doe", email: "john@example.com" }
497 * response.text // => '{"name":"John Doe","email":"john@example.com"}'
498 * ```
499 *
500 * @stability unstable
501 * @category models
502 * @since 4.0.0
503 */
504export class GenerateObjectResponse<
505 Tools extends Record<string, Tool.Any>,
506 A,
507 ParametersMode extends Response.ToolParametersMode = "decoded"
508> extends GenerateTextResponse<Tools, ParametersMode> {
509 /**
510 * The parsed structured object that conforms to the provided schema.
511 */
512 readonly value: A
513
514 constructor(value: A, content: Array<Response.Part<Tools, ParametersMode>>) {
515 super(content)
516 this.value = value
517 }
518}
519
520// =============================================================================
521// Utility Types
522// =============================================================================
523
524/**
525 * The supported toolkit option shapes for language model operations.
526 *
527 * @stability unstable
528 * @category utility types
529 * @since 4.0.0
530 */
531export type ToolkitOption<
532 Tools extends Record<string, Tool.Any>,
533 E = never,
534 R = any
535> = Tools extends any ? (
536 | Toolkit.WithHandler<Tools>
537 | Effect.Effect<
538 Toolkit.WithHandler<Tools>,
539 E,
540 R
541 >
542 )
543 : never
544
545/**
546 * The supported toolkit input shapes for language model operation options.
547 *
548 * **Details**
549 *
550 * Unlike `ToolkitOption`, this type does not distribute over unions. It is
551 * intended for call-site assignability, while `ToolkitOption` remains the
552 * distributive helper used for extraction and inference.
553 *
554 * @stability unstable
555 * @category utility types
556 * @since 4.0.0
557 */
558export type ToolkitInput<
559 Tools extends Record<string, Tool.Any>,
560 E = never,
561 R = any
562> =
563 | ToolkitOption<Tools, E, R>
564 | Toolkit.WithHandler<Tools>
565 | Effect.Effect<
566 Toolkit.WithHandler<Tools>,
567 E,
568 R
569 >
570
571type ExtractToolsFromToolkitOption<ToolkitValue> = ToolkitValue extends Toolkit.WithHandler<infer Tools> ? Tools
572 : ToolkitValue extends Effect.Effect<
573 Toolkit.WithHandler<infer _Tools>,
574 infer _E,
575 infer _R
576 > ? _Tools
577 : never
578
579/**
580 * Utility type that extracts the toolset from LanguageModel options.
581 *
582 * @stability unstable
583 * @category utility types
584 * @since 4.0.0
585 */
586export type ExtractTools<Options> = Options extends {
587 readonly toolkit: infer ToolkitValue
588} ? ExtractToolsFromToolkitOption<Exclude<ToolkitValue, undefined>>
589 : {}
590
591/**
592 * Resolves to `"encoded"` when tool call resolution is
593 * disabled, otherwise `"opaque"`.
594 *
595 * @stability unstable
596 * @category utility types
597 * @since 4.0.0
598 */
599export type ExtractToolParametersMode<Options> = Options extends {
600 readonly disableToolCallResolution: true
601} ? "encoded"
602 : "opaque"
603
604type ExtractErrorFromToolkitOption<ToolkitValue, DisableToolCallResolution extends boolean> = ToolkitValue extends
605 Toolkit.WithHandler<infer Tools> ?
606 | AiError.AiError
607 | (DisableToolCallResolution extends true ? never : Tool.HandlerError<Tools[keyof Tools]>)
608 : ToolkitValue extends Effect.Effect<
609 Toolkit.WithHandler<infer _Tools>,
610 infer E,
611 infer _R
612 > ? AiError.AiError | E | (DisableToolCallResolution extends true ? never : Tool.HandlerError<_Tools[keyof _Tools]>)
613 : AiError.AiError
614
615type ExtractServicesFromToolkitOption<ToolkitValue> = ToolkitValue extends Toolkit.WithHandler<infer Tools> ?
616 | Tool.HandlerServices<Tools[keyof Tools]>
617 | Tool.ResultDecodingServices<Tools[keyof Tools]>
618 | Tool.ParametersEncodingServices<Tools[keyof Tools]>
619 : ToolkitValue extends Effect.Effect<
620 Toolkit.WithHandler<infer Tools>,
621 infer _E,
622 infer R
623 > ?
624 | Tool.HandlerServices<Tools[keyof Tools]>
625 | Tool.ResultDecodingServices<Tools[keyof Tools]>
626 | Tool.ParametersEncodingServices<Tools[keyof Tools]>
627 | R
628 : never
629
630// Disabled resolution needs parameter encoding but not handlers or result
631// decoding. Match Toolkit before Effect to omit its handler requirement.
632type ExtractDisabledResolutionServicesFromToolkitOption<ToolkitValue> = ToolkitValue extends
633 Toolkit.WithHandler<infer Tools> ? Tool.ParametersEncodingServices<Tools[keyof Tools]>
634 : ToolkitValue extends Toolkit.Toolkit<infer Tools> ? Tool.ParametersEncodingServices<Tools[keyof Tools]>
635 : ToolkitValue extends Effect.Effect<
636 Toolkit.WithHandler<infer Tools>,
637 infer _E,
638 infer R
639 > ? Tool.ParametersEncodingServices<Tools[keyof Tools]> | R
640 : never
641
642type ExtractToolkitResolutionError<ToolkitValue> = ToolkitValue extends Effect.Effect<
643 Toolkit.WithHandler<infer _Tools>,
644 infer E,
645 infer _R
646> ? E
647 : never
648
649type ExtractToolkitResolutionServices<ToolkitValue> = ToolkitValue extends Effect.Effect<
650 Toolkit.WithHandler<infer _Tools>,
651 infer _E,
652 infer R
653> ? R
654 : never
655
656/**
657 * Utility type that extracts the error type from LanguageModel options.
658 *
659 * **Details**
660 *
661 * Automatically infers the possible error types based on toolkit configuration
662 * and tool call resolution settings.
663 *
664 * @stability unstable
665 * @category utility types
666 * @since 4.0.0
667 */
668export type ExtractError<Options> = Options extends {
669 readonly disableToolCallResolution: true
670 readonly toolkit: infer ToolkitValue
671} ? ExtractErrorFromToolkitOption<Exclude<ToolkitValue, undefined>, true>
672 : Options extends {
673 readonly toolkit: infer ToolkitValue
674 } ? ExtractErrorFromToolkitOption<Exclude<ToolkitValue, undefined>, false>
675 : Options extends {
676 readonly disableToolCallResolution: true
677 } ? AiError.AiError
678 : AiError.AiError
679
680/**
681 * Utility type that extracts the context requirements from LanguageModel options.
682 *
683 * **Details**
684 *
685 * Automatically infers the required services based on the toolkit configuration.
686 *
687 * @stability unstable
688 * @category utility types
689 * @since 4.0.0
690 */
691export type ExtractServices<Options> = Options extends {
692 readonly disableToolCallResolution: true
693} ? Options extends {
694 readonly toolkit: infer ToolkitValue
695 } ? ExtractDisabledResolutionServicesFromToolkitOption<Exclude<ToolkitValue, undefined>>
696 : never
697 : Options extends {
698 readonly toolkit: infer ToolkitValue
699 } ? ExtractServicesFromToolkitOption<Exclude<ToolkitValue, undefined>>
700 : never
701
702// =============================================================================
703// Service Constructor
704// =============================================================================
705
706/**
707 * Configuration options passed along to language model provider implementations.
708 *
709 * **Details**
710 *
711 * This interface defines the normalized options that are passed to the
712 * underlying provider implementation, regardless of the specific provider being
713 * used.
714 *
715 * @stability unstable
716 * @category options
717 * @since 4.0.0
718 */
719export interface ProviderOptions {
720 /**
721 * The prompt messages to use to generate text.
722 */
723 readonly prompt: Prompt.Prompt
724
725 /**
726 * The tools that the large language model will have available to provide
727 * additional information which can be incorporated into its text generation.
728 */
729 readonly tools: ReadonlyArray<Tool.Any>
730
731 /**
732 * The format the response should be provided in.
733 *
734 * **Details**
735 *
736 * If `"text"` is specified, the large language model response is returned as
737 * text. If `"json"` is specified, the large language model response is
738 * provided as a JSON object that conforms to the shape of the specified schema.
739 * The default is `{ type: "text" }`.
740 */
741 readonly responseFormat:
742 | {
743 readonly type: "text"
744 }
745 | {
746 readonly type: "json"
747 readonly objectName: string
748 readonly schema: Schema.Top
749 }
750
751 /**
752 * The tool choice mode for the language model.
753 * - `auto` (default): The model can decide whether or not to call tools, as
754 * well as which tools to call.
755 * - `required`: The model **must** call a tool but can decide which tool will
756 * be called.
757 * - `none`: The model **must not** call a tool.
758 * - `{ tool: <tool_name> }`: The model must call the specified tool.
759 * - `{ mode?: "auto" (default) | "required", "oneOf": [<tool-names>] }`: The
760 * model is restricted to the subset of tools specified by `oneOf`. When
761 * `mode` is `"auto"` or omitted, the model can decide whether or not a tool
762 * from the allowed subset of tools can be called. When `mode` is
763 * `"required"`, the model **must** call one tool from the allowed subset of
764 * tools.
765 */
766 readonly toolChoice: ToolChoice<any>
767
768 /**
769 * The span to use to trace interactions with the large language model.
770 */
771 readonly span: Span
772
773 /**
774 * The previous response identifier for incremental provider calls.
775 */
776 readonly previousResponseId: string | undefined
777
778 /**
779 * The prompt reduced to messages not yet seen by the provider.
780 */
781 readonly incrementalPrompt: Prompt.Prompt | undefined
782
783 /**
784 * Whether this request is retrying an incremental request with the full
785 * prompt after the provider rejected the incremental request.
786 */
787 readonly incrementalFallback?: boolean | undefined
788}
789
790/**
791 * Creates a LanguageModel service from provider-specific text generation and
792 * streaming implementations.
793 *
794 * **When to use**
795 *
796 * Use when you are implementing a provider adapter and need to expose the
797 * standard language-model service while keeping provider-specific request hooks
798 * behind it.
799 *
800 * **Details**
801 *
802 * The returned service implements `generateText`, `generateObject`, and
803 * `streamText`. It prepares `ProviderOptions` for each request, including the
804 * normalized prompt, tools, tool choice, response format, tracing span, and
805 * incremental response fields, before calling the supplied provider hook.
806 * Structured object generation uses the `generateText` hook and the configured
807 * `codecTransformer`, or `defaultCodecTransformer` when none is supplied.
808 *
809 * **Gotchas**
810 *
811 * Provider hooks must return encoded response parts that match the toolkit and
812 * response format prepared in `ProviderOptions`; invalid parts fail decoding as
813 * `AiError.InvalidOutputError`.
814 *
815 * @see {@link LanguageModel} for the returned service contract
816 * @see {@link ProviderOptions} for the normalized options passed to provider hooks
817 * @see {@link defaultCodecTransformer} for the default structured-output schema transformer
818 *
819 * @stability unstable
820 * @category constructors
821 * @since 4.0.0
822 */
823export const make: (params: {
824 /**
825 * A method that requests text generation from the large language model provider and returns the final result when generation finishes.
826 */
827 readonly generateText: (
828 options: ProviderOptions
829 ) => Effect.Effect<Array<Response.PartEncoded>, AiError.AiError, IdGenerator>
830
831 /**
832 * A method that requests text generation from the large language model provider and streams intermediate results.
833 */
834 readonly streamText: (
835 options: ProviderOptions
836 ) => Stream.Stream<Response.StreamPartEncoded, AiError.AiError, IdGenerator>
837
838 /**
839 * A function that transforms a `Schema.Codec` into a provider-compatible form
840 * for structured output generation.
841 */
842 readonly codecTransformer?: CodecTransformer | undefined
843
844 /** Whether the effective provider configuration supports system messages in history. */
845 readonly supportsSystemMessagesInHistory?: Effect.Effect<boolean> | undefined
846}) => Effect.Effect<LanguageModel> = Effect.fnUntraced(function*(params) {
847 const codecTransformer = params.codecTransformer ?? defaultCodecTransformer
848
849 const parentSpanTransformer = yield* Effect.serviceOption(
850 CurrentSpanTransformer
851 )
852 const getSpanTransformer = Effect.serviceOption(
853 CurrentSpanTransformer
854 ).pipe(Effect.map(Option.orElse(() => parentSpanTransformer)))
855
856 const idGenerator = yield* Effect.serviceOption(IdGenerator).pipe(
857 Effect.map(Option.getOrElse(() => defaultIdGenerator))
858 )
859
860 const generateText = <
861 Options extends NoExcessProperties<GenerateTextOptions<any>, Options>,
862 Tools extends Record<string, Tool.Any> = {}
863 >(
864 options: Options & GenerateTextOptions<Tools>
865 ): Effect.Effect<
866 GenerateTextResponse<Tools>,
867 ExtractError<Options>,
868 ExtractServices<Options>
869 > =>
870 Effect.useSpan(
871 "LanguageModel.generateText",
872 {
873 attributes: {
874 concurrency: options.concurrency,
875 toolChoice: options.toolChoice
876 }
877 },
878 Effect.fnUntraced(
879 function*(span) {
880 const spanTransformer = yield* getSpanTransformer
881
882 const providerOptions: Mutable<ProviderOptions> = {
883 prompt: Prompt.make(options.prompt),
884 tools: [],
885 toolChoice: "none",
886 responseFormat: { type: "text" },
887 span,
888 previousResponseId: undefined,
889 incrementalPrompt: undefined
890 }
891 const content = yield* generateContent(options, providerOptions)
892
893 applySpanTransformer(
894 spanTransformer,
895 content as any,
896 providerOptions
897 )
898
899 return new GenerateTextResponse(content)
900 },
901 Effect.catchTag("SchemaError", (error) =>
902 Effect.fail(
903 AiError.make({
904 module: "LanguageModel",
905 method: "generateText",
906 reason: AiError.InvalidOutputError.fromSchemaError(error)
907 })
908 )),
909 (effect, span) => Effect.withParentSpan(effect, span, { captureStackTrace: false }),
910 Effect.provideService(IdGenerator, idGenerator)
911 )
912 ) as any
913
914 const generateObject = <
915 ObjectEncoded extends Record<string, any>,
916 StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
917 Options extends NoExcessProperties<
918 GenerateObjectOptions<any, StructuredOutputSchema>,
919 Options
920 >,
921 Tools extends Record<string, Tool.Any> = {}
922 >(
923 options: Options & GenerateObjectOptions<Tools, StructuredOutputSchema>
924 ): Effect.Effect<
925 GenerateObjectResponse<Tools, StructuredOutputSchema["Type"], ExtractToolParametersMode<Options>>,
926 ExtractError<Options>,
927 ExtractServices<Options> | StructuredOutputSchema["DecodingServices"]
928 > => {
929 const objectName = getObjectName(options.objectName, options.schema)
930 return Effect.useSpan(
931 "LanguageModel.generateObject",
932 {
933 attributes: {
934 objectName,
935 concurrency: options.concurrency,
936 toolChoice: options.toolChoice
937 }
938 },
939 Effect.fnUntraced(
940 function*(span) {
941 const spanTransformer = yield* getSpanTransformer
942
943 const providerOptions: Mutable<ProviderOptions> = {
944 prompt: Prompt.make(options.prompt),
945 tools: [],
946 toolChoice: "none",
947 responseFormat: {
948 type: "json",
949 objectName,
950 schema: options.schema
951 },
952 span,
953 previousResponseId: undefined,
954 incrementalPrompt: undefined
955 }
956
957 const content = yield* generateContent(options, providerOptions)
958
959 applySpanTransformer(
960 spanTransformer,
961 content as any,
962 providerOptions
963 )
964
965 const { codec } = yield* Effect.try({
966 try: () => codecTransformer(options.schema),
967 catch: (error) =>
968 AiError.make({
969 module: "LanguageModel",
970 method: "generateObject",
971 reason: new AiError.UnsupportedSchemaError({
972 description: error instanceof Error ? error.message : String(error)
973 })
974 })
975 })
976
977 const value = yield* resolveStructuredOutput(content as any, codec)
978
979 return new GenerateObjectResponse(value, content)
980 },
981 Effect.catchTag("SchemaError", (error) =>
982 Effect.fail(
983 AiError.make({
984 module: "LanguageModel",
985 method: "generateObject",
986 reason: AiError.InvalidOutputError.fromSchemaError(error)
987 })
988 )),
989 (effect, span) => Effect.withParentSpan(effect, span, { captureStackTrace: false }),
990 Effect.provideService(IdGenerator, idGenerator)
991 )
992 ) as any
993 }
994
995 const streamText: <
996 Options extends NoExcessProperties<GenerateTextOptions<any>, Options>,
997 Tools extends Record<string, Tool.Any> = {}
998 >(
999 options: Options & GenerateTextOptions<Tools>
1000 ) => Stream.Stream<
1001 Response.StreamPart<Tools>,
1002 ExtractError<Options>,
1003 ExtractServices<Options>
1004 > = Effect.fnUntraced(
1005 function*<
1006 Tools extends Record<string, Tool.Any>,
1007 Options extends NoExcessProperties<GenerateTextOptions<Tools>, Options>
1008 >(options: Options & GenerateTextOptions<Tools>) {
1009 const span = yield* Effect.makeSpanScoped("LanguageModel.streamText", {
1010 attributes: {
1011 concurrency: options.concurrency,
1012 toolChoice: options.toolChoice
1013 }
1014 })
1015
1016 const providerOptions: Mutable<ProviderOptions> = {
1017 prompt: Prompt.make(options.prompt),
1018 tools: [],
1019 toolChoice: "none",
1020 responseFormat: { type: "text" },
1021 span,
1022 previousResponseId: undefined,
1023 incrementalPrompt: undefined
1024 }
1025
1026 // Resolve the content stream for the request
1027 const stream = yield* streamContent(options, providerOptions)
1028
1029 // Return the stream immediately if there is no span transformer
1030 const spanTransformer = yield* getSpanTransformer
1031 if (Option.isNone(spanTransformer)) {
1032 return stream
1033 }
1034
1035 // Otherwise aggregate generated content and apply the span transformer
1036 // when the stream is finished
1037 const content: Array<Response.StreamPart<Tools>> = []
1038 return stream.pipe(
1039 Stream.mapArray((parts) => {
1040 content.push(...parts)
1041 return parts
1042 }),
1043 Stream.ensuring(
1044 Effect.sync(() => {
1045 spanTransformer.value({
1046 ...providerOptions,
1047 response: content as any
1048 })
1049 })
1050 )
1051 )
1052 },
1053 Stream.unwrap,
1054 Stream.mapError((error) =>
1055 Schema.isSchemaError(error)
1056 ? AiError.make({
1057 module: "LanguageModel",
1058 method: "streamText",
1059 reason: AiError.InvalidOutputError.fromSchemaError(error)
1060 })
1061 : error
1062 ),
1063 Stream.provideService(IdGenerator, idGenerator)
1064 ) as any
1065
1066 const generateContent: <
1067 Options extends NoExcessProperties<GenerateTextOptions<any>, Options>,
1068 Tools extends Record<string, Tool.Any> = {}
1069 >(
1070 options: Options & GenerateTextOptions<Tools>,
1071 providerOptions: Mutable<ProviderOptions>
1072 ) => Effect.Effect<
1073 Array<Response.Part<Tools>>,
1074 AiError.AiError | Schema.SchemaError,
1075 IdGenerator
1076 > = Effect.fnUntraced(function*<
1077 Tools extends Record<string, Tool.Any>,
1078 Options extends NoExcessProperties<GenerateTextOptions<Tools>, Options>
1079 >(
1080 options: Options & GenerateTextOptions<Tools>,
1081 providerOptions: Mutable<ProviderOptions>
1082 ) {
1083 const tracker = Option.getOrUndefined(yield* Effect.serviceOption(ResponseIdTracker.ResponseIdTracker))
1084 const toolChoice = options.toolChoice ?? "auto"
1085 const concurrency = options.concurrency ?? "unbounded"
1086 providerOptions.span.attribute("concurrency", concurrency)
1087
1088 const generateWithNonIncrementalFallback = () => {
1089 const requestOptions: ProviderOptions = {
1090 ...providerOptions
1091 }
1092 const fallbackPrompt = requestOptions.prompt
1093 const fallbackOptions: ProviderOptions = {
1094 ...requestOptions,
1095 prompt: fallbackPrompt,
1096 incrementalPrompt: undefined,
1097 previousResponseId: undefined,
1098 incrementalFallback: true
1099 }
1100 return requestOptions.incrementalPrompt
1101 ? params.generateText(requestOptions).pipe(
1102 Effect.catchReason("AiError", "InvalidRequestError", (_) => params.generateText(fallbackOptions))
1103 )
1104 : params.generateText(requestOptions)
1105 }
1106
1107 // Check for pending approvals that need resolution
1108 const { approved, denied } = collectToolApprovals(
1109 providerOptions.prompt.content,
1110 { excludeResolved: true }
1111 )
1112 const hasPendingApprovals = approved.length > 0 || denied.length > 0
1113
1114 // If there is no toolkit, the generated content can be returned immediately
1115 if (Predicate.isUndefined(options.toolkit)) {
1116 // But first check if we have pending approvals that require a toolkit
1117 if (hasPendingApprovals) {
1118 return yield* AiError.make({
1119 module: "LanguageModel",
1120 method: "generateText",
1121 reason: new AiError.ToolkitRequiredError({
1122 pendingApprovals: [...approved, ...denied]
1123 .map((result) => result.toolCall?.name)
1124 .filter(Predicate.isNotUndefined)
1125 })
1126 })
1127 }
1128 if (tracker) {
1129 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1130 if (Option.isSome(prepared)) {
1131 providerOptions.previousResponseId = prepared.value.previousResponseId
1132 providerOptions.incrementalPrompt = prepared.value.prompt
1133 }
1134 }
1135 const ResponseSchema = Schema.mutable(
1136 Schema.Array(Response.Part(Toolkit.empty))
1137 )
1138 const rawContent = yield* generateWithNonIncrementalFallback()
1139 const content = yield* Schema.decodeEffect(ResponseSchema)(rawContent)
1140 if (tracker) {
1141 const responseMetadata = content.find((part) => part.type === "response-metadata")
1142 if (Predicate.isNotUndefined(responseMetadata) && Predicate.isNotUndefined(responseMetadata.id)) {
1143 tracker.markParts(providerOptions.prompt.content, responseMetadata.id)
1144 }
1145 }
1146 return content as Array<Response.Part<Tools>>
1147 }
1148
1149 // If there is a toolkit resolve and apply it to the provider options
1150 const toolkit = yield* resolveToolkit<Tools, any, any>(options.toolkit)
1151
1152 // If the resolved toolkit is empty, return the generated content immediately
1153 if (Object.values(toolkit.tools).length === 0) {
1154 // But first check if we have pending approvals that require a toolkit
1155 if (hasPendingApprovals) {
1156 return yield* AiError.make({
1157 module: "LanguageModel",
1158 method: "generateText",
1159 reason: new AiError.ToolkitRequiredError({
1160 pendingApprovals: [...approved, ...denied]
1161 .map((result) => result.toolCall?.name)
1162 .filter(Predicate.isNotUndefined)
1163 })
1164 })
1165 }
1166 if (tracker) {
1167 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1168 if (Option.isSome(prepared)) {
1169 providerOptions.previousResponseId = prepared.value.previousResponseId
1170 providerOptions.incrementalPrompt = prepared.value.prompt
1171 }
1172 }
1173 const ResponseSchema = Schema.mutable(
1174 Schema.Array(Response.Part(Toolkit.empty))
1175 )
1176 const rawContent = yield* generateWithNonIncrementalFallback()
1177 const content = yield* Schema.decodeEffect(ResponseSchema)(rawContent)
1178 if (tracker) {
1179 const responseMetadata = content.find((part) => part.type === "response-metadata")
1180 if (Predicate.isNotUndefined(responseMetadata) && Predicate.isNotUndefined(responseMetadata.id)) {
1181 tracker.markParts(providerOptions.prompt.content, responseMetadata.id)
1182 }
1183 }
1184 return content as Array<Response.Part<Tools>>
1185 }
1186
1187 // Pre-resolve pending tool approvals before calling the LLM
1188 let preResolvedResults: Array<Response.ToolResultPart<string, unknown, unknown>> = []
1189 if (hasPendingApprovals) {
1190 for (const approval of approved) {
1191 if (approval.toolCall && !toolkit.tools[approval.toolCall.name]) {
1192 return yield* AiError.make({
1193 module: "LanguageModel",
1194 method: "generateText",
1195 reason: new AiError.ToolNotFoundError({
1196 toolName: approval.toolCall.name,
1197 availableTools: Object.keys(toolkit.tools)
1198 })
1199 })
1200 }
1201 }
1202
1203 const approvedResults = yield* executeApprovedToolCalls(
1204 approved,
1205 toolkit,
1206 concurrency
1207 )
1208 const deniedResults = createDenialResults(denied)
1209 preResolvedResults = [...approvedResults, ...deniedResults]
1210
1211 if (preResolvedResults.length > 0) {
1212 providerOptions.prompt = Prompt.fromMessages([
1213 ...providerOptions.prompt.content,
1214 ...Prompt.fromResponseParts(preResolvedResults).content
1215 ])
1216 }
1217 }
1218
1219 // Strip all resolved approval artifacts (both current and from previous
1220 // rounds) in a single pass before sending to the provider.
1221 {
1222 const { approved: allResolved, denied: allDenied } = collectToolApprovals(
1223 providerOptions.prompt.content
1224 )
1225 if (allResolved.length > 0 || allDenied.length > 0) {
1226 providerOptions.prompt = stripResolvedApprovals(
1227 providerOptions.prompt,
1228 allResolved,
1229 allDenied
1230 )
1231 }
1232 }
1233
1234 const tools = typeof toolChoice === "object" && "oneOf" in toolChoice
1235 ? Object.values(toolkit.tools).filter((tool) => toolChoice.oneOf.includes(tool.name))
1236 : Object.values(toolkit.tools)
1237 providerOptions.tools = tools
1238 providerOptions.toolChoice = toolChoice
1239
1240 if (tracker) {
1241 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1242 if (Option.isSome(prepared)) {
1243 providerOptions.previousResponseId = prepared.value.previousResponseId
1244 providerOptions.incrementalPrompt = prepared.value.prompt
1245 }
1246 }
1247
1248 const ResponseSchema = Schema.mutable(Schema.Array(Response.Part(
1249 options.disableToolCallResolution === true
1250 ? makeToolkitWithEncodedParameters(toolkit)
1251 : makeToolkitWithOpaqueParameters(toolkit)
1252 )))
1253
1254 // If tool call resolution is disabled, return the response without
1255 // resolving the tool calls that were generated
1256 if (options.disableToolCallResolution === true) {
1257 const rawContent = yield* generateWithNonIncrementalFallback()
1258 const content = yield* Schema.decodeEffect(ResponseSchema)(rawContent)
1259 if (tracker) {
1260 const responseMetadata = content.find((part) => part.type === "response-metadata")
1261 if (Predicate.isNotUndefined(responseMetadata) && Predicate.isNotUndefined(responseMetadata.id)) {
1262 tracker.markParts(providerOptions.prompt.content, responseMetadata.id)
1263 }
1264 }
1265 return [...preResolvedResults, ...content] as Array<Response.Part<Tools>>
1266 }
1267
1268 const rawContent = yield* generateWithNonIncrementalFallback()
1269
1270 // Validate before running tool handlers.
1271 const content = yield* Schema.decodeEffect(ResponseSchema)(rawContent)
1272 yield* validateProviderExecutedToolCalls(toolkit, rawContent)
1273
1274 // Resolve the generated tool calls. When the finish reason indicates an
1275 // incomplete response, handlers do not run and every executable tool call
1276 // gets a synthesized failure result instead.
1277 const incompleteFinishReason = findIncompleteFinishReason(rawContent)
1278 const toolResults = incompleteFinishReason !== undefined
1279 ? rawContent
1280 .filter((part): part is Response.ToolCallPartEncoded =>
1281 part.type === "tool-call" &&
1282 part.providerExecuted !== true &&
1283 toolkit.tools[part.name] !== undefined
1284 )
1285 .map((part) => makeInterruptedToolResult(part, incompleteFinishReason) as ToolResolutionResult<Tools>)
1286 : yield* resolveToolCalls(
1287 rawContent,
1288 toolkit,
1289 providerOptions.prompt.content,
1290 concurrency
1291 ).pipe(
1292 Stream.filter(
1293 (result) =>
1294 result.type === "tool-approval-request" ||
1295 result.preliminary === false
1296 ),
1297 Stream.runCollect
1298 )
1299
1300 if (tracker) {
1301 const responseMetadata = content.find((part) => part.type === "response-metadata")
1302 if (Predicate.isNotUndefined(responseMetadata) && Predicate.isNotUndefined(responseMetadata.id)) {
1303 tracker.markParts(providerOptions.prompt.content, responseMetadata.id)
1304 }
1305 }
1306
1307 // Retain pre-resolved results so Chat can recognize completed approvals on later turns.
1308 return [...preResolvedResults, ...content, ...toolResults] as Array<Response.Part<Tools>>
1309 })
1310
1311 const streamContent: <
1312 Options extends NoExcessProperties<GenerateTextOptions<any>, Options>,
1313 Tools extends Record<string, Tool.Any> = {}
1314 >(
1315 options: Options & GenerateTextOptions<Tools>,
1316 providerOptions: Mutable<ProviderOptions>
1317 ) => Effect.Effect<
1318 Stream.Stream<
1319 Response.StreamPart<Tools>,
1320 AiError.AiError | Schema.SchemaError,
1321 IdGenerator
1322 >,
1323 Options extends { readonly toolkit: infer ToolkitValue } ?
1324 ExtractToolkitResolutionError<Exclude<ToolkitValue, undefined>>
1325 : never,
1326 Options extends { readonly toolkit: infer ToolkitValue } ?
1327 ExtractToolkitResolutionServices<Exclude<ToolkitValue, undefined>>
1328 : never
1329 > = Effect.fnUntraced(function*<
1330 Tools extends Record<string, Tool.Any>,
1331 Options extends NoExcessProperties<GenerateTextOptions<Tools>, Options>
1332 >(
1333 options: Options & GenerateTextOptions<Tools>,
1334 providerOptions: Mutable<ProviderOptions>
1335 ) {
1336 const tracker = Option.getOrUndefined(yield* Effect.serviceOption(ResponseIdTracker.ResponseIdTracker))
1337 const toolChoice = options.toolChoice ?? "auto"
1338 const concurrency = options.concurrency ?? "unbounded"
1339 providerOptions.span.attribute("concurrency", concurrency)
1340
1341 const streamWithNonIncrementalFallback = () => {
1342 const requestOptions: ProviderOptions = {
1343 ...providerOptions
1344 }
1345 const fallbackPrompt = requestOptions.prompt
1346 const fallbackOptions: ProviderOptions = {
1347 ...requestOptions,
1348 prompt: fallbackPrompt,
1349 incrementalPrompt: undefined,
1350 previousResponseId: undefined,
1351 incrementalFallback: true
1352 }
1353 // Only response parts with content count as emitted - a lone
1354 // response-metadata part must not disable the full-prompt fallback.
1355 let emitted = false
1356 return requestOptions.incrementalPrompt
1357 ? params.streamText(requestOptions).pipe(
1358 Stream.tap((part) =>
1359 Effect.sync(() => {
1360 if (part.type !== "response-metadata") emitted = true
1361 })
1362 ),
1363 Stream.catchReason(
1364 "AiError",
1365 "InvalidRequestError",
1366 (_, error) => emitted ? Stream.fail(error) : params.streamText(fallbackOptions)
1367 )
1368 )
1369 : params.streamText(requestOptions)
1370 }
1371
1372 // Check for pending approvals that need resolution
1373 const { approved: pendingApproved, denied: pendingDenied } = collectToolApprovals(providerOptions.prompt.content, {
1374 excludeResolved: true
1375 })
1376 const hasPendingApprovals = pendingApproved.length > 0 || pendingDenied.length > 0
1377
1378 // If there is no toolkit, return immediately
1379 if (Predicate.isUndefined(options.toolkit)) {
1380 // But first check if we have pending approvals that require a toolkit
1381 if (hasPendingApprovals) {
1382 return yield* AiError.make({
1383 module: "LanguageModel",
1384 method: "streamText",
1385 reason: new AiError.ToolkitRequiredError({
1386 pendingApprovals: [...pendingApproved, ...pendingDenied]
1387 .map((a) => a.toolCall?.name)
1388 .filter(Predicate.isNotUndefined)
1389 })
1390 })
1391 }
1392 if (tracker) {
1393 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1394 if (Option.isSome(prepared)) {
1395 providerOptions.previousResponseId = prepared.value.previousResponseId
1396 providerOptions.incrementalPrompt = prepared.value.prompt
1397 }
1398 }
1399 const schema = Schema.NonEmptyArray(Response.StreamPart(Toolkit.empty))
1400 const decodeParts = Schema.decodeEffect(schema)
1401 return pipe(
1402 streamWithNonIncrementalFallback(),
1403 Stream.mapArrayEffect((parts) =>
1404 decodeParts(parts).pipe(
1405 tracker ?
1406 Effect.tap((decodedParts) => {
1407 for (const part of decodedParts) {
1408 if (part.type === "response-metadata" && Predicate.isNotUndefined(part.id)) {
1409 tracker.markParts(providerOptions.prompt.content, part.id)
1410 }
1411 }
1412 return Effect.void
1413 }) :
1414 identity
1415 )
1416 )
1417 ) as Stream.Stream<
1418 Response.StreamPart<Tools>,
1419 AiError.AiError | Schema.SchemaError,
1420 IdGenerator
1421 >
1422 }
1423
1424 // If there is a toolkit resolve and apply it to the provider options
1425 const toolkit = yield* resolveToolkit<Tools, any, any>(options.toolkit)
1426
1427 // If the toolkit is empty, return immediately
1428 if (Object.values(toolkit.tools).length === 0) {
1429 // But first check if we have pending approvals that require a toolkit
1430 if (hasPendingApprovals) {
1431 return yield* AiError.make({
1432 module: "LanguageModel",
1433 method: "streamText",
1434 reason: new AiError.ToolkitRequiredError({
1435 pendingApprovals: [...pendingApproved, ...pendingDenied]
1436 .map((a) => a.toolCall?.name)
1437 .filter(Predicate.isNotUndefined)
1438 })
1439 })
1440 }
1441 if (tracker) {
1442 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1443 if (Option.isSome(prepared)) {
1444 providerOptions.previousResponseId = prepared.value.previousResponseId
1445 providerOptions.incrementalPrompt = prepared.value.prompt
1446 }
1447 }
1448 const schema = Schema.NonEmptyArray(Response.StreamPart(Toolkit.empty))
1449 const decodeParts = Schema.decodeEffect(schema)
1450 return pipe(
1451 streamWithNonIncrementalFallback(),
1452 Stream.mapArrayEffect((parts) =>
1453 decodeParts(parts).pipe(
1454 tracker ?
1455 Effect.tap((decodedParts) => {
1456 for (const part of decodedParts) {
1457 if (part.type === "response-metadata" && part.id) {
1458 tracker.markParts(providerOptions.prompt.content, part.id)
1459 }
1460 }
1461 return Effect.void
1462 }) :
1463 identity
1464 )
1465 )
1466 ) as Stream.Stream<
1467 Response.StreamPart<Tools>,
1468 AiError.AiError | Schema.SchemaError,
1469 IdGenerator
1470 >
1471 }
1472
1473 // Pre-resolve pending tool approvals before calling the LLM
1474 let preResolvedStreamParts: Array<Response.StreamPart<Tools>> = []
1475
1476 if (hasPendingApprovals) {
1477 for (const approval of pendingApproved) {
1478 if (approval.toolCall && !toolkit.tools[approval.toolCall.name]) {
1479 return yield* AiError.make({
1480 module: "LanguageModel",
1481 method: "streamText",
1482 reason: new AiError.ToolNotFoundError({
1483 toolName: approval.toolCall.name,
1484 availableTools: Object.keys(toolkit.tools)
1485 })
1486 })
1487 }
1488 }
1489
1490 const approvedResults = yield* executeApprovedToolCalls(
1491 pendingApproved,
1492 toolkit,
1493 concurrency
1494 )
1495 const deniedResults = createDenialResults(pendingDenied)
1496 const preResolvedResults = [...approvedResults, ...deniedResults]
1497
1498 if (preResolvedResults.length > 0) {
1499 providerOptions.prompt = Prompt.fromMessages([
1500 ...providerOptions.prompt.content,
1501 ...Prompt.fromResponseParts(preResolvedResults).content
1502 ])
1503 }
1504
1505 // Emit pre-resolved tool-results as stream parts so Chat.streamText
1506 // persists them to history. This lets collectToolApprovals find them
1507 // on subsequent rounds and skip the now-resolved approvals.
1508 // Preserve the existing encoded result representation for streaming approvals.
1509 for (const r of preResolvedResults) {
1510 preResolvedStreamParts.push(
1511 Response.makePart("tool-result", {
1512 id: r.id,
1513 name: r.name,
1514 providerExecuted: false,
1515 preliminary: false,
1516 result: r.encodedResult,
1517 encodedResult: r.encodedResult,
1518 isFailure: r.isFailure
1519 }) as Response.StreamPart<Tools>
1520 )
1521 }
1522 }
1523
1524 // Strip all resolved approval artifacts (both current and from previous
1525 // rounds) in a single pass before sending to the provider.
1526 const { approved: allResolved, denied: allDenied } = collectToolApprovals(
1527 providerOptions.prompt.content
1528 )
1529 if (allResolved.length > 0 || allDenied.length > 0) {
1530 providerOptions.prompt = stripResolvedApprovals(
1531 providerOptions.prompt,
1532 allResolved,
1533 allDenied
1534 )
1535 }
1536
1537 const tools = typeof toolChoice === "object" && "oneOf" in toolChoice
1538 ? Object.values(toolkit.tools).filter((tool) => toolChoice.oneOf.includes(tool.name))
1539 : Object.values(toolkit.tools)
1540 providerOptions.tools = tools
1541 providerOptions.toolChoice = toolChoice
1542
1543 if (tracker) {
1544 const prepared = tracker.prepareUnsafe(providerOptions.prompt)
1545 if (Option.isSome(prepared)) {
1546 providerOptions.previousResponseId = prepared.value.previousResponseId
1547 providerOptions.incrementalPrompt = prepared.value.prompt
1548 }
1549 }
1550
1551 const ResponseSchema = Schema.NonEmptyArray(Response.StreamPart(
1552 options.disableToolCallResolution === true
1553 ? makeToolkitWithEncodedParameters(toolkit)
1554 : makeToolkitWithOpaqueParameters(toolkit)
1555 ))
1556 const decodeParts = Schema.decodeEffect(ResponseSchema)
1557
1558 // If tool call resolution is disabled, return the response without
1559 // resolving the tool calls that were generated
1560 if (options.disableToolCallResolution === true) {
1561 return streamWithNonIncrementalFallback().pipe(
1562 Stream.mapArrayEffect((parts) =>
1563 decodeParts(parts).pipe(
1564 tracker ?
1565 Effect.tap((decodedParts) => {
1566 for (const part of decodedParts) {
1567 if (part.type === "response-metadata" && Predicate.isNotUndefined(part.id)) {
1568 tracker.markParts(providerOptions.prompt.content, part.id)
1569 }
1570 }
1571 return Effect.void
1572 }) :
1573 identity
1574 )
1575 )
1576 ) as Stream.Stream<
1577 Response.StreamPart<Tools>,
1578 AiError.AiError | Schema.SchemaError,
1579 IdGenerator
1580 >
1581 }
1582
1583 // Queue for decoded parts and tool results
1584 const queue = yield* Queue.make<
1585 Response.StreamPart<Tools>,
1586 | AiError.AiError
1587 | Cause.Done
1588 | Schema.SchemaError
1589 >()
1590 const deferredFinishParts: Array<Response.StreamPart<Tools>> = []
1591
1592 // Emit pre-resolved tool results so Chat.streamText persists them to
1593 // history. This ensures collectToolApprovals({ excludeResolved }) can
1594 // find the corresponding tool-results on future rounds.
1595 if (preResolvedStreamParts.length > 0) {
1596 yield* Queue.offerAll(queue, preResolvedStreamParts)
1597 }
1598
1599 // FiberSet to track concurrent tool call handlers
1600 const toolCallFibers = yield* FiberSet.make<void, AiError.AiError>()
1601 const toolCallSemaphore = concurrency === "unbounded"
1602 ? undefined
1603 : yield* Semaphore.make(concurrency)
1604 // Tool calls that have been observed but not yet resolved with a final
1605 // result or approval request (id -> tool name)
1606 const pendingToolCalls = new Map<string, string>()
1607 // One-chunk lookahead buffer: a tool call's handler only starts once the
1608 // stream has moved past the call (the next chunk arrives, or the stream
1609 // ends with a complete finish). Providers emit a truncating finish
1610 // back-to-back with the last tool call, so this window is what lets an
1611 // incomplete finish prevent handlers from starting at all.
1612 const bufferedToolCalls: Array<Response.ToolCallPartEncoded> = []
1613
1614 // Helper function to handle tool calls with approval logic
1615 const handleToolCall = Effect.fnUntraced(function*(part: Response.ToolCallPartEncoded) {
1616 const tool = toolkit.tools[part.name]
1617 if (!tool) return
1618
1619 const needsApproval = yield* isApprovalNeeded(
1620 tool,
1621 part,
1622 providerOptions.prompt.content
1623 )
1624
1625 if (needsApproval) {
1626 const idGen = yield* IdGenerator
1627 const approvalId = yield* idGen.generateId()
1628 const approvalPart = Response.makePart("tool-approval-request", {
1629 approvalId,
1630 toolCallId: part.id
1631 }) as Response.StreamPart<Tools>
1632 Queue.offerUnsafe(queue, approvalPart)
1633 pendingToolCalls.delete(part.id)
1634 return
1635 }
1636
1637 yield* toolkit.handle(part.name, part.params as any, part.id).pipe(
1638 Stream.unwrap,
1639 Stream.runForEach((result) => {
1640 const toolResultPart = Response.makePart("tool-result", {
1641 id: part.id,
1642 name: part.name,
1643 providerExecuted: false,
1644 result: result.result,
1645 encodedResult: result.encodedResult,
1646 isFailure: result.isFailure,
1647 preliminary: result.preliminary
1648 }) as Response.StreamPart<Tools>
1649 return Effect.sync(() => {
1650 Queue.offerUnsafe(queue, toolResultPart)
1651 if (result.preliminary !== true) {
1652 pendingToolCalls.delete(part.id)
1653 }
1654 })
1655 })
1656 )
1657 })
1658
1659 const forkBufferedToolCalls = Effect.suspend(() => {
1660 if (bufferedToolCalls.length === 0) return Effect.void
1661 return Effect.forEach(
1662 bufferedToolCalls.splice(0),
1663 (part) => {
1664 const effect = handleToolCall(part)
1665 return FiberSet.run(
1666 toolCallFibers,
1667 toolCallSemaphore ? toolCallSemaphore.withPermit(effect) : effect
1668 )
1669 },
1670 { discard: true }
1671 )
1672 })
1673
1674 yield* streamWithNonIncrementalFallback().pipe(
1675 Stream.runForEachArray(
1676 Effect.fnUntraced(function*(chunk) {
1677 const parts = (yield* decodeParts(chunk)) as ReadonlyArray<Response.StreamPart<Tools>>
1678 yield* validateProviderExecutedToolCalls(toolkit, chunk)
1679 if (tracker) {
1680 for (const part of parts) {
1681 if (part.type === "response-metadata" && part.id) {
1682 tracker.markParts(providerOptions.prompt.content, part.id)
1683 }
1684 }
1685 }
1686 // Defer finish parts until all tool handlers complete. This guarantees
1687 // tool results are emitted before finish in streaming mode.
1688 const immediateParts: Array<Response.StreamPart<Tools>> = []
1689 for (const part of parts) {
1690 if (part.type === "finish") {
1691 deferredFinishParts.push(part)
1692 } else {
1693 immediateParts.push(part)
1694 }
1695 }
1696 if (immediateParts.length > 0) {
1697 yield* Queue.offerAll(queue, immediateParts)
1698 }
1699 // The stream has moved past any previously buffered tool calls, so
1700 // start their handlers now - unless a finish part (possibly
1701 // recorded just above) reports an incomplete response, in which
1702 // case they stay pending and resolve with synthesized failure
1703 // results.
1704 if (findIncompleteFinishReason(deferredFinishParts) === undefined) {
1705 yield* forkBufferedToolCalls
1706 }
1707 // Buffer this chunk's tool calls until the next chunk or the end of
1708 // the stream - use the raw chunk for encoded params
1709 for (const part of chunk) {
1710 if (part.type === "tool-call" && part.providerExecuted !== true) {
1711 if (toolkit.tools[part.name] !== undefined) {
1712 pendingToolCalls.set(part.id, part.name)
1713 }
1714 bufferedToolCalls.push(part)
1715 }
1716 }
1717 })
1718 ),
1719 // Wait for all tool calls to either:
1720 // - complete (FiberSet.awaitEmpty)
1721 // - fail (FiberSet.join)
1722 // If the provider reported an incomplete finish, interrupt any handlers
1723 // that are still running instead and emit synthesized failure results
1724 // for the tool calls left without one, so the response leaves no tool
1725 // call unanswered.
1726 Effect.andThen(
1727 Effect.suspend(() => {
1728 const incompleteFinishReason = findIncompleteFinishReason(deferredFinishParts)
1729 if (incompleteFinishReason === undefined) {
1730 return forkBufferedToolCalls.pipe(
1731 Effect.andThen(Effect.raceFirst(
1732 FiberSet.join(toolCallFibers),
1733 FiberSet.awaitEmpty(toolCallFibers)
1734 ))
1735 )
1736 }
1737 return FiberSet.clear(toolCallFibers).pipe(
1738 Effect.andThen(Effect.suspend(() =>
1739 Queue.offerAll(
1740 queue,
1741 Array.from(pendingToolCalls, ([id, name]) =>
1742 makeInterruptedToolResult({ id, name }, incompleteFinishReason) as Response.StreamPart<Tools>)
1743 )
1744 ))
1745 )
1746 })
1747 ),
1748 Effect.andThen(
1749 Queue.offerAll(queue, deferredFinishParts)
1750 ),
1751 // And then end the queue
1752 Effect.andThen(Queue.end(queue)),
1753 Effect.tapCause((cause) => Queue.failCause(queue, cause)),
1754 Effect.forkScoped
1755 )
1756
1757 return Stream.fromQueue(queue)
1758 }) as any
1759
1760 return LanguageModel.of({
1761 [TypeId]: TypeId,
1762 supportsSystemMessagesInHistory: params.supportsSystemMessagesInHistory,
1763 generateText: generateText as LanguageModel["generateText"],
1764 generateObject: generateObject as LanguageModel["generateObject"],
1765 streamText: streamText as LanguageModel["streamText"]
1766 })
1767})
1768
1769// =============================================================================
1770// Accessors
1771// =============================================================================
1772
1773/**
1774 * Generates text using a language model.
1775 *
1776 * **Example** (Generating text with options)
1777 *
1778 * ```ts import.meta.vitest
1779 * import { Effect, Layer, Stream } from "effect"
1780 * import { LanguageModel } from "effect/ai"
1781 *
1782 * const FakeLanguageModel = Layer.effect(
1783 * LanguageModel.LanguageModel,
1784 * LanguageModel.make({
1785 * generateText: (options) =>
1786 * Effect.succeed([
1787 * {
1788 * type: "text",
1789 * text: options.toolChoice === "none"
1790 * ? "Code flows through types / Errors become values / Programs stay composed"
1791 * : "Unexpected tool choice"
1792 * },
1793 * {
1794 * type: "finish",
1795 * reason: "stop",
1796 * usage: {
1797 * inputTokens: { total: 6 },
1798 * outputTokens: { total: 12 }
1799 * }
1800 * }
1801 * ]),
1802 * streamText: () => Stream.empty
1803 * })
1804 * )
1805 *
1806 * const program = LanguageModel.generateText({
1807 * prompt: "Write a haiku about programming",
1808 * toolChoice: "none"
1809 * }).pipe(
1810 * Effect.map((response) => [response.text, response.usage.inputTokens.total]),
1811 * Effect.provide(FakeLanguageModel)
1812 * )
1813 *
1814 * await Effect.runPromise(program) // => ["Code flows through types / Errors become values / Programs stay composed", 6]
1815 * ```
1816 *
1817 * @stability unstable
1818 * @category text generation
1819 * @since 4.0.0
1820 */
1821export const generateText: {
1822 // No toolkit: force `{}` instead of falling back to `Record<string, Tool.Any>`.
1823 <
1824 Options extends NoExcessProperties<GenerateTextOptionsWithoutToolkit, Options>
1825 >(
1826 options: Options & GenerateTextOptionsWithoutToolkit
1827 ): Effect.Effect<
1828 GenerateTextResponse<{}>,
1829 ExtractError<Options>,
1830 LanguageModel | ExtractServices<Options>
1831 >
1832 // Generic toolkit: preserve caller-supplied `Tools` in helpers like `<Tools>(toolkit: WithHandler<Tools>) => ...`.
1833 <
1834 Tools extends Record<string, Tool.Any>,
1835 Options extends NoExcessProperties<GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }, Options>
1836 >(
1837 options: Options & GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }
1838 ): Effect.Effect<
1839 GenerateTextResponse<Tools, ExtractToolParametersMode<Options>>,
1840 ExtractError<Options>,
1841 LanguageModel | ExtractServices<Options>
1842 >
1843 // Toolkit unions: recover distributive `ExtractTools<Options>` inference for `toolkitA | toolkitB` call sites.
1844 <
1845 Options extends {
1846 readonly toolkit: ToolkitOption<any>
1847 } & NoExcessProperties<GenerateTextOptions<any>, Options>
1848 >(
1849 options: Options & GenerateTextOptions<ExtractTools<Options>> & { readonly toolkit: Options["toolkit"] }
1850 ): Effect.Effect<
1851 GenerateTextResponse<ExtractTools<Options>, ExtractToolParametersMode<Options>>,
1852 ExtractError<Options>,
1853 ExtractServices<Options> | LanguageModel
1854 >
1855} = (options: GenerateTextOptions<any>): Effect.Effect<
1856 GenerateTextResponse<any, any>,
1857 AiError.AiError,
1858 LanguageModel
1859> =>
1860 Effect.flatMap(
1861 Effect.service(LanguageModel),
1862 (model) => model.generateText(options as any)
1863 )
1864
1865/**
1866 * Generates a structured object from a schema using a language model.
1867 *
1868 * **Example** (Generating an object)
1869 *
1870 * ```ts import.meta.vitest
1871 * import { Effect, Layer, Schema, Stream } from "effect"
1872 * import { LanguageModel } from "effect/ai"
1873 *
1874 * const EventSchema = Schema.Struct({
1875 * title: Schema.String,
1876 * date: Schema.String,
1877 * location: Schema.String
1878 * })
1879 *
1880 * const FakeLanguageModel = Layer.effect(
1881 * LanguageModel.LanguageModel,
1882 * LanguageModel.make({
1883 * generateText: () =>
1884 * Effect.succeed([{
1885 * type: "text",
1886 * text: '{"title":"Tech Conference","date":"March 15th","location":"San Francisco"}'
1887 * }]),
1888 * streamText: () => Stream.empty
1889 * })
1890 * )
1891 *
1892 * const program = LanguageModel.generateObject({
1893 * prompt: "Extract event info: Tech Conference on March 15th in San Francisco",
1894 * schema: EventSchema,
1895 * objectName: "event"
1896 * }).pipe(
1897 * Effect.map((response) => response.value),
1898 * Effect.provide(FakeLanguageModel)
1899 * )
1900 *
1901 * await Effect.runPromise(program) // => { title: "Tech Conference", date: "March 15th", location: "San Francisco" }
1902 * ```
1903 *
1904 * @stability unstable
1905 * @category generators
1906 * @since 4.0.0
1907 */
1908export const generateObject = <
1909 ObjectEncoded extends Record<string, any>,
1910 StructuredOutputSchema extends Schema.Encoder<ObjectEncoded, unknown>,
1911 Options extends NoExcessProperties<
1912 GenerateObjectOptions<any, StructuredOutputSchema>,
1913 Options
1914 >
1915>(
1916 options: Options & GenerateObjectOptions<ExtractTools<Options>, StructuredOutputSchema>
1917): Effect.Effect<
1918 GenerateObjectResponse<
1919 ExtractTools<Options>,
1920 StructuredOutputSchema["Type"],
1921 ExtractToolParametersMode<Options>
1922 >,
1923 ExtractError<Options>,
1924 ExtractServices<Options> | StructuredOutputSchema["DecodingServices"] | LanguageModel
1925> =>
1926 Effect.flatMap(
1927 Effect.service(LanguageModel),
1928 (model) => model.generateObject(options as any)
1929 ) as any
1930
1931/**
1932 * Generates text using a language model with streaming output.
1933 *
1934 * **Details**
1935 *
1936 * Returns a stream of response parts that are emitted as soon as they are
1937 * available from the model, enabling real-time text generation experiences.
1938 *
1939 * **Example** (Streaming text deltas)
1940 *
1941 * ```ts import.meta.vitest
1942 * import { Effect, Layer, Stream } from "effect"
1943 * import { LanguageModel } from "effect/ai"
1944 *
1945 * const FakeLanguageModel = Layer.effect(
1946 * LanguageModel.LanguageModel,
1947 * LanguageModel.make({
1948 * generateText: () => Effect.succeed([]),
1949 * streamText: () =>
1950 * Stream.make(
1951 * { type: "text-delta", id: "story", delta: "The explorer reached orbit." },
1952 * { type: "text-delta", id: "story", delta: " Earth glowed below." }
1953 * )
1954 * })
1955 * )
1956 *
1957 * const program = LanguageModel.streamText({
1958 * prompt: "Write a story about a space explorer"
1959 * }).pipe(
1960 * Stream.runFold(() => "", (text, part) => part.type === "text-delta" ? text + part.delta : text),
1961 * Effect.provide(FakeLanguageModel)
1962 * )
1963 *
1964 * await Effect.runPromise(program) // => "The explorer reached orbit. Earth glowed below."
1965 * ```
1966 *
1967 * @stability unstable
1968 * @category text generation
1969 * @since 4.0.0
1970 */
1971export const streamText: {
1972 // No toolkit: force `{}` instead of falling back to `Record<string, Tool.Any>`.
1973 <
1974 Options extends NoExcessProperties<GenerateTextOptionsWithoutToolkit, Options>
1975 >(
1976 options: Options & GenerateTextOptionsWithoutToolkit
1977 ): Stream.Stream<
1978 Response.StreamPart<{}>,
1979 ExtractError<Options>,
1980 ExtractServices<Options> | LanguageModel
1981 >
1982 // Generic toolkit: preserve caller-supplied `Tools` in helpers like `<Tools>(toolkit: WithHandler<Tools>) => ...`.
1983 <
1984 Tools extends Record<string, Tool.Any>,
1985 Options extends NoExcessProperties<GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }, Options>
1986 >(
1987 options: Options & GenerateTextOptions<Tools> & { readonly toolkit: ToolkitInput<Tools> }
1988 ): Stream.Stream<
1989 Response.StreamPart<Tools, ExtractToolParametersMode<Options>>,
1990 ExtractError<Options>,
1991 ExtractServices<Options> | LanguageModel
1992 >
1993 // Toolkit unions: recover distributive `ExtractTools<Options>` inference for `toolkitA | toolkitB` call sites.
1994 <
1995 Options extends {
1996 readonly toolkit: ToolkitOption<any>
1997 } & NoExcessProperties<GenerateTextOptions<any>, Options>
1998 >(
1999 options: Options & GenerateTextOptions<ExtractTools<Options>> & { readonly toolkit: Options["toolkit"] }
2000 ): Stream.Stream<
2001 Response.StreamPart<ExtractTools<Options>, ExtractToolParametersMode<Options>>,
2002 ExtractError<Options>,
2003 ExtractServices<Options> | LanguageModel
2004 >
2005} = (options: GenerateTextOptions<any>): Stream.Stream<
2006 Response.StreamPart<{}>,
2007 AiError.AiError,
2008 LanguageModel
2009> =>
2010 Stream.unwrap(Effect.map(
2011 Effect.service(LanguageModel),
2012 (model) => model.streamText(options as any)
2013 )) as any
2014
2015// =============================================================================
2016// Tool Approval Helpers
2017// =============================================================================
2018
2019interface ApprovalResult {
2020 readonly approvalId: string
2021 readonly toolCallId: string
2022 readonly approved: boolean
2023 readonly reason?: string | undefined
2024 readonly toolCall?: Prompt.ToolCallPart | undefined
2025}
2026
2027interface CollectToolApprovalsOptions {
2028 readonly excludeResolved?: boolean
2029}
2030
2031const collectToolApprovals = (
2032 messages: ReadonlyArray<Prompt.Message>,
2033 options?: CollectToolApprovalsOptions
2034): {
2035 readonly approved: Array<ApprovalResult>
2036 readonly denied: Array<ApprovalResult>
2037} => {
2038 const requests = new Map<
2039 string,
2040 Pick<ApprovalResult, "approvalId" | "toolCallId">
2041 >()
2042 const responses: Array<Omit<ApprovalResult, "toolCallId" | "toolCall">> = []
2043 const toolCallsById = new Map<string, Prompt.ToolCallPart>()
2044 const toolResultIds = new Set<string>()
2045
2046 // Collect all tool approval requests, responses, tool calls, and tool results
2047 for (const message of messages) {
2048 if (message.role === "assistant") {
2049 for (const part of message.content) {
2050 if (part.type === "tool-approval-request") {
2051 requests.set(part.approvalId, {
2052 approvalId: part.approvalId,
2053 toolCallId: part.toolCallId
2054 })
2055 }
2056 if (part.type === "tool-call") {
2057 toolCallsById.set(part.id, part)
2058 }
2059 }
2060 }
2061 if (message.role === "tool") {
2062 for (const part of message.content) {
2063 if (part.type === "tool-approval-response") {
2064 responses.push({
2065 approvalId: part.approvalId,
2066 approved: part.approved,
2067 reason: part.reason
2068 })
2069 }
2070 if (part.type === "tool-result") {
2071 toolResultIds.add(part.id)
2072 }
2073 }
2074 }
2075 }
2076
2077 const approved: Array<ApprovalResult> = []
2078 const denied: Array<ApprovalResult> = []
2079
2080 for (const response of responses) {
2081 const request = requests.get(response.approvalId)
2082 if (Predicate.isNotUndefined(request)) {
2083 // Skip if already resolved
2084 if (options?.excludeResolved && toolResultIds.has(request.toolCallId)) {
2085 continue
2086 }
2087
2088 const result: ApprovalResult = {
2089 ...response,
2090 toolCallId: request.toolCallId,
2091 toolCall: toolCallsById.get(request.toolCallId)
2092 }
2093
2094 if (response.approved) {
2095 approved.push(result)
2096 } else {
2097 denied.push(result)
2098 }
2099 }
2100 }
2101
2102 return { approved, denied }
2103}
2104
2105/**
2106 * Strip resolved approval artifacts from the prompt before sending to the
2107 * provider. After pre-resolving approvals (executing approved tools and
2108 * creating denial results), the original `tool-approval-request` parts in
2109 * assistant messages and `tool-approval-response` parts in tool messages are
2110 * no longer needed. Leaving them in causes provider-specific errors (e.g.
2111 * OpenAI rejects `mcp_approval_response` items that reference approval
2112 * requests it never issued).
2113 */
2114const stripResolvedApprovals = (
2115 prompt: Prompt.Prompt,
2116 approved: ReadonlyArray<ApprovalResult>,
2117 denied: ReadonlyArray<ApprovalResult>
2118): Prompt.Prompt => {
2119 const resolvedApprovalIds = new Set<string>()
2120 for (const a of approved) resolvedApprovalIds.add(a.approvalId)
2121 for (const d of denied) resolvedApprovalIds.add(d.approvalId)
2122
2123 const cleanedMessages: Array<Prompt.Message> = []
2124
2125 for (const message of prompt.content) {
2126 if (message.role === "assistant") {
2127 const filteredContent = message.content.filter(
2128 (part) =>
2129 part.type !== "tool-approval-request" ||
2130 !resolvedApprovalIds.has(part.approvalId)
2131 )
2132 if (filteredContent.length > 0) {
2133 cleanedMessages.push(
2134 Prompt.makeMessage("assistant", {
2135 content: filteredContent,
2136 options: message.options
2137 })
2138 )
2139 }
2140 } else if (message.role === "tool") {
2141 const filteredContent = message.content.filter(
2142 (part) =>
2143 part.type !== "tool-approval-response" ||
2144 !resolvedApprovalIds.has(part.approvalId)
2145 )
2146 if (filteredContent.length > 0) {
2147 cleanedMessages.push(
2148 Prompt.makeMessage("tool", {
2149 content: filteredContent,
2150 options: message.options
2151 })
2152 )
2153 }
2154 } else {
2155 cleanedMessages.push(message)
2156 }
2157 }
2158
2159 return Prompt.fromMessages(cleanedMessages)
2160}
2161
2162const isApprovalNeeded = Effect.fnUntraced(function*<T extends Tool.Any>(
2163 tool: T,
2164 toolCall: Response.ToolCallPartEncoded,
2165 messages: ReadonlyArray<Prompt.Message>
2166): Effect.fn.Return<boolean, Schema.SchemaError, Tool.HandlerServices<T>> {
2167 if (Predicate.isUndefined(tool.needsApproval)) {
2168 return false
2169 }
2170
2171 if (typeof tool.needsApproval === "function") {
2172 const params = yield* Schema.decodeUnknownEffect(tool.parametersSchema)(
2173 toolCall.params
2174 ) as any
2175
2176 const result = tool.needsApproval(params, {
2177 toolCallId: toolCall.id,
2178 messages
2179 })
2180
2181 return Effect.isEffect(result) ? yield* result : result
2182 }
2183
2184 return tool.needsApproval
2185}, Effect.orElseSucceed(constFalse))
2186
2187const executeApprovedToolCalls = <Tools extends Record<string, Tool.Any>>(
2188 approvals: ReadonlyArray<ApprovalResult>,
2189 toolkit: Toolkit.WithHandler<Tools>,
2190 concurrency: Concurrency
2191): Effect.Effect<
2192 Array<Response.ToolResultPart<string, unknown, unknown>>,
2193 Tool.HandlerError<Tools[keyof Tools]> | AiError.AiError,
2194 Tool.HandlerServices<Tools[keyof Tools]>
2195> => {
2196 const executeTool = Effect.fnUntraced(function*(approval: ApprovalResult) {
2197 const toolCall = approval.toolCall
2198
2199 if (Predicate.isUndefined(toolCall)) {
2200 return yield* Effect.die("Approval missing tool call reference")
2201 }
2202
2203 const tool = toolkit.tools[toolCall.name]
2204
2205 if (Predicate.isUndefined(tool)) {
2206 return yield* AiError.make({
2207 module: "LanguageModel",
2208 method: "generateText",
2209 reason: new AiError.ToolNotFoundError({
2210 toolName: toolCall.name,
2211 availableTools: Object.keys(toolkit.tools)
2212 })
2213 })
2214 }
2215
2216 const resultStream = yield* toolkit.handle(
2217 toolCall.name,
2218 toolCall.params as any,
2219 approval.toolCallId
2220 )
2221
2222 const terminalResult = yield* resultStream.pipe(
2223 Stream.filter((result) => result.preliminary === false),
2224 Stream.run(Sink.last()),
2225 Effect.flatMap(
2226 Option.match({
2227 onNone: () => Effect.die("Tool handler did not produce a final result"),
2228 onSome: Effect.succeed
2229 })
2230 )
2231 )
2232
2233 return Response.makePart("tool-result", {
2234 id: approval.toolCallId,
2235 name: toolCall.name,
2236 result: terminalResult.result,
2237 encodedResult: terminalResult.encodedResult,
2238 isFailure: terminalResult.isFailure,
2239 preliminary: terminalResult.preliminary,
2240 providerExecuted: false
2241 })
2242 })
2243
2244 return Effect.forEach(approvals, executeTool, {
2245 concurrency
2246 })
2247}
2248
2249const createDenialResults = (
2250 denials: ReadonlyArray<ApprovalResult>
2251): ReadonlyArray<Response.ToolResultPart<string, unknown, unknown>> => {
2252 const results: Array<Response.ToolResultPart<string, unknown, unknown>> = []
2253 for (const denial of denials) {
2254 if (Predicate.isNotUndefined(denial.toolCall)) {
2255 results.push(
2256 Response.makePart("tool-result", {
2257 id: denial.toolCallId,
2258 name: denial.toolCall.name,
2259 isFailure: true,
2260 result: { type: "execution-denied", reason: denial.reason },
2261 encodedResult: { type: "execution-denied", reason: denial.reason },
2262 preliminary: false,
2263 providerExecuted: false
2264 })
2265 )
2266 }
2267 }
2268 return results
2269}
2270
2271// =============================================================================
2272// Tool Call Resolution
2273// =============================================================================
2274
2275// Finish reasons that indicate the provider completed the response. Anything
2276// else (including "unknown", "other", and future reasons) fails safe and
2277// prevents tool handlers from running.
2278const completeFinishReasons: ReadonlyArray<Response.FinishReason> = ["stop", "tool-calls", "pause"]
2279
2280const findIncompleteFinishReason = (
2281 content: ReadonlyArray<{ readonly type: string; readonly reason?: unknown }>
2282): string | undefined => {
2283 for (const part of content) {
2284 if (
2285 part.type === "finish" &&
2286 !completeFinishReasons.includes(part.reason as Response.FinishReason)
2287 ) {
2288 return typeof part.reason === "string" ? part.reason : "unknown"
2289 }
2290 }
2291 return undefined
2292}
2293
2294// Synthesized failure result for a tool call whose handler was interrupted or
2295// never started because the response finished with an incomplete reason. This
2296// keeps the tool call resolved so the conversation history stays well-formed
2297// for subsequent provider requests.
2298const makeInterruptedToolResult = (
2299 toolCall: { readonly id: string; readonly name: string },
2300 finishReason: string
2301) => {
2302 const result = {
2303 type: "execution-interrupted",
2304 reason: `Tool call execution was interrupted because the response finished with reason "${finishReason}"`
2305 }
2306 return Response.makePart("tool-result", {
2307 id: toolCall.id,
2308 name: toolCall.name,
2309 providerExecuted: false,
2310 preliminary: false,
2311 result,
2312 encodedResult: result,
2313 isFailure: true
2314 })
2315}
2316
2317type ToolResolutionResult<Tools extends Record<string, Tool.Any>> =
2318 | Response.ToolResultPart<
2319 Tool.Name<Tools[keyof Tools]>,
2320 Tool.Success<Tools[keyof Tools]>,
2321 Tool.Failure<Tools[keyof Tools]>
2322 >
2323 | Response.ToolApprovalRequestPart
2324
2325const resolveToolCalls = <Tools extends Record<string, Tool.Any>>(
2326 content: ReadonlyArray<Response.AllPartsEncoded>,
2327 toolkit: Toolkit.WithHandler<Tools>,
2328 messages: ReadonlyArray<Prompt.Message>,
2329 concurrency: Concurrency
2330): Stream.Stream<
2331 ToolResolutionResult<Tools>,
2332 Tool.HandlerError<Tools[keyof Tools]> | AiError.AiError,
2333 Tool.HandlerServices<Tools[keyof Tools]> | IdGenerator
2334> => {
2335 const toolCalls: Array<Response.ToolCallPartEncoded> = []
2336
2337 for (const part of content) {
2338 if (part.type === "tool-call") {
2339 if (part.providerExecuted === true) {
2340 continue
2341 }
2342 toolCalls.push(part)
2343 }
2344 }
2345
2346 const { approved, denied } = collectToolApprovals(messages)
2347 const approvedToolCallIds = new Set(
2348 approved.map((approval) => approval.toolCallId)
2349 )
2350 const deniedByToolCallId = new Map(
2351 denied.map((denial) => [denial.toolCallId, denial])
2352 )
2353
2354 const streams = toolCalls.map((toolCall) =>
2355 Effect.gen(function*() {
2356 const tool = toolkit.tools[toolCall.name]
2357 if (!tool) {
2358 return Stream.empty
2359 }
2360
2361 if (deniedByToolCallId.has(toolCall.id)) {
2362 const denial = deniedByToolCallId.get(toolCall.id)!
2363 return Stream.succeed(
2364 Response.makePart("tool-result", {
2365 id: toolCall.id,
2366 name: toolCall.name,
2367 providerExecuted: false,
2368 isFailure: true,
2369 result: { type: "execution-denied", reason: denial.reason },
2370 encodedResult: { type: "execution-denied", reason: denial.reason },
2371 preliminary: false
2372 }) as ToolResolutionResult<Tools>
2373 )
2374 }
2375
2376 if (approvedToolCallIds.has(toolCall.id)) {
2377 return toolkit.handle(toolCall.name, toolCall.params as any, toolCall.id).pipe(
2378 Stream.unwrap,
2379 Stream.map(
2380 (result) =>
2381 Response.makePart("tool-result", {
2382 id: toolCall.id,
2383 name: toolCall.name,
2384 providerExecuted: false,
2385 result: result.result,
2386 encodedResult: result.encodedResult,
2387 isFailure: result.isFailure,
2388 preliminary: result.preliminary
2389 }) as ToolResolutionResult<Tools>
2390 )
2391 )
2392 }
2393
2394 const needsApproval = yield* isApprovalNeeded(tool, toolCall, messages)
2395 if (needsApproval) {
2396 const generator = yield* IdGenerator
2397 const approvalId = yield* generator.generateId()
2398 return Stream.succeed(
2399 Response.makePart("tool-approval-request", {
2400 approvalId,
2401 toolCallId: toolCall.id
2402 }) as ToolResolutionResult<Tools>
2403 )
2404 }
2405
2406 return toolkit.handle(toolCall.name, toolCall.params as any, toolCall.id).pipe(
2407 Stream.unwrap,
2408 Stream.map(
2409 (result) =>
2410 Response.makePart("tool-result", {
2411 id: toolCall.id,
2412 name: toolCall.name,
2413 providerExecuted: false,
2414 result: result.result,
2415 encodedResult: result.encodedResult,
2416 isFailure: result.isFailure,
2417 preliminary: result.preliminary
2418 }) as ToolResolutionResult<Tools>
2419 )
2420 )
2421 }).pipe(Stream.unwrap)
2422 )
2423
2424 return Stream.mergeAll(streams, { concurrency })
2425}
2426
2427// =============================================================================
2428// Utilities
2429// =============================================================================
2430
2431// Stable parameter-mode copies let Response reuse each tool's part schemas.
2432const parameterModeTools = new WeakMap<Tool.Any, { encoded?: Tool.Any; opaque?: Tool.Any }>()
2433
2434const withParameterMode = (tool: Tool.Any, mode: "encoded" | "opaque"): Tool.Any => {
2435 let tools = parameterModeTools.get(tool)
2436 if (tools === undefined) {
2437 tools = {}
2438 parameterModeTools.set(tool, tools)
2439 }
2440 return tools[mode] ??= tool.setParameters(
2441 mode === "encoded" ? Schema.toEncoded(tool.parametersSchema) : Schema.Unknown
2442 )
2443}
2444
2445const makeToolkitWithEncodedParameters = <Tools extends Record<string, Tool.Any>>(
2446 toolkit: Toolkit.WithHandler<Tools>
2447): Toolkit.Any =>
2448 Toolkit.make(
2449 ...Object.values(toolkit.tools).map((tool) => withParameterMode(tool, "encoded"))
2450 )
2451
2452const makeToolkitWithOpaqueParameters = <Tools extends Record<string, Tool.Any>>(
2453 toolkit: Toolkit.WithHandler<Tools>
2454): Toolkit.Any =>
2455 Toolkit.make(
2456 ...Object.values(toolkit.tools).map((tool) => withParameterMode(tool, "opaque"))
2457 )
2458
2459// Provider-executed tools bypass Toolkit, so validate their parameters here.
2460const validateProviderExecutedToolCalls = <Tools extends Record<string, Tool.Any>>(
2461 toolkit: Toolkit.WithHandler<Tools>,
2462 parts: ReadonlyArray<Response.PartEncoded | Response.StreamPartEncoded>
2463): Effect.Effect<void, Schema.SchemaError> =>
2464 Effect.forEach(
2465 parts,
2466 (part) => {
2467 if (part.type !== "tool-call" || part.providerExecuted !== true) {
2468 return Effect.void
2469 }
2470 const tool = toolkit.tools[part.name]
2471 if (Predicate.isUndefined(tool) || !Schema.isSchema(tool.parametersSchema)) {
2472 return Effect.void
2473 }
2474 return Effect.asVoid(
2475 Schema.decodeUnknownEffect(tool.parametersSchema)(part.params)
2476 ) as Effect.Effect<void, Schema.SchemaError>
2477 },
2478 { discard: true }
2479 )
2480
2481const resolveToolkit = <Tools extends Record<string, Tool.Any>, E, R>(
2482 toolkit: ToolkitInput<Tools, E, R>
2483): Effect.Effect<Toolkit.WithHandler<Tools>, E, R> =>
2484 (Effect.isEffect(toolkit)
2485 ? toolkit
2486 : Effect.succeed(toolkit as unknown as Toolkit.WithHandler<Tools>)) as any
2487
2488/**
2489 * @internal
2490 */
2491export const getObjectName = <StructuredOutputSchema extends Schema.Constraint>(
2492 objectName: string | undefined,
2493 schema: StructuredOutputSchema
2494): string => {
2495 if (Predicate.isNotUndefined(objectName)) {
2496 return objectName
2497 }
2498 if ("identifier" in schema && typeof schema.identifier === "string") {
2499 return schema.identifier
2500 }
2501 const identifier = SchemaAST.resolveIdentifier(schema.ast)
2502 if (typeof identifier === "string") {
2503 return identifier
2504 }
2505 return "generateObject"
2506}
2507
2508const resolveStructuredOutput = Effect.fnUntraced(function*<
2509 StructuredOutputSchema extends Schema.Constraint
2510>(response: ReadonlyArray<Response.AllParts<any>>, schema: StructuredOutputSchema) {
2511 const texts: Array<string> = []
2512 for (const part of response) {
2513 if (part.type === "text") {
2514 texts.push(part.text)
2515 }
2516 }
2517
2518 const text = texts.join("")
2519
2520 if (text.length === 0) {
2521 return yield* AiError.make({
2522 module: "LanguageModel",
2523 method: "generateObject",
2524 reason: new AiError.StructuredOutputError({
2525 description: "No text content in response",
2526 responseText: text
2527 })
2528 })
2529 }
2530
2531 const decode = Schema.decodeEffect(Schema.fromJsonString(schema))
2532 return yield* Effect.mapError(decode(text), (error) =>
2533 AiError.make({
2534 module: "LanguageModel",
2535 method: "generateObject",
2536 reason: AiError.StructuredOutputError.fromSchemaError(error, text)
2537 }))
2538})
2539
2540const applySpanTransformer = (
2541 transformer: Option.Option<SpanTransformer>,
2542 response: ReadonlyArray<Response.AllParts<any>>,
2543 options: ProviderOptions
2544): void => {
2545 if (Option.isSome(transformer)) {
2546 transformer.value({ ...options, response: response as any })
2547 }
2548}