import { Effect, Schema } from "effect" import type { Namespace } from "./namespace.js" import type { Tools } from "./tools.js" /** * JSON Schema subset for model-visible signatures. CodeMode does not validate values against * these schemas. */ export type JsonSchema = { readonly type?: string | ReadonlyArray readonly enum?: ReadonlyArray readonly const?: unknown readonly anyOf?: ReadonlyArray readonly oneOf?: ReadonlyArray readonly allOf?: ReadonlyArray readonly not?: JsonSchema readonly properties?: Readonly> readonly required?: ReadonlyArray readonly items?: JsonSchema readonly additionalProperties?: boolean | JsonSchema readonly description?: string readonly default?: unknown readonly format?: string readonly deprecated?: boolean readonly minimum?: number readonly maximum?: number readonly exclusiveMinimum?: number readonly exclusiveMaximum?: number readonly multipleOf?: number readonly minLength?: number readonly maxLength?: number readonly pattern?: string readonly minItems?: number readonly maxItems?: number readonly uniqueItems?: boolean readonly $ref?: string readonly $defs?: Readonly> readonly definitions?: Readonly> } /** Either a validating Effect Schema or a render-only JSON Schema document. */ export type SchemaType = Schema.Decoder | JsonSchema /** Context passed to a tool call: its stable idempotency key (`:`). */ export type ToolContext = { readonly key: string } export type ExecuteFn = ( input: Input, context: ToolContext, ) => Effect.Effect /** * A tool execution wrapper for idempotent work that is safe to run again. If a suspend lands while the call is running * (`engine: "vm"`), the VM interrupts it without firing `tool.after`, keeps the call queued in the snapshot, and * re-runs it on resume with the same `key` without double-counting against `maxToolCalls`. */ export type Idempotent = { (input: Input, context: ToolContext): Effect.Effect readonly _tag: "CodeModeIdempotent" readonly execute: ExecuteFn } /** * Options for `Tool.durable`: `start` begins the work under `key` and returns JSON `data` saved in the snapshot; * `wait` connects to the running work and waits for its output; `cancel` stops the work when the run is interrupted * for real or ends, never when it is suspended. `wait` is not tied to the program's JS `await`: the VM calls `wait` * right after `start` returns, and again for every `started` call right at resume, whatever the program does with the * promise. */ export type DurableOptions< Data extends Schema.Json, Input = unknown, Output = unknown, RStart = never, RWait = never, RCancel = never, > = { readonly start: (input: Input, context: ToolContext) => Effect.Effect readonly wait: (data: NoInfer, context: ToolContext) => Effect.Effect readonly cancel: (data: NoInfer) => Effect.Effect } /** A tool execution wrapper for work that keeps running across a suspend and reconnects through its JSON data. */ export type Durable = { (input: Input, context: ToolContext): Effect.Effect readonly _tag: "CodeModeDurable" readonly start: (input: Input, context: ToolContext) => Effect.Effect readonly wait: (data: never, context: ToolContext) => Effect.Effect readonly cancel: (data: never) => Effect.Effect } /** Executable tool exposed through CodeMode's `tools` object. */ export type Tool = { readonly _tag: "CodeModeTool" readonly description: string readonly input: SchemaType readonly freeform?: boolean readonly output: SchemaType | undefined readonly execute: (input: unknown, context?: ToolContext) => Effect.Effect readonly mode?: Idempotent | Durable | undefined } type InputType = S extends Schema.Decoder ? S["Type"] : unknown type ResultType = S extends undefined ? void : S extends Schema.Decoder ? S["Encoded"] : unknown /** Options for declaring one CodeMode tool. */ export type Options = { readonly description: string readonly input: I /** * The tool's input is one required string field that programs pass directly, as in `tools.patch("...")`. * The object form still works, and the executor receives the object either way. */ readonly freeform?: boolean readonly output?: O readonly execute: | ExecuteFn, ResultType, R> | Idempotent, ResultType, R> | Durable, ResultType, R> } export const isTool = (value: Tool | Namespace | Tools | undefined): value is Tool => value !== undefined && Object.hasOwn(value, "_tag") && value._tag === "CodeModeTool" /** Marks a tool's `execute` function as safe to interrupt on suspend and re-run on resume with the same key. */ export const idempotent = ( execute: ExecuteFn, ): Idempotent => Object.assign((input: Input, context: ToolContext) => execute(input, context), { _tag: "CodeModeIdempotent" as const, execute, }) /** * Wraps a tool's execution as `start`, `wait`, and `cancel` so work started before a suspend keeps running and * reconnects through its JSON data on resume. */ export const durable = < Data extends Schema.Json, Input = unknown, Output = unknown, RStart = never, RWait = never, RCancel = never, >( options: DurableOptions, ): Durable => { type R = RStart | RWait | RCancel const wait = options.wait as (data: Schema.Json, context: ToolContext) => Effect.Effect return Object.assign( (input: Input, context: ToolContext): Effect.Effect => Effect.flatMap(options.start(input, context), (data) => wait(data, context)), { _tag: "CodeModeDurable" as const, start: options.start, wait: options.wait as (data: never, context: ToolContext) => Effect.Effect, cancel: options.cancel as (data: never) => Effect.Effect, }, ) } /** * Declares one schema-described tool available to a CodeMode program through `tools.*`. * * Effect Schemas validate values; JSON Schemas only shape the model-visible signature. * Without `output`, results are exposed as `void`. Hosts remain responsible for authorization * and durable side effects. */ export const make = ( options: Options, ): Tool => { const spec = options.execute const mode = "_tag" in spec ? (spec as Idempotent | Durable) : undefined return { _tag: "CodeModeTool", description: options.description, input: options.input, ...(options.freeform && { freeform: true }), output: options.output, ...(mode === undefined ? {} : { mode }), execute: (input, context = { key: crypto.randomUUID() }) => spec(input as InputType, context), } }