@@ -4,7 +4,8 @@ import { Agent } from "@opencode-ai/schema/agent"
44import type { LLM } from "@opencode-ai/schema/llm"
55import { Session } from "@opencode-ai/schema/session"
66import { SessionMessage } from "@opencode-ai/schema/session-message"
7- import { Effect , JsonSchema , Schema , type Scope } from "effect"
7+ import type { StandardJSONSchemaV1 , StandardSchemaV1 } from "@standard-schema/spec"
8+ import { Effect , JsonSchema , Schema } from "effect"
89import type { Hooks , Transform } from "./registration.js"
910
1011export interface Context {
@@ -20,7 +21,34 @@ export interface Progress {
2021 readonly content ?: ReadonlyArray < Content >
2122}
2223
23- export type SchemaType < A > = Schema . Codec < A , any >
24+ export type StandardSchemaType < Input = unknown , Output = Input > = StandardSchemaV1 < Input , Output > &
25+ StandardJSONSchemaV1 < Input , Output >
26+ export type SchemaType < A > = Schema . Codec < A , any > | StandardSchemaType < any , A >
27+ type IsAny < A > = 0 extends 1 & A ? true : false
28+ export type InputValue < S > =
29+ IsAny < S > extends true
30+ ? any
31+ : S extends Schema . Codec < infer A , any >
32+ ? A
33+ : S extends StandardSchemaV1 < any , infer A >
34+ ? A
35+ : never
36+ export type OutputValue < S > =
37+ IsAny < S > extends true
38+ ? any
39+ : S extends Schema . Codec < infer A , any >
40+ ? A
41+ : S extends StandardSchemaV1 < infer A , any >
42+ ? A
43+ : never
44+ export type EncodedValue < S > =
45+ IsAny < S > extends true
46+ ? any
47+ : S extends Schema . Codec < any , infer A >
48+ ? A
49+ : S extends StandardSchemaV1 < any , infer A >
50+ ? A
51+ : never
2452
2553type ToolDefinition = {
2654 readonly name : string
@@ -45,16 +73,6 @@ type ToolOutput = {
4573 readonly content : ReadonlyArray < LLM . ToolContent >
4674}
4775
48- declare const TypeId : unique symbol
49-
50- export interface Definition < Input extends SchemaType < any > , Output extends SchemaType < any > > {
51- readonly [ TypeId ] : {
52- readonly _Input : Input
53- readonly _Output : Output
54- }
55- }
56-
57- export type AnyTool = Definition < any , any >
5876export class Failure extends Schema . TaggedErrorClass < Failure > ( ) ( "LLM.ToolFailure" , {
5977 message : Schema . String ,
6078 error : Schema . optional ( Schema . Defect ( ) ) ,
@@ -70,7 +88,7 @@ export type Content =
7088 | { readonly type : "text" ; readonly text : string }
7189 | { readonly type : "file" ; readonly data : string ; readonly mime : string ; readonly name ?: string }
7290
73- type Config <
91+ export type Definition <
7492 Input extends SchemaType < any > ,
7593 Output extends SchemaType < any > ,
7694 Structured extends SchemaType < any > = Output ,
@@ -79,17 +97,15 @@ type Config<
7997 readonly input : Input
8098 readonly output : Output
8199 readonly structured ?: Structured
100+ readonly permission ?: string
82101 readonly toStructuredOutput ?: ( input : {
83- readonly input : Schema . Schema . Type < Input >
84- readonly output : Output [ "Encoded" ]
85- } ) => Schema . Schema . Type < Structured >
86- readonly execute : (
87- input : Schema . Schema . Type < Input > ,
88- context : Context ,
89- ) => Effect . Effect < Schema . Schema . Type < Output > , Failure >
102+ readonly input : InputValue < Input >
103+ readonly output : EncodedValue < Output >
104+ } ) => OutputValue < Structured >
105+ readonly execute : ( input : InputValue < Input > , context : Context ) => Effect . Effect < OutputValue < Output > , Failure >
90106 readonly toModelOutput ?: ( input : {
91- readonly input : Schema . Schema . Type < Input >
92- readonly output : Output [ "Encoded" ]
107+ readonly input : InputValue < Input >
108+ readonly output : EncodedValue < Output >
93109 } ) => ReadonlyArray < Content >
94110}
95111
@@ -103,109 +119,25 @@ export type DynamicOutput = {
103119 * time (MCP servers, plugin manifests). Input is passed through as `unknown`;
104120 * `execute` returns the already-projected structured value and model content.
105121 */
106- type DynamicConfig = {
122+ export type DynamicDefinition = {
107123 readonly description : string
108124 readonly jsonSchema : JsonSchema . JsonSchema
109125 readonly outputSchema ?: JsonSchema . JsonSchema
110- readonly execute : ( input : unknown , context : Context ) => Effect . Effect < DynamicOutput , Failure >
111- }
112-
113- type Runtime = {
114126 readonly permission ?: string
115- readonly definition : ( name : string ) => ToolDefinition
116- readonly settle : ( call : ToolCall , context : Context ) => Effect . Effect < ToolOutput , Failure >
127+ readonly execute : ( input : unknown , context : Context ) => Effect . Effect < DynamicOutput , Failure >
117128}
118129
119- const runtimes = new WeakMap < AnyTool , Runtime > ( )
130+ export type AnyTool = Definition < any , any , any > | DynamicDefinition
120131
121132export function make <
122133 Input extends SchemaType < any > ,
123134 Output extends SchemaType < any > ,
124135 Structured extends SchemaType < any > = Output ,
125- > ( config : Config < Input , Output , Structured > ) : Definition < Input , Structured >
126- export function make ( config : DynamicConfig ) : AnyTool
127- export function make ( config : Config < any , any , any > | DynamicConfig ) : AnyTool {
128- if ( "jsonSchema" in config ) return makeDynamic ( config )
129- return makeTyped ( config )
130- }
131-
132- function makeTyped <
133- Input extends SchemaType < any > ,
134- Output extends SchemaType < any > ,
135- Structured extends SchemaType < any > = Output ,
136- > ( config : Config < Input , Output , Structured > ) : Definition < Input , Structured > {
137- const tool = Object . freeze ( { } ) as Definition < Input , Structured >
138- const definitions = new Map < string , ToolDefinition > ( )
139- runtimes . set ( tool , {
140- definition : ( name ) => {
141- const cached = definitions . get ( name )
142- if ( cached ) return cached
143- const definition : ToolDefinition = {
144- name,
145- description : config . description ,
146- inputSchema : toJsonSchema ( config . input ) ,
147- outputSchema : toJsonSchema ( config . structured ?? config . output ) ,
148- }
149- definitions . set ( name , definition )
150- return definition
151- } ,
152- settle : ( call , context ) =>
153- Schema . decodeUnknownEffect ( config . input ) ( call . input ) . pipe (
154- Effect . mapError ( ( error ) => new Failure ( { message : `Invalid tool input: ${ error . message } ` } ) ) ,
155- Effect . flatMap ( ( input ) =>
156- config . execute ( input , context ) . pipe (
157- Effect . flatMap ( ( output ) =>
158- Schema . encodeEffect ( config . output ) ( output ) . pipe (
159- Effect . flatMap ( ( output ) => {
160- if ( ! config . structured || ! config . toStructuredOutput )
161- return Effect . succeed ( { output, structured : output } )
162- return Schema . encodeEffect ( config . structured ) ( config . toStructuredOutput ( { input, output } ) ) . pipe (
163- Effect . map ( ( structured ) => ( { output, structured } ) ) ,
164- )
165- } ) ,
166- Effect . mapError (
167- ( error ) =>
168- new Failure ( {
169- message : `Tool returned an invalid value for its output schema: ${ error . message } ` ,
170- } ) ,
171- ) ,
172- ) ,
173- ) ,
174- Effect . map ( ( { output, structured } ) => ( {
175- structured,
176- content :
177- config . toModelOutput ?.( { input, output } ) . map ( toModelContent ) ??
178- ( typeof output === "string" ? [ { type : "text" as const , text : output } ] : [ ] ) ,
179- } ) ) ,
180- ) ,
181- ) ,
182- ) ,
183- } )
184- return tool
185- }
186-
187- function makeDynamic ( config : DynamicConfig ) : AnyTool {
188- const tool = Object . freeze ( { } ) as AnyTool
189- const definitions = new Map < string , ToolDefinition > ( )
190- runtimes . set ( tool , {
191- definition : ( name ) => {
192- const cached = definitions . get ( name )
193- if ( cached ) return cached
194- const definition : ToolDefinition = {
195- name,
196- description : config . description ,
197- inputSchema : config . jsonSchema ,
198- outputSchema : config . outputSchema ,
199- }
200- definitions . set ( name , definition )
201- return definition
202- } ,
203- settle : ( call , context ) =>
204- config
205- . execute ( call . input , context )
206- . pipe ( Effect . map ( ( output ) => ( { structured : output . structured , content : output . content . map ( toModelContent ) } ) ) ) ,
207- } )
208- return tool
136+ > ( config : Definition < Input , Output , Structured > ) : Definition < Input , Output , Structured >
137+ export function make ( config : DynamicDefinition ) : DynamicDefinition
138+ export function make ( config : AnyTool ) : AnyTool
139+ export function make ( config : AnyTool ) : AnyTool {
140+ return config
209141}
210142
211143function toModelContent ( part : Content ) {
@@ -230,23 +162,110 @@ export const registrationEntries = (tools: Readonly<Record<string, AnyTool>>, gr
230162 }
231163 } )
232164
233- export const withPermission = < Input extends SchemaType < any > , Output extends SchemaType < any > > (
234- tool : Definition < Input , Output > ,
165+ export const withPermission = < T extends AnyTool > (
166+ tool : T ,
235167 permission : string ,
236- ) => {
237- const decorated = Object . freeze ( { } ) as Definition < Input , Output >
238- runtimes . set ( decorated , { ...runtimeOf ( tool ) , permission } )
239- return decorated
168+ ) : Omit < T , "permission" > & {
169+ readonly permission : string
170+ } => ( { ...tool , permission } )
171+
172+ export const permission = ( tool : AnyTool , name : string ) => tool . permission ?? name
173+
174+ export const definition = ( name : string , tool : AnyTool ) : ToolDefinition =>
175+ "jsonSchema" in tool
176+ ? {
177+ name,
178+ description : tool . description ,
179+ inputSchema : tool . jsonSchema ,
180+ outputSchema : tool . outputSchema ,
181+ }
182+ : {
183+ name,
184+ description : tool . description ,
185+ inputSchema : inputJsonSchema ( tool . input ) ,
186+ outputSchema : outputJsonSchema ( tool . structured ?? tool . output ) ,
187+ }
188+
189+ export const settle = ( tool : AnyTool , call : ToolCall , context : Context ) : Effect . Effect < ToolOutput , Failure > => {
190+ if ( "jsonSchema" in tool )
191+ return tool
192+ . execute ( call . input , context )
193+ . pipe ( Effect . map ( ( output ) => ( { structured : output . structured , content : output . content . map ( toModelContent ) } ) ) )
194+ return decodeInput ( tool . input , call . input ) . pipe (
195+ Effect . flatMap ( ( input ) =>
196+ tool . execute ( input , context ) . pipe (
197+ Effect . flatMap ( ( value ) =>
198+ encodeOutput ( tool . output , value ) . pipe (
199+ Effect . flatMap ( ( output ) => {
200+ if ( ! tool . structured || ! tool . toStructuredOutput ) return Effect . succeed ( { output, structured : output } )
201+ return encodeOutput ( tool . structured , tool . toStructuredOutput ( { input, output } ) ) . pipe (
202+ Effect . map ( ( structured ) => ( { output, structured } ) ) ,
203+ )
204+ } ) ,
205+ ) ,
206+ ) ,
207+ Effect . map ( ( { output, structured } ) => ( {
208+ structured,
209+ content :
210+ tool . toModelOutput ?.( { input, output } ) . map ( toModelContent ) ??
211+ ( typeof output === "string" ? [ { type : "text" as const , text : output } ] : [ ] ) ,
212+ } ) ) ,
213+ ) ,
214+ ) ,
215+ )
216+ }
217+
218+ function decodeInput ( schema : SchemaType < any > , value : unknown ) : Effect . Effect < any , Failure > {
219+ if ( Schema . isSchema ( schema ) )
220+ return Schema . decodeUnknownEffect ( schema ) ( value ) . pipe (
221+ Effect . mapError ( ( error ) => new Failure ( { message : `Invalid tool input: ${ error . message } ` } ) ) ,
222+ )
223+ return validateStandard ( schema , value , "Invalid tool input" )
224+ }
225+
226+ function encodeOutput ( schema : SchemaType < any > , value : unknown ) : Effect . Effect < any , Failure > {
227+ if ( Schema . isSchema ( schema ) )
228+ return Schema . encodeEffect ( schema ) ( value ) . pipe (
229+ Effect . mapError (
230+ ( error ) => new Failure ( { message : `Tool returned an invalid value for its output schema: ${ error . message } ` } ) ,
231+ ) ,
232+ )
233+ return validateStandard ( schema , value , "Tool returned an invalid value for its output schema" )
240234}
241235
242- export const permission = ( tool : AnyTool , name : string ) => runtimeOf ( tool ) . permission ?? name
243- export const definition = ( name : string , tool : AnyTool ) => runtimeOf ( tool ) . definition ( name )
244- export const settle = ( tool : AnyTool , call : ToolCall , context : Context ) => runtimeOf ( tool ) . settle ( call , context )
236+ function validateStandard ( schema : StandardSchemaType , value : unknown , prefix : string ) : Effect . Effect < unknown , Failure > {
237+ const result = Effect . try ( {
238+ try : ( ) => schema [ "~standard" ] . validate ( value ) ,
239+ catch : ( error ) => new Failure ( { message : `${ prefix } : ${ error instanceof Error ? error . message : String ( error ) } ` } ) ,
240+ } )
241+ return result . pipe (
242+ Effect . flatMap ( ( result ) =>
243+ result instanceof Promise
244+ ? Effect . tryPromise ( {
245+ try : ( ) => result ,
246+ catch : ( error ) =>
247+ new Failure ( { message : `${ prefix } : ${ error instanceof Error ? error . message : String ( error ) } ` } ) ,
248+ } )
249+ : Effect . succeed ( result ) ,
250+ ) ,
251+ Effect . flatMap ( ( result ) =>
252+ result . issues
253+ ? Effect . fail ( new Failure ( { message : `${ prefix } : ${ result . issues . map ( ( issue ) => issue . message ) . join ( ", " ) } ` } ) )
254+ : Effect . succeed ( result . value ) ,
255+ ) ,
256+ )
257+ }
258+
259+ function inputJsonSchema ( schema : SchemaType < any > ) : JsonSchema . JsonSchema {
260+ if ( ! Schema . isSchema ( schema ) )
261+ return schema [ "~standard" ] . jsonSchema . input ( { target : "draft-2020-12" } ) as JsonSchema . JsonSchema
262+ return toJsonSchema ( schema )
263+ }
245264
246- function runtimeOf ( tool : AnyTool ) {
247- const runtime = runtimes . get ( tool )
248- if ( ! runtime ) throw new TypeError ( "Invalid Tool value" )
249- return runtime
265+ function outputJsonSchema ( schema : SchemaType < any > ) : JsonSchema . JsonSchema {
266+ if ( ! Schema . isSchema ( schema ) )
267+ return schema [ "~standard" ] . jsonSchema . output ( { target : "draft-2020-12" } ) as JsonSchema . JsonSchema
268+ return toJsonSchema ( schema )
250269}
251270
252271function toJsonSchema ( schema : Schema . Top ) : JsonSchema . JsonSchema {
0 commit comments