Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions .changeset/great-eyes-buy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
"@effect/ai": patch
---

Remove `Either` / `EitherEncoded` from tool call results.

Specifically, the encoding of tool call results as an `Either` / `EitherEncoded` has been removed and is replaced by encoding the tool call success / failure directly into the `result` property.

To allow type-safe discrimination between a tool call result which was a success vs. one that was a failure, an `isFailure` property has also been added to the `"tool-result"` part. If `isFailure` is `true`, then the tool call handler result was an error.

```ts
import * as AnthropicClient from "@effect/ai-anthropic/AnthropicClient"
import * as AnthropicLanguageModel from "@effect/ai-anthropic/AnthropicLanguageModel"
import * as LanguageModel from "@effect/ai/LanguageModel"
import * as Tool from "@effect/ai/Tool"
import * as Toolkit from "@effect/ai/Toolkit"
import * as NodeHttpClient from "@effect/platform-node/NodeHttpClient"
import { Config, Effect, Layer, Schema, Stream } from "effect"

const Claude = AnthropicLanguageModel.model("claude-4-sonnet-20250514")

const MyTool = Tool.make("MyTool", {
description: "An example of a tool with success and failure types",
failureMode: "return", // Return errors in the response
parameters: { bar: Schema.Number },
success: Schema.Number,
failure: Schema.Struct({ reason: Schema.Literal("reason-1", "reason-2") })
})

const MyToolkit = Toolkit.make(MyTool)

const MyToolkitLayer = MyToolkit.toLayer({
MyTool: () => Effect.succeed(42)
})

const program = LanguageModel.streamText({
prompt: "Tell me about the meaning of life",
toolkit: MyToolkit
}).pipe(
Stream.runForEach((part) => {
if (part.type === "tool-result" && part.name === "MyTool") {
// The `isFailure` property can be used to discriminate whether the result
// of a tool call is a success or a failure
if (part.isFailure) {
part.result
// ^? { readonly reason: "reason-1" | "reason-2"; }
} else {
part.result
// ^? number
}
}
return Effect.void
}),
Effect.provide(Claude)
)

const Anthropic = AnthropicClient.layerConfig({
apiKey: Config.redacted("ANTHROPIC_API_KEY")
}).pipe(Layer.provide(NodeHttpClient.layerUndici))

program.pipe(
Effect.provide([Anthropic, MyToolkitLayer]),
Effect.runPromise
)
```
3 changes: 2 additions & 1 deletion packages/ai/ai/src/LanguageModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1010,12 +1010,13 @@ const resolveToolCalls = <Tools extends Record<string, Tool.Any>>(

return Effect.forEach(toolCalls, (toolCall) => {
return toolkit.handle(toolCall.name, toolCall.params as any).pipe(
Effect.map(({ encodedResult, result }) =>
Effect.map(({ encodedResult, isFailure, result }) =>
Response.makePart("tool-result", {
id: toolCall.id,
name: toolCall.name,
result,
encodedResult,
isFailure,
providerExecuted: false,
...(toolCall.providerName !== undefined
? { providerName: toolCall.providerName }
Expand Down
53 changes: 29 additions & 24 deletions packages/ai/ai/src/Prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -606,13 +606,11 @@ export const toolCallPart = (params: PartConstructorParams<ToolCallPart>): ToolC
* const toolResultPart: Prompt.ToolResultPart = Prompt.makePart("tool-result", {
* id: "call_123",
* name: "get_weather",
* isFailure: false,
* result: {
* _tag: "Right",
* right: {
* temperature: 22,
* condition: "sunny",
* humidity: 65
* }
* temperature: 22,
* condition: "sunny",
* humidity: 65
* }
* })
* ```
Expand All @@ -629,10 +627,14 @@ export interface ToolResultPart extends BasePart<"tool-result", ToolResultPartOp
* Name of the tool that was executed.
*/
readonly name: string
/**
* Whether or not the result of executing the tool call handler was an error.
*/
readonly isFailure: boolean
/**
* The result returned by the tool execution.
*/
readonly result: Schema.EitherEncoded<unknown, unknown>
readonly result: unknown
}

/**
Expand All @@ -650,10 +652,14 @@ export interface ToolResultPartEncoded extends BasePartEncoded<"tool-result", To
* Name of the tool that was executed.
*/
readonly name: string
/**
* Whether or not the result of executing the tool call handler was an error.
*/
readonly isFailure: boolean
/**
* The result returned by the tool execution.
*/
readonly result: Schema.EitherEncoded<unknown, unknown>
readonly result: unknown
}

/**
Expand All @@ -675,10 +681,8 @@ export const ToolResultPart: Schema.Schema<ToolResultPart, ToolResultPartEncoded
type: Schema.Literal("tool-result"),
id: Schema.String,
name: Schema.String,
result: Schema.encodedSchema(Schema.Either({
left: Schema.Unknown,
right: Schema.Unknown
})),
isFailure: Schema.Boolean,
result: Schema.Unknown,
options: Schema.optionalWith(ProviderOptions, { default: constEmptyObject })
}).pipe(
Schema.attachPropertySignature(PartTypeId, PartTypeId),
Expand Down Expand Up @@ -1028,9 +1032,10 @@ export const userMessage = (params: MessageConstructorParams<UserMessage>): User
* Prompt.makePart("tool-result", {
* id: "call_123",
* name: "get_weather",
* isFailure: false,
* result: {
* _tag: "Right",
* right: { temperature: 72, condition: "sunny" }
* temperature: 72,
* condition: "sunny"
* }
* }),
* Prompt.makePart("text", {
Expand Down Expand Up @@ -1138,15 +1143,13 @@ export const assistantMessage = (params: MessageConstructorParams<AssistantMessa
* Prompt.makePart("tool-result", {
* id: "call_123",
* name: "search_web",
* isFailure: false,
* result: {
* _tag: "Right",
* right: {
* query: "TypeScript best practices",
* results: [
* { title: "TypeScript Handbook", url: "https://..." },
* { title: "Effective TypeScript", url: "https://..." }
* ]
* }
* query: "TypeScript best practices",
* results: [
* { title: "TypeScript Handbook", url: "https://..." },
* { title: "Effective TypeScript", url: "https://..." }
* ]
* }
* })
* ]
Expand Down Expand Up @@ -1564,8 +1567,9 @@ const isValidPart = (part: Response.AnyPart): part is ValidResponsePart => {
* Response.makePart("tool-result", {
* id: "call_1",
* name: "get_time",
* result: Either.right("10:30 AM"),
* encodedResult: { _tag: "Right", right: "10:30 AM" },
* isFailure: false,
* result: "10:30 AM",
* encodedResult: "10:30 AM",
* providerExecuted: false
* })
* ]
Expand Down Expand Up @@ -1650,6 +1654,7 @@ export const fromResponseParts = (parts: ReadonlyArray<Response.AnyPart>): Promp
toolParts.push(makePart("tool-result", {
id: part.id,
name: part.providerName ?? part.name,
isFailure: part.isFailure,
result: part.encodedResult
}))
break
Expand Down
Loading