@neurocode-ai/codemode 1.18.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +23 -0
- package/README.md +369 -0
- package/codemode.md +176 -0
- package/package.json +25 -0
- package/src/codemode.ts +159 -0
- package/src/index.ts +4 -0
- package/src/interpreter/model.ts +201 -0
- package/src/interpreter/runtime.ts +3465 -0
- package/src/openapi/TODO.md +19 -0
- package/src/openapi/index.ts +130 -0
- package/src/openapi/runtime.ts +326 -0
- package/src/openapi/spec.ts +511 -0
- package/src/openapi/types.ts +112 -0
- package/src/stdlib/collections.ts +51 -0
- package/src/stdlib/console.ts +4 -0
- package/src/stdlib/date.ts +94 -0
- package/src/stdlib/json.ts +42 -0
- package/src/stdlib/math.ts +65 -0
- package/src/stdlib/number.ts +66 -0
- package/src/stdlib/object.ts +77 -0
- package/src/stdlib/promise.ts +6 -0
- package/src/stdlib/regexp.ts +74 -0
- package/src/stdlib/string.ts +52 -0
- package/src/stdlib/url.ts +90 -0
- package/src/stdlib/value.ts +86 -0
- package/src/tool-error.ts +11 -0
- package/src/tool-runtime.ts +806 -0
- package/src/tool-schema.ts +301 -0
- package/src/tool.ts +96 -0
- package/src/values.ts +49 -0
- package/sst-env.d.ts +10 -0
- package/test/codemode.test.ts +1163 -0
- package/test/enumeration.test.ts +159 -0
- package/test/fixtures/openapi-happy-path.json +230 -0
- package/test/fixtures/opencode-v2-openapi.json +23730 -0
- package/test/openapi.test.ts +964 -0
- package/test/parity.test.ts +425 -0
- package/test/promise.test.ts +456 -0
- package/test/signature.test.ts +449 -0
- package/test/stdlib.test.ts +715 -0
- package/tsconfig.json +7 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import { JsonPointer, Schema } from "effect"
|
|
2
|
+
import type { Definition, JsonSchema, SchemaType } from "./tool.js"
|
|
3
|
+
|
|
4
|
+
const isEffectSchema = (schema: SchemaType): schema is Schema.Decoder<unknown> & Schema.Top => Schema.isSchema(schema)
|
|
5
|
+
|
|
6
|
+
const renderLiteral = (value: unknown): string => JSON.stringify(value) ?? "unknown"
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Bare TypeScript identifier - usable unquoted as an object key (and, in the tool runtime,
|
|
10
|
+
* with dot access as a tool-path segment). Anything else must be quoted/bracketed.
|
|
11
|
+
*/
|
|
12
|
+
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/
|
|
13
|
+
|
|
14
|
+
/** Renders a property name as a valid TS object key: bare when an identifier, quoted otherwise. */
|
|
15
|
+
const renderKey = (name: string): string => (identifierSegment.test(name) ? name : JSON.stringify(name))
|
|
16
|
+
|
|
17
|
+
const effectNumberSentinel = (schema: JsonSchema) =>
|
|
18
|
+
schema.type === "string" &&
|
|
19
|
+
Array.isArray(schema.enum) &&
|
|
20
|
+
schema.enum.length === 1 &&
|
|
21
|
+
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity")
|
|
22
|
+
|
|
23
|
+
const intersection = (members: ReadonlyArray<string>): string => {
|
|
24
|
+
const concrete = members.filter((member) => member !== "unknown")
|
|
25
|
+
if (concrete.length === 0) return "unknown"
|
|
26
|
+
if (concrete.length === 1) return concrete[0] ?? "unknown"
|
|
27
|
+
return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Recursion ceiling for schema rendering. Object, array, and union recursion all increment
|
|
32
|
+
* depth, so this bounds every recursion path - pathological or structurally cyclic schemas
|
|
33
|
+
* degrade to `unknown` instead of overflowing the stack (rendering must never throw).
|
|
34
|
+
*/
|
|
35
|
+
const MAX_RENDER_DEPTH = 8
|
|
36
|
+
|
|
37
|
+
type RenderContext = {
|
|
38
|
+
readonly definitions: Readonly<Record<string, JsonSchema>>
|
|
39
|
+
/** Indented, JSDoc-annotated multiline rendering (search results); compact single line otherwise. */
|
|
40
|
+
readonly pretty: boolean
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const hasUnresolvedRef = (
|
|
44
|
+
schema: JsonSchema,
|
|
45
|
+
definitions: Readonly<Record<string, JsonSchema>>,
|
|
46
|
+
seen: ReadonlySet<string> = new Set(),
|
|
47
|
+
visited: ReadonlySet<JsonSchema> = new Set(),
|
|
48
|
+
): boolean => {
|
|
49
|
+
if (visited.has(schema)) return false
|
|
50
|
+
const nextVisited = new Set([...visited, schema])
|
|
51
|
+
if (schema.$ref !== undefined) {
|
|
52
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
|
|
53
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
|
|
54
|
+
if (name === undefined || definitions[name] === undefined || seen.has(name)) return true
|
|
55
|
+
if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited)) return true
|
|
56
|
+
}
|
|
57
|
+
return [
|
|
58
|
+
...(schema.anyOf ?? []),
|
|
59
|
+
...(schema.oneOf ?? []),
|
|
60
|
+
...(schema.allOf ?? []),
|
|
61
|
+
...Object.values(schema.properties ?? {}),
|
|
62
|
+
...(schema.items === undefined ? [] : [schema.items]),
|
|
63
|
+
...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []),
|
|
64
|
+
].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Schema constraints a TypeScript type cannot express natively but a model benefits from,
|
|
69
|
+
* surfaced as JSDoc tags (`@deprecated`, `@default`, `@format`, `@minItems`, `@maxItems`).
|
|
70
|
+
*/
|
|
71
|
+
const docTags = (schema: JsonSchema): Array<string> => {
|
|
72
|
+
const tags: Array<string> = []
|
|
73
|
+
if (schema.deprecated === true) tags.push("@deprecated")
|
|
74
|
+
if (schema.default !== undefined) {
|
|
75
|
+
try {
|
|
76
|
+
const rendered = JSON.stringify(schema.default)
|
|
77
|
+
if (rendered !== undefined) tags.push(`@default ${rendered}`)
|
|
78
|
+
} catch {
|
|
79
|
+
// unserializable default: skip rather than emit a broken tag
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (typeof schema.format === "string") tags.push(`@format ${schema.format}`)
|
|
83
|
+
if (typeof schema.minItems === "number") tags.push(`@minItems ${schema.minItems}`)
|
|
84
|
+
if (typeof schema.maxItems === "number") tags.push(`@maxItems ${schema.maxItems}`)
|
|
85
|
+
return tags
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Format a schema `description` plus `tags` as a JSDoc comment at the given indent,
|
|
90
|
+
* preserving multi-line text (a single line stays `/** ... *\/`; multiple lines become a
|
|
91
|
+
* `*`-prefixed block). `*\/` is neutralized so nothing can close the comment early, and
|
|
92
|
+
* blank leading/trailing lines are trimmed. Returns "" (else a trailing newline) so
|
|
93
|
+
* callers can prepend it directly to the field line.
|
|
94
|
+
*/
|
|
95
|
+
const jsdoc = (description: string | undefined, tags: ReadonlyArray<string>, pad: string): string => {
|
|
96
|
+
const lines = [...(description === undefined ? [] : description.split("\n")), ...tags].map((line) =>
|
|
97
|
+
line.replaceAll("*/", "* /").replace(/\s+$/, ""),
|
|
98
|
+
)
|
|
99
|
+
while (lines.length > 0 && lines[0]!.trim() === "") lines.shift()
|
|
100
|
+
while (lines.length > 0 && lines[lines.length - 1]!.trim() === "") lines.pop()
|
|
101
|
+
if (lines.length === 0) return ""
|
|
102
|
+
if (lines.length === 1) return `${pad}/** ${lines[0]} */\n`
|
|
103
|
+
const body = lines.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`).join("\n")
|
|
104
|
+
return `${pad}/**\n${body}\n${pad} */\n`
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const renderSchema = (
|
|
108
|
+
schema: JsonSchema,
|
|
109
|
+
ctx: RenderContext,
|
|
110
|
+
depth = 0,
|
|
111
|
+
seen: ReadonlySet<string> = new Set(),
|
|
112
|
+
): string => {
|
|
113
|
+
if (depth > MAX_RENDER_DEPTH) return "unknown"
|
|
114
|
+
const nested =
|
|
115
|
+
schema.definitions === undefined && schema.$defs === undefined
|
|
116
|
+
? ctx
|
|
117
|
+
: { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } }
|
|
118
|
+
if (schema.$ref) {
|
|
119
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
|
|
120
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
|
|
121
|
+
if (!name || !nested.definitions[name] || seen.has(name)) return "unknown"
|
|
122
|
+
return intersection([
|
|
123
|
+
renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])),
|
|
124
|
+
renderSchema({ ...schema, $ref: undefined }, nested, depth + 1, seen),
|
|
125
|
+
])
|
|
126
|
+
}
|
|
127
|
+
if (schema.const !== undefined) return renderLiteral(schema.const)
|
|
128
|
+
if (schema.enum) return schema.enum.map(renderLiteral).join(" | ")
|
|
129
|
+
const alternatives = schema.anyOf ?? schema.oneOf
|
|
130
|
+
if (alternatives) {
|
|
131
|
+
// Effect's number schema emits `anyOf: [{ type: "number" }, { const: "NaN" },
|
|
132
|
+
// { const: "Infinity" }, { const: "-Infinity" }]`. Collapse only that artifact;
|
|
133
|
+
// real JSON Schema unions such as `string | number` or `number | null` must keep
|
|
134
|
+
// every branch.
|
|
135
|
+
if (
|
|
136
|
+
alternatives.some((item) => item.type === "number") &&
|
|
137
|
+
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item))
|
|
138
|
+
)
|
|
139
|
+
return "number"
|
|
140
|
+
// An empty Schema.Struct({}) emits `anyOf: [{ type: "object" }, { type: "array" }]`
|
|
141
|
+
// (no properties/items); render the bare shape as {} instead of `{} | Array<unknown>`.
|
|
142
|
+
if (
|
|
143
|
+
alternatives.length === 2 &&
|
|
144
|
+
alternatives[0]?.type === "object" &&
|
|
145
|
+
alternatives[0].properties === undefined &&
|
|
146
|
+
alternatives[1]?.type === "array" &&
|
|
147
|
+
alternatives[1].items === undefined
|
|
148
|
+
) {
|
|
149
|
+
return "{}"
|
|
150
|
+
}
|
|
151
|
+
const members = alternatives.map((item) => renderSchema(item, nested, depth + 1, seen))
|
|
152
|
+
if (members.some((member) => member === "unknown")) return "unknown"
|
|
153
|
+
return intersection([
|
|
154
|
+
members.join(" | "),
|
|
155
|
+
renderSchema({ ...schema, anyOf: undefined, oneOf: undefined }, nested, depth + 1, seen),
|
|
156
|
+
])
|
|
157
|
+
}
|
|
158
|
+
if (schema.allOf) {
|
|
159
|
+
const members = schema.allOf.map((item) => renderSchema(item, nested, depth + 1, seen))
|
|
160
|
+
if (schema.allOf.some((item) => hasUnresolvedRef(item, nested.definitions))) return "unknown"
|
|
161
|
+
return intersection([renderSchema({ ...schema, allOf: undefined }, nested, depth + 1, seen), ...members])
|
|
162
|
+
}
|
|
163
|
+
if (Array.isArray(schema.type)) {
|
|
164
|
+
return schema.type.map((item) => renderSchema({ ...schema, type: item }, nested, depth + 1, seen)).join(" | ")
|
|
165
|
+
}
|
|
166
|
+
if (schema.type === "string") return "string"
|
|
167
|
+
if (schema.type === "number" || schema.type === "integer") return "number"
|
|
168
|
+
if (schema.type === "boolean") return "boolean"
|
|
169
|
+
if (schema.type === "null") return "null"
|
|
170
|
+
if (schema.type === "array") return `Array<${renderSchema(schema.items ?? {}, nested, depth + 1, seen)}>`
|
|
171
|
+
if (schema.type === "object" || schema.properties) {
|
|
172
|
+
const required = new Set(schema.required ?? [])
|
|
173
|
+
const properties = Object.entries(schema.properties ?? {})
|
|
174
|
+
const additional = schema.additionalProperties
|
|
175
|
+
const indexType =
|
|
176
|
+
additional && typeof additional === "object" ? renderSchema(additional, nested, depth + 1, seen) : undefined
|
|
177
|
+
const field = ([name, value]: readonly [string, JsonSchema]) =>
|
|
178
|
+
`${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, nested, depth + 1, seen)}`
|
|
179
|
+
|
|
180
|
+
if (!ctx.pretty) {
|
|
181
|
+
const fields = properties.map(field)
|
|
182
|
+
if (indexType !== undefined) fields.push(`[key: string]: ${indexType}`)
|
|
183
|
+
return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Pretty: an indented block, each described field preceded by its JSDoc comment.
|
|
187
|
+
if (properties.length === 0 && indexType === undefined) return "{}"
|
|
188
|
+
const pad = " ".repeat(depth + 1)
|
|
189
|
+
const lines = properties.map(
|
|
190
|
+
(entry) => `${jsdoc(entry[1].description, docTags(entry[1]), pad)}${pad}${field(entry)},`,
|
|
191
|
+
)
|
|
192
|
+
if (indexType !== undefined) lines.push(`${pad}[key: string]: ${indexType},`)
|
|
193
|
+
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`
|
|
194
|
+
}
|
|
195
|
+
return "unknown"
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export const toTypeScript = (schema: Schema.Top, decoded = false, pretty = false): string => {
|
|
199
|
+
try {
|
|
200
|
+
const visible = decoded ? Schema.toType(schema) : schema
|
|
201
|
+
const document = Schema.toJsonSchemaDocument(visible) as {
|
|
202
|
+
readonly schema: JsonSchema
|
|
203
|
+
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
|
204
|
+
}
|
|
205
|
+
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty })
|
|
206
|
+
} catch {
|
|
207
|
+
return "unknown"
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Renders a raw JSON Schema document as a TypeScript type string. */
|
|
212
|
+
export const jsonSchemaToTypeScript = (schema: JsonSchema, pretty = false): string => {
|
|
213
|
+
try {
|
|
214
|
+
return renderSchema(schema, { definitions: { ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) }, pretty })
|
|
215
|
+
} catch {
|
|
216
|
+
return "unknown"
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** One input property of a tool, extracted best-effort from its input schema. */
|
|
221
|
+
export type InputProperty = {
|
|
222
|
+
readonly name: string
|
|
223
|
+
readonly description: string | undefined
|
|
224
|
+
readonly required: boolean
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* The property names, descriptions, and required flags of a tool's input schema - the raw
|
|
229
|
+
* material for search text. Best-effort: Effect Schemas go through their
|
|
230
|
+
* JSON Schema document (the same emission signature rendering uses); JSON Schemas are read
|
|
231
|
+
* directly, resolving a trivial top-level `$ref` into `$defs`/`definitions` when present.
|
|
232
|
+
* Anything unresolvable yields `[]` (search falls back to path + description).
|
|
233
|
+
*/
|
|
234
|
+
export const inputProperties = <R>(definition: Definition<R>): Array<InputProperty> => {
|
|
235
|
+
try {
|
|
236
|
+
const document = isEffectSchema(definition.input)
|
|
237
|
+
? (Schema.toJsonSchemaDocument(definition.input) as {
|
|
238
|
+
readonly schema: JsonSchema
|
|
239
|
+
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
|
240
|
+
})
|
|
241
|
+
: {
|
|
242
|
+
schema: definition.input,
|
|
243
|
+
definitions: { ...(definition.input.definitions ?? {}), ...(definition.input.$defs ?? {}) },
|
|
244
|
+
}
|
|
245
|
+
const definitions = document.definitions ?? {}
|
|
246
|
+
let schema = document.schema
|
|
247
|
+
if (schema.$ref !== undefined) {
|
|
248
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1]
|
|
249
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment)
|
|
250
|
+
const resolved = name === undefined ? undefined : definitions[name]
|
|
251
|
+
if (resolved === undefined) return []
|
|
252
|
+
schema = resolved
|
|
253
|
+
}
|
|
254
|
+
const required = new Set(schema.required ?? [])
|
|
255
|
+
return Object.entries(schema.properties ?? {}).map(([name, value]) => ({
|
|
256
|
+
name,
|
|
257
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
258
|
+
required: required.has(name),
|
|
259
|
+
}))
|
|
260
|
+
} catch {
|
|
261
|
+
return []
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* The model-visible TypeScript type of a tool's input. `pretty` renders an indented
|
|
267
|
+
* multiline block with schema descriptions and constraints as JSDoc comments on the
|
|
268
|
+
* fields; the default stays the compact single-line form.
|
|
269
|
+
*/
|
|
270
|
+
export const inputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
|
|
271
|
+
isEffectSchema(definition.input)
|
|
272
|
+
? toTypeScript(definition.input, false, pretty)
|
|
273
|
+
: jsonSchemaToTypeScript(definition.input, pretty)
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* The model-visible TypeScript type of a tool's result; tools without an output schema
|
|
277
|
+
* return `unknown`. `pretty` renders the JSDoc-annotated multiline form, as for inputs.
|
|
278
|
+
*/
|
|
279
|
+
export const outputTypeScript = <R>(definition: Definition<R>, pretty = false): string =>
|
|
280
|
+
definition.output === undefined
|
|
281
|
+
? "unknown"
|
|
282
|
+
: isEffectSchema(definition.output)
|
|
283
|
+
? toTypeScript(definition.output, true, pretty)
|
|
284
|
+
: jsonSchemaToTypeScript(definition.output, pretty)
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Decodes tool input before `run` is invoked. Effect Schemas validate (throwing on failure);
|
|
288
|
+
* JSON-Schema-described inputs pass through unvalidated (render-only).
|
|
289
|
+
*/
|
|
290
|
+
export const decodeInput = <R>(definition: Definition<R>, value: unknown): unknown =>
|
|
291
|
+
isEffectSchema(definition.input) ? Schema.decodeUnknownSync(definition.input)(value) : value
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Decodes a tool result before it is exposed to the program. Effect Schemas validate and
|
|
295
|
+
* transform (throwing on failure); JSON Schema outputs and tools without an output schema pass
|
|
296
|
+
* the host value through unchanged.
|
|
297
|
+
*/
|
|
298
|
+
export const decodeOutput = <R>(definition: Definition<R>, value: unknown): unknown =>
|
|
299
|
+
definition.output !== undefined && isEffectSchema(definition.output)
|
|
300
|
+
? Schema.decodeUnknownSync(definition.output)(value)
|
|
301
|
+
: value
|
package/src/tool.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect"
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* JSON Schema subset accepted for render-only tool schemas.
|
|
5
|
+
*
|
|
6
|
+
* A JSON-Schema-described side of a tool is used to generate the model-visible TypeScript
|
|
7
|
+
* signature only - CodeMode performs no validation against it. This is the natural shape for
|
|
8
|
+
* adapter-provided tools (e.g. MCP definitions) whose schemas arrive as JSON Schema documents.
|
|
9
|
+
*/
|
|
10
|
+
export type JsonSchema = {
|
|
11
|
+
readonly type?: string | ReadonlyArray<string>
|
|
12
|
+
readonly enum?: ReadonlyArray<unknown>
|
|
13
|
+
readonly const?: unknown
|
|
14
|
+
readonly anyOf?: ReadonlyArray<JsonSchema>
|
|
15
|
+
readonly oneOf?: ReadonlyArray<JsonSchema>
|
|
16
|
+
readonly allOf?: ReadonlyArray<JsonSchema>
|
|
17
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>
|
|
18
|
+
readonly required?: ReadonlyArray<string>
|
|
19
|
+
readonly items?: JsonSchema
|
|
20
|
+
readonly additionalProperties?: boolean | JsonSchema
|
|
21
|
+
readonly description?: string
|
|
22
|
+
readonly default?: unknown
|
|
23
|
+
readonly format?: string
|
|
24
|
+
readonly deprecated?: boolean
|
|
25
|
+
readonly minItems?: number
|
|
26
|
+
readonly maxItems?: number
|
|
27
|
+
readonly $ref?: string
|
|
28
|
+
readonly $defs?: Readonly<Record<string, JsonSchema>>
|
|
29
|
+
readonly definitions?: Readonly<Record<string, JsonSchema>>
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Either a validating Effect Schema or a render-only JSON Schema document. */
|
|
33
|
+
export type SchemaType = Schema.Decoder<unknown> | JsonSchema
|
|
34
|
+
|
|
35
|
+
/** Schema-backed tool definition consumed by a CodeMode tool tree. */
|
|
36
|
+
export type Definition<R = never> = {
|
|
37
|
+
readonly _tag: "CodeModeTool"
|
|
38
|
+
readonly description: string
|
|
39
|
+
readonly input: SchemaType
|
|
40
|
+
readonly output: SchemaType | undefined
|
|
41
|
+
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, R>
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The value `run` receives: the decoded type for Effect Schemas, `unknown` for JSON Schemas. */
|
|
45
|
+
type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown
|
|
46
|
+
|
|
47
|
+
/** The value `run` returns: the encoded type for Effect Schemas, `unknown` otherwise. */
|
|
48
|
+
type ResultType<S> = S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown
|
|
49
|
+
|
|
50
|
+
/** Options for defining one CodeMode tool. */
|
|
51
|
+
export type Options<I extends SchemaType, O extends SchemaType | undefined, R = never> = {
|
|
52
|
+
readonly description: string
|
|
53
|
+
readonly input: I
|
|
54
|
+
readonly output?: O
|
|
55
|
+
readonly run: (input: InputType<I>) => Effect.Effect<ResultType<O>, unknown, R>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export const isDefinition = <R = never>(value: unknown): value is Definition<R> =>
|
|
59
|
+
typeof value === "object" && value !== null && "_tag" in value && value._tag === "CodeModeTool"
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Defines one schema-described tool available to a CodeMode program through `tools.*`.
|
|
63
|
+
*
|
|
64
|
+
* `input` and `output` each accept a validating Effect Schema or a render-only JSON Schema
|
|
65
|
+
* document. Effect Schema input is decoded before `run` is invoked, and `run` returns the
|
|
66
|
+
* encoded representation of an Effect Schema `output`, which CodeMode decodes before returning
|
|
67
|
+
* it to the program. JSON Schemas only shape the model-visible signature; values pass through
|
|
68
|
+
* unvalidated. `output` is optional - without it the signature advertises `unknown` and the
|
|
69
|
+
* host result is exposed as-is. The host tool remains responsible for authorization and
|
|
70
|
+
* durable side-effect handling.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* ```ts
|
|
74
|
+
* const lookup = Tool.make({
|
|
75
|
+
* description: "Look up an order",
|
|
76
|
+
* input: Schema.Struct({ id: Schema.String }),
|
|
77
|
+
* output: Schema.Struct({ status: Schema.String }),
|
|
78
|
+
* run: ({ id }) => Effect.succeed({ status: "open" }),
|
|
79
|
+
* })
|
|
80
|
+
*
|
|
81
|
+
* const fromJsonSchema = Tool.make({
|
|
82
|
+
* description: "Call an adapter-described tool",
|
|
83
|
+
* input: { type: "object", properties: { id: { type: "string" } }, required: ["id"] },
|
|
84
|
+
* run: (input) => callHost(input),
|
|
85
|
+
* })
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
export const make = <I extends SchemaType, const O extends SchemaType | undefined = undefined, R = never>(
|
|
89
|
+
options: Options<I, O, R>,
|
|
90
|
+
): Definition<R> => ({
|
|
91
|
+
_tag: "CodeModeTool",
|
|
92
|
+
description: options.description,
|
|
93
|
+
input: options.input,
|
|
94
|
+
output: options.output,
|
|
95
|
+
run: (input) => options.run(input as InputType<I>),
|
|
96
|
+
})
|
package/src/values.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Effect, Fiber } from "effect"
|
|
2
|
+
|
|
3
|
+
export class SandboxPromise {
|
|
4
|
+
interrupted = false
|
|
5
|
+
constructor(
|
|
6
|
+
readonly fiber: Fiber.Fiber<unknown, unknown> | undefined,
|
|
7
|
+
readonly immediate?: Effect.Effect<unknown, unknown>,
|
|
8
|
+
) {}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export class SandboxDate {
|
|
12
|
+
constructor(readonly time: number) {}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class SandboxRegExp {
|
|
16
|
+
readonly regex: RegExp
|
|
17
|
+
constructor(pattern: string, flags: string) {
|
|
18
|
+
this.regex = new RegExp(pattern, flags)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export class SandboxMap {
|
|
23
|
+
readonly map = new Map<unknown, unknown>()
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export class SandboxSet {
|
|
27
|
+
readonly set = new Set<unknown>()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class SandboxURLSearchParams {
|
|
31
|
+
constructor(readonly params: URLSearchParams) {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class SandboxURL {
|
|
35
|
+
readonly searchParams: SandboxURLSearchParams
|
|
36
|
+
constructor(readonly url: URL) {
|
|
37
|
+
this.searchParams = new SandboxURLSearchParams(url.searchParams)
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const isSandboxValue = (
|
|
42
|
+
value: unknown,
|
|
43
|
+
): value is SandboxDate | SandboxRegExp | SandboxMap | SandboxSet | SandboxURL | SandboxURLSearchParams =>
|
|
44
|
+
value instanceof SandboxDate ||
|
|
45
|
+
value instanceof SandboxRegExp ||
|
|
46
|
+
value instanceof SandboxMap ||
|
|
47
|
+
value instanceof SandboxSet ||
|
|
48
|
+
value instanceof SandboxURL ||
|
|
49
|
+
value instanceof SandboxURLSearchParams
|
package/sst-env.d.ts
ADDED