forked from anomalyco/opencode
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtool-define.test.ts
More file actions
154 lines (134 loc) · 5.14 KB
/
Copy pathtool-define.test.ts
File metadata and controls
154 lines (134 loc) · 5.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
import { describe, expect } from "bun:test"
import { LayerNode } from "@opencode-ai/core/effect/layer-node"
import { Cause, Effect, Exit, Schema } from "effect"
import { Agent } from "../../src/agent/agent"
import { MessageID, SessionID } from "../../src/session/schema"
import { Tool } from "@/tool/tool"
import { Truncate } from "@/tool/truncate"
import { testEffect } from "../lib/effect"
const it = testEffect(LayerNode.compile(LayerNode.group([Truncate.node, Agent.node])))
const params = Schema.Struct({ input: Schema.String })
function makeCtx(): Tool.Context {
return {
sessionID: SessionID.descending(),
messageID: MessageID.ascending(),
agent: "build",
abort: new AbortController().signal,
messages: [],
metadata() {
return Effect.void
},
ask() {
return Effect.void
},
}
}
function makeTool(id: string, executeFn?: () => void) {
return {
description: "test tool",
parameters: params,
execute() {
executeFn?.()
return Effect.succeed({ title: "test", output: "ok", metadata: {} })
},
}
}
describe("Tool.define", () => {
it.effect("object-defined tool does not mutate the original init object", () =>
Effect.gen(function* () {
const original = makeTool("test")
const originalExecute = original.execute
const info = yield* Tool.define("test-tool", Effect.succeed(original))
yield* info.init()
yield* info.init()
yield* info.init()
expect(original.execute).toBe(originalExecute)
}),
)
it.effect("effect-defined tool returns fresh objects and is unaffected", () =>
Effect.gen(function* () {
const info = yield* Tool.define(
"test-fn-tool",
Effect.succeed(() => Effect.succeed(makeTool("test"))),
)
const first = yield* info.init()
const second = yield* info.init()
expect(first).not.toBe(second)
}),
)
it.effect("object-defined tool returns distinct objects per init() call", () =>
Effect.gen(function* () {
const info = yield* Tool.define("test-copy", Effect.succeed(makeTool("test")))
const first = yield* info.init()
const second = yield* info.init()
expect(first).not.toBe(second)
}),
)
it.effect("execute receives decoded parameters", () =>
Effect.gen(function* () {
const parameters = Schema.Struct({
count: Schema.NumberFromString.pipe(Schema.optional, Schema.withDecodingDefaultType(Effect.succeed(5))),
})
const calls: Array<Schema.Schema.Type<typeof parameters>> = []
const info = yield* Tool.define(
"test-decoded",
Effect.succeed({
description: "test tool",
parameters,
execute(args: Schema.Schema.Type<typeof parameters>) {
calls.push(args)
return Effect.succeed({ title: "test", output: "ok", metadata: { truncated: false } })
},
}),
)
const ctx = makeCtx()
const tool = yield* info.init()
const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType<typeof tool.execute>
yield* execute({}, ctx)
yield* execute({ count: "7" }, ctx)
expect(calls).toEqual([{ count: 5 }, { count: 7 }])
}),
)
// Regression for #28438: the wrap is the canonical "untyped → typed" boundary.
// When the LLM emits a tool call with a payload that fails the parameter
// schema, the wrap must surface a typed `Tool.InvalidArgumentsError` whose
// `.message` is the actionable prose the AI SDK feeds back to the model.
it.effect("invalid args surface as Tool.InvalidArgumentsError with friendly message and JSON path", () =>
Effect.gen(function* () {
const parameters = Schema.Struct({
questions: Schema.Array(
Schema.Struct({
question: Schema.String,
options: Schema.Array(Schema.String),
}),
),
})
const info = yield* Tool.define(
"qtest",
Effect.succeed({
description: "test tool",
parameters,
execute() {
return Effect.succeed({ title: "ok", output: "ok", metadata: { truncated: false } })
},
}),
)
const tool = yield* info.init()
const execute = tool.execute as unknown as (args: unknown, ctx: Tool.Context) => ReturnType<typeof tool.execute>
// Missing required `question` field on the first questions[] entry.
const exit = yield* execute({ questions: [{ options: ["a"] }] }, makeCtx()).pipe(Effect.exit)
expect(Exit.isFailure(exit)).toBe(true)
if (!Exit.isFailure(exit)) return
// The wrap ends with Effect.orDie, so the failure lives in the cause as a
// defect. Recover the typed instance from there.
const die = exit.cause.reasons.find(Cause.isDieReason)
const error = die?.defect
expect(error).toBeInstanceOf(Tool.InvalidArgumentsError)
const args = error as Tool.InvalidArgumentsError
expect(args.tool).toBe("qtest")
expect(args.message).toContain("qtest tool was called with invalid arguments")
expect(args.message).toContain("Please rewrite the input")
expect(args.message).toContain(`["questions"][0]["question"]`)
}),
)
})