Skip to content

Commit 7704034

Browse files
authored
Preserve response part assignability after narrowing (#7448)
1 parent 505dc05 commit 7704034

3 files changed

Lines changed: 90 additions & 12 deletions

File tree

.changeset/tidy-parts-narrow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"effect": patch
3+
---
4+
5+
Fix response tool part assignability after narrowing generic intersected tool records.

packages/effect/src/unstable/ai/Response.ts

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -411,25 +411,30 @@ export const StreamPart = <T extends Toolkit.Any | Toolkit.WithHandler<any>>(
411411
export type ToolCallParts<
412412
Tools extends Record<string, Tool.Any>,
413413
EncodedParameters extends boolean = false
414-
> = {
415-
[Name in keyof Tools]: Name extends string ? ToolCallPart<
416-
Name,
417-
EncodedParameters extends true ? Tool.ParametersEncoded<Tools[Name]> : Tool.Parameters<Tools[Name]>
418-
>
419-
: never
420-
}[keyof Tools]
414+
> = ToolCallPartForName<Tools, EncodedParameters, keyof Tools>
415+
416+
type ToolCallPartForName<
417+
Tools extends Record<string, Tool.Any>,
418+
EncodedParameters extends boolean,
419+
Name extends keyof Tools
420+
> = Name extends string ? ToolCallPart<
421+
Name,
422+
EncodedParameters extends true ? Tool.ParametersEncoded<Tools[Name]> : Tool.Parameters<Tools[Name]>
423+
>
424+
: never
421425

422426
/**
423427
* Utility type that extracts tool result parts from a set of tools.
424428
*
425429
* @category utility types
426430
* @since 4.0.0
427431
*/
428-
export type ToolResultParts<Tools extends Record<string, Tool.Any>> = {
429-
[Name in keyof Tools]: Name extends string
430-
? ToolResultPart<Name, Tool.Success<Tools[Name]>, Tool.FailureResult<Tools[Name]>>
431-
: never
432-
}[keyof Tools]
432+
export type ToolResultParts<Tools extends Record<string, Tool.Any>> = ToolResultPartForName<Tools, keyof Tools>
433+
434+
type ToolResultPartForName<
435+
Tools extends Record<string, Tool.Any>,
436+
Name extends keyof Tools
437+
> = Name extends string ? ToolResultPart<Name, Tool.Success<Tools[Name]>, Tool.FailureResult<Tools[Name]>> : never
433438

434439
// =============================================================================
435440
// Base Part
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { Schema } from "effect"
2+
import { type Response, Tool } from "effect/unstable/ai"
3+
import { describe, expect, it } from "tstyche"
4+
5+
const BooleanTool = Tool.make("BooleanTool", {
6+
parameters: Schema.Boolean,
7+
success: Schema.String,
8+
failure: Schema.Boolean
9+
})
10+
11+
const TransformTool = Tool.make("TransformTool", {
12+
parameters: Schema.FiniteFromString,
13+
success: Schema.Number,
14+
failure: Schema.Struct({ message: Schema.String })
15+
})
16+
17+
type Tools = {
18+
readonly BooleanTool: typeof BooleanTool
19+
readonly TransformTool: typeof TransformTool
20+
}
21+
22+
describe("Response", () => {
23+
it("preserves generic intersected tool records after narrowing", () => {
24+
const narrow = <
25+
StaticTools extends Record<string, Tool.Any>,
26+
DynamicTools extends Record<string, Tool.Any>,
27+
EncodedParameters extends boolean
28+
>(part: Response.StreamPart<StaticTools & DynamicTools, EncodedParameters>) => {
29+
if (part.type === "error") return
30+
31+
const narrowed: Response.StreamPart<StaticTools & DynamicTools, EncodedParameters> = part
32+
return narrowed
33+
}
34+
35+
void narrow
36+
})
37+
38+
it("preserves tool names with decoded and encoded parameters", () => {
39+
const decoded = null as unknown as Response.ToolCallParts<Tools>
40+
if (decoded.name === "BooleanTool") {
41+
expect(decoded.params).type.toBe<boolean>()
42+
} else {
43+
expect(decoded.params).type.toBe<number>()
44+
}
45+
46+
const encoded = null as unknown as Response.ToolCallParts<Tools, true>
47+
if (encoded.name === "BooleanTool") {
48+
expect(encoded.params).type.toBe<boolean>()
49+
} else {
50+
expect(encoded.params).type.toBe<string>()
51+
}
52+
})
53+
54+
it("preserves tool names with success and failure results", () => {
55+
const result = null as unknown as Response.ToolResultParts<Tools>
56+
if (result.name === "BooleanTool") {
57+
if (result.isFailure) {
58+
expect(result.result).type.toBe<boolean>()
59+
} else {
60+
expect(result.result).type.toBe<string>()
61+
}
62+
} else if (result.isFailure) {
63+
expect(result.result).type.toBe<{ readonly message: string }>()
64+
} else {
65+
expect(result.result).type.toBe<number>()
66+
}
67+
})
68+
})

0 commit comments

Comments
 (0)