@opencode/codemode 0.0.0-reserved → 2.0.1
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/README.md +184 -2
- package/dist/codemode.d.ts +145 -0
- package/dist/codemode.js +66 -0
- package/dist/data.d.ts +25 -0
- package/dist/data.js +158 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.js +7 -0
- package/dist/interpreter/errors.d.ts +10 -0
- package/dist/interpreter/errors.js +112 -0
- package/dist/interpreter/execute.d.ts +5 -0
- package/dist/interpreter/execute.js +171 -0
- package/dist/interpreter/globals.d.ts +13 -0
- package/dist/interpreter/globals.js +64 -0
- package/dist/interpreter/host.d.ts +41 -0
- package/dist/interpreter/host.js +44 -0
- package/dist/interpreter/intrinsics.d.ts +10 -0
- package/dist/interpreter/intrinsics.js +41 -0
- package/dist/interpreter/methods.d.ts +4 -0
- package/dist/interpreter/methods.js +837 -0
- package/dist/interpreter/model.d.ts +89 -0
- package/dist/interpreter/model.js +90 -0
- package/dist/interpreter/objects.d.ts +37 -0
- package/dist/interpreter/objects.js +154 -0
- package/dist/interpreter/promises.d.ts +31 -0
- package/dist/interpreter/promises.js +270 -0
- package/dist/interpreter/references.d.ts +7 -0
- package/dist/interpreter/references.js +93 -0
- package/dist/interpreter/runner.d.ts +26 -0
- package/dist/interpreter/runner.js +45 -0
- package/dist/interpreter/runtime.d.ts +19 -0
- package/dist/interpreter/runtime.js +1942 -0
- package/dist/interpreter/scope.d.ts +15 -0
- package/dist/interpreter/scope.js +79 -0
- package/dist/interpreter/transpile.node.d.ts +5 -0
- package/dist/interpreter/transpile.node.js +19 -0
- package/dist/interpreter/transpile.workerd.d.ts +5 -0
- package/dist/interpreter/transpile.workerd.js +6 -0
- package/dist/namespace.d.ts +15 -0
- package/dist/namespace.js +7 -0
- package/dist/openapi/index.d.ts +7 -0
- package/dist/openapi/index.js +101 -0
- package/dist/openapi/runtime.d.ts +4 -0
- package/dist/openapi/runtime.js +283 -0
- package/dist/openapi/spec.d.ts +20 -0
- package/dist/openapi/spec.js +583 -0
- package/dist/openapi/types.d.ts +122 -0
- package/dist/openapi/types.js +2 -0
- package/dist/stdlib/array.d.ts +3 -0
- package/dist/stdlib/array.js +68 -0
- package/dist/stdlib/collections.d.ts +9 -0
- package/dist/stdlib/collections.js +173 -0
- package/dist/stdlib/console.d.ts +3 -0
- package/dist/stdlib/console.js +137 -0
- package/dist/stdlib/date.d.ts +8 -0
- package/dist/stdlib/date.js +208 -0
- package/dist/stdlib/json.d.ts +3 -0
- package/dist/stdlib/json.js +101 -0
- package/dist/stdlib/math.d.ts +8 -0
- package/dist/stdlib/math.js +89 -0
- package/dist/stdlib/number.d.ts +4 -0
- package/dist/stdlib/number.js +69 -0
- package/dist/stdlib/object.d.ts +7 -0
- package/dist/stdlib/object.js +106 -0
- package/dist/stdlib/regexp.d.ts +10 -0
- package/dist/stdlib/regexp.js +120 -0
- package/dist/stdlib/string.d.ts +2 -0
- package/dist/stdlib/string.js +51 -0
- package/dist/stdlib/url.d.ts +16 -0
- package/dist/stdlib/url.js +161 -0
- package/dist/stdlib/value.d.ts +8 -0
- package/dist/stdlib/value.js +98 -0
- package/dist/stdlib/web.d.ts +4 -0
- package/dist/stdlib/web.js +21 -0
- package/dist/tool-error.d.ts +11 -0
- package/dist/tool-error.js +9 -0
- package/dist/tool-runtime.d.ts +69 -0
- package/dist/tool-runtime.js +254 -0
- package/dist/tool-schema.d.ts +16 -0
- package/dist/tool-schema.js +263 -0
- package/dist/tool.d.ts +66 -0
- package/dist/tool.js +16 -0
- package/dist/tools.d.ts +5 -0
- package/dist/tools.js +1 -0
- package/dist/values.d.ts +37 -0
- package/dist/values.js +56 -0
- package/package.json +37 -6
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Formatter, Schema } from "effect";
|
|
2
|
+
import { fromData, toData, ToolRuntimeError } from "./data.js";
|
|
3
|
+
import { toolError } from "./tool-error.js";
|
|
4
|
+
import { decodeInput as decodeToolInput, decodeOutput as decodeToolOutput, identifierSegment, inputProperties, inputTypeScript, isEmptyInput, outputTypeScript, } from "./tool-schema.js";
|
|
5
|
+
import { isNamespace } from "./namespace.js";
|
|
6
|
+
import { isTool } from "./tool.js";
|
|
7
|
+
export const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
8
|
+
const defaultSearchLimit = 10;
|
|
9
|
+
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
10
|
+
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
11
|
+
const SearchInput = Schema.Struct({
|
|
12
|
+
query: Schema.optionalKey(Schema.String),
|
|
13
|
+
namespace: Schema.optionalKey(Schema.String),
|
|
14
|
+
limit: Schema.optionalKey(PositiveInt),
|
|
15
|
+
offset: Schema.optionalKey(NonNegativeInt),
|
|
16
|
+
});
|
|
17
|
+
const SearchItem = Schema.Struct({
|
|
18
|
+
path: Schema.String,
|
|
19
|
+
description: Schema.String,
|
|
20
|
+
signature: Schema.String,
|
|
21
|
+
});
|
|
22
|
+
const SearchOutput = Schema.Struct({
|
|
23
|
+
items: Schema.Array(SearchItem),
|
|
24
|
+
remaining: NonNegativeInt,
|
|
25
|
+
next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })),
|
|
26
|
+
});
|
|
27
|
+
export const toolExpression = (path) => "tools" +
|
|
28
|
+
path
|
|
29
|
+
.split(".")
|
|
30
|
+
.map((segment) => (identifierSegment.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`))
|
|
31
|
+
.join("");
|
|
32
|
+
export class ToolReference {
|
|
33
|
+
path;
|
|
34
|
+
constructor(path) {
|
|
35
|
+
this.path = path;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const toolTrie = (tools) => {
|
|
39
|
+
const root = { children: new Map() };
|
|
40
|
+
const insert = (node, group) => {
|
|
41
|
+
for (const [name, value] of Object.entries(group)) {
|
|
42
|
+
let current = node;
|
|
43
|
+
for (const segment of name.split(".")) {
|
|
44
|
+
if (segment === "")
|
|
45
|
+
throw new TypeError(`Tool name '${name}' contains an empty segment.`);
|
|
46
|
+
const child = current.children.get(segment) ?? { children: new Map() };
|
|
47
|
+
current.children.set(segment, child);
|
|
48
|
+
current = child;
|
|
49
|
+
}
|
|
50
|
+
if (isTool(value))
|
|
51
|
+
current.tool = value;
|
|
52
|
+
else if (isNamespace(value)) {
|
|
53
|
+
current.namespace = value;
|
|
54
|
+
insert(current, value.tools);
|
|
55
|
+
}
|
|
56
|
+
else
|
|
57
|
+
insert(current, value);
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
insert(root, tools);
|
|
61
|
+
return root;
|
|
62
|
+
};
|
|
63
|
+
const canonicalSegments = (path) => path.flatMap((segment) => segment.split("."));
|
|
64
|
+
const flattenTools = (node, path = [], namespaces = []) => {
|
|
65
|
+
const next = node.namespace === undefined ? namespaces : [...namespaces, node.namespace];
|
|
66
|
+
return [
|
|
67
|
+
...(node.tool === undefined ? [] : [{ path: path.join("."), tool: node.tool, namespaces: next }]),
|
|
68
|
+
...Array.from(node.children).flatMap(([name, child]) => flattenTools(child, [...path, name], next)),
|
|
69
|
+
];
|
|
70
|
+
};
|
|
71
|
+
const describeTool = (visible) => ({
|
|
72
|
+
path: visible.path,
|
|
73
|
+
description: visible.tool.description,
|
|
74
|
+
signature: isEmptyInput(visible.tool)
|
|
75
|
+
? `${toolExpression(visible.path)}(): Promise<${outputTypeScript(visible.tool, true)}>`
|
|
76
|
+
: `${toolExpression(visible.path)}(input: ${inputTypeScript(visible.tool, true)}): Promise<${outputTypeScript(visible.tool, true)}>`,
|
|
77
|
+
});
|
|
78
|
+
const tokenize = (query) => query
|
|
79
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
80
|
+
.toLowerCase()
|
|
81
|
+
.split(/[^a-z0-9]+/)
|
|
82
|
+
.filter((term) => term.length > 0 && term !== "*");
|
|
83
|
+
const termForms = (term) => {
|
|
84
|
+
const forms = [term];
|
|
85
|
+
if (term.endsWith("es") && term.length > 3)
|
|
86
|
+
forms.push(term.slice(0, -2));
|
|
87
|
+
if (term.endsWith("s") && term.length > 2)
|
|
88
|
+
forms.push(term.slice(0, -1));
|
|
89
|
+
return forms;
|
|
90
|
+
};
|
|
91
|
+
const makeSearchTool = (searchIndex) => ({
|
|
92
|
+
_tag: "CodeModeTool",
|
|
93
|
+
description: "Search available tools",
|
|
94
|
+
input: SearchInput,
|
|
95
|
+
output: SearchOutput,
|
|
96
|
+
execute: (input) => Effect.sync(() => {
|
|
97
|
+
const request = input;
|
|
98
|
+
const query = request.query ?? "";
|
|
99
|
+
const offset = request.offset ?? 0;
|
|
100
|
+
const scoped = request.namespace === undefined
|
|
101
|
+
? searchIndex
|
|
102
|
+
: searchIndex.filter((entry) => entry.description.path === request.namespace ||
|
|
103
|
+
entry.description.path.startsWith(`${request.namespace}.`));
|
|
104
|
+
const trimmed = query.trim();
|
|
105
|
+
const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed;
|
|
106
|
+
const exact = pathQuery === ""
|
|
107
|
+
? undefined
|
|
108
|
+
: scoped.find((entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed);
|
|
109
|
+
const terms = tokenize(query).map(termForms);
|
|
110
|
+
const ranked = exact !== undefined
|
|
111
|
+
? [exact]
|
|
112
|
+
: scoped
|
|
113
|
+
.map((entry) => {
|
|
114
|
+
const path = entry.description.path.toLowerCase();
|
|
115
|
+
const description = entry.description.description.toLowerCase();
|
|
116
|
+
const score = terms.reduce((total, forms) => total +
|
|
117
|
+
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
|
|
118
|
+
(forms.some((form) => path.includes(form)) ? 8 : 0) +
|
|
119
|
+
(forms.some((form) => description.includes(form)) ? 4 : 0) +
|
|
120
|
+
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0), 0);
|
|
121
|
+
return { entry, score };
|
|
122
|
+
})
|
|
123
|
+
.filter(({ score }) => terms.length === 0 || score > 0)
|
|
124
|
+
.sort((left, right) => right.score - left.score || compareText(left.entry.description.path, right.entry.description.path))
|
|
125
|
+
.map(({ entry }) => entry);
|
|
126
|
+
const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({
|
|
127
|
+
...description,
|
|
128
|
+
path: toolExpression(description.path),
|
|
129
|
+
}));
|
|
130
|
+
const remaining = Math.max(0, ranked.length - offset - items.length);
|
|
131
|
+
return {
|
|
132
|
+
items,
|
|
133
|
+
remaining,
|
|
134
|
+
next: remaining > 0 ? { offset: offset + items.length } : null,
|
|
135
|
+
};
|
|
136
|
+
}),
|
|
137
|
+
});
|
|
138
|
+
/** Exact callable signature of the built-in `search` function, for host-owned instructions. */
|
|
139
|
+
export const searchSignature = (() => {
|
|
140
|
+
const tool = makeSearchTool([]);
|
|
141
|
+
return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`;
|
|
142
|
+
})();
|
|
143
|
+
const toSearchEntry = (visible) => ({
|
|
144
|
+
description: describeTool(visible),
|
|
145
|
+
searchText: [
|
|
146
|
+
visible.path,
|
|
147
|
+
visible.tool.description,
|
|
148
|
+
...visible.namespaces.flatMap((namespace) => (namespace.description === undefined ? [] : [namespace.description])),
|
|
149
|
+
...inputProperties(visible.tool).flatMap(({ name, description: property }) => property === undefined ? [name] : [name, property]),
|
|
150
|
+
]
|
|
151
|
+
.join("\n")
|
|
152
|
+
.toLowerCase(),
|
|
153
|
+
});
|
|
154
|
+
export const prepare = (tools) => {
|
|
155
|
+
const root = toolTrie(tools);
|
|
156
|
+
// Discovery bytes are durable instructions, so order only after canonical-path collisions settle.
|
|
157
|
+
const visible = flattenTools(root).sort((left, right) => compareText(left.path, right.path));
|
|
158
|
+
return {
|
|
159
|
+
root,
|
|
160
|
+
catalog: visible.map(describeTool),
|
|
161
|
+
searchIndex: visible.map(toSearchEntry),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
164
|
+
const lookup = (root, segments) => segments.reduce((node, segment) => node?.children.get(segment), root);
|
|
165
|
+
const namespaceKeys = (root, path) => {
|
|
166
|
+
const segments = canonicalSegments(path);
|
|
167
|
+
const node = lookup(root, segments);
|
|
168
|
+
if (node === undefined) {
|
|
169
|
+
throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${segments.join(".")}'.`);
|
|
170
|
+
}
|
|
171
|
+
return Array.from(node.children.keys());
|
|
172
|
+
};
|
|
173
|
+
const resolve = (root, path) => {
|
|
174
|
+
const segments = canonicalSegments(path);
|
|
175
|
+
const node = lookup(root, segments);
|
|
176
|
+
if (node === undefined) {
|
|
177
|
+
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
|
|
178
|
+
"The tool may have been removed or renamed. Use search to find available tools.",
|
|
179
|
+
]);
|
|
180
|
+
}
|
|
181
|
+
if (node.tool === undefined) {
|
|
182
|
+
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`);
|
|
183
|
+
}
|
|
184
|
+
return node.tool;
|
|
185
|
+
};
|
|
186
|
+
/** Per-execution call state over tools prepared once for the runtime. */
|
|
187
|
+
export const make = (prepared, maxToolCalls, hooks) => {
|
|
188
|
+
const calls = [];
|
|
189
|
+
const root = prepared.root;
|
|
190
|
+
const searchTool = makeSearchTool(prepared.searchIndex);
|
|
191
|
+
const observeEnd = (effect, call) => {
|
|
192
|
+
const onEnd = hooks?.onToolCallEnd;
|
|
193
|
+
if (onEnd === undefined)
|
|
194
|
+
return effect;
|
|
195
|
+
const startedAt = Date.now();
|
|
196
|
+
return effect.pipe(Effect.onExit((exit) => {
|
|
197
|
+
const durationMs = Date.now() - startedAt;
|
|
198
|
+
if (Exit.isSuccess(exit))
|
|
199
|
+
return onEnd({ ...call, durationMs, outcome: "success" });
|
|
200
|
+
if (Cause.hasInterruptsOnly(exit.cause))
|
|
201
|
+
return onEnd({ ...call, durationMs, outcome: "interrupted" });
|
|
202
|
+
const error = Cause.squash(exit.cause);
|
|
203
|
+
const message = error instanceof Error ? error.message : Cause.pretty(exit.cause);
|
|
204
|
+
return onEnd({ ...call, durationMs, outcome: "failure", message });
|
|
205
|
+
}));
|
|
206
|
+
};
|
|
207
|
+
const recordCall = (call) => {
|
|
208
|
+
if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
|
|
209
|
+
throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`);
|
|
210
|
+
}
|
|
211
|
+
calls.push(call);
|
|
212
|
+
};
|
|
213
|
+
const executeTool = (name, tool, externalArgs) => Effect.gen(function* () {
|
|
214
|
+
const normalized = externalArgs.length === 0 ? [{}] : externalArgs;
|
|
215
|
+
if (normalized.length !== 1)
|
|
216
|
+
throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects at most one input object.`);
|
|
217
|
+
const input = yield* Effect.try({
|
|
218
|
+
try: () => decodeToolInput(tool, normalized[0]),
|
|
219
|
+
catch: (cause) => new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(cause)}`, name === "search" ? [] : ["The signature may have changed. Use search to get the current signature."]),
|
|
220
|
+
});
|
|
221
|
+
const index = yield* Effect.sync(() => {
|
|
222
|
+
recordCall({ name });
|
|
223
|
+
return calls.length - 1;
|
|
224
|
+
});
|
|
225
|
+
const call = { index, name, input };
|
|
226
|
+
return yield* observeEnd(Effect.gen(function* () {
|
|
227
|
+
if (hooks?.onToolCallStart !== undefined)
|
|
228
|
+
yield* hooks.onToolCallStart(call);
|
|
229
|
+
const raw = yield* Effect.suspend(() => tool.execute(input)).pipe(Effect.catchCause((cause) => {
|
|
230
|
+
if (Cause.hasInterruptsOnly(cause))
|
|
231
|
+
return Effect.interrupt;
|
|
232
|
+
return Effect.fail(toolError(Cause.prettyErrors(cause)
|
|
233
|
+
.map((error) => (error.cause ? Formatter.format(error) : error.message || error.name))
|
|
234
|
+
.join("\n")));
|
|
235
|
+
}));
|
|
236
|
+
return yield* Effect.try({
|
|
237
|
+
try: () => fromData(decodeToolOutput(tool, raw), `Result from tool '${name}'`),
|
|
238
|
+
catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`),
|
|
239
|
+
});
|
|
240
|
+
}), call);
|
|
241
|
+
});
|
|
242
|
+
return {
|
|
243
|
+
calls,
|
|
244
|
+
keys: (path) => namespaceKeys(root, path),
|
|
245
|
+
search: (args) => Effect.suspend(() => executeTool("search", searchTool, args.map((arg) => toData(arg, "Arguments for tool 'search'")))),
|
|
246
|
+
execute: (path, args) => Effect.gen(function* () {
|
|
247
|
+
const name = canonicalSegments(path).join(".");
|
|
248
|
+
const externalArgs = args.map((arg) => toData(arg, `Arguments for tool '${name}'`));
|
|
249
|
+
const tool = resolve(root, path);
|
|
250
|
+
return yield* executeTool(name, tool, externalArgs);
|
|
251
|
+
}),
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
export * as ToolRuntime from "./tool-runtime.js";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
import type { Tool, JsonSchema } from "./tool.js";
|
|
3
|
+
export declare const identifierSegment: RegExp;
|
|
4
|
+
export declare const toTypeScript: (schema: Schema.Top, decoded?: boolean, pretty?: boolean) => string;
|
|
5
|
+
export declare const jsonSchemaToTypeScript: (schema: JsonSchema, pretty?: boolean) => string;
|
|
6
|
+
export type InputProperty = {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly description: string | undefined;
|
|
9
|
+
readonly required: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare const inputProperties: <R>(tool: Tool<R>) => Array<InputProperty>;
|
|
12
|
+
export declare const inputTypeScript: <R>(tool: Tool<R>, pretty?: boolean) => string;
|
|
13
|
+
export declare const isEmptyInput: <R>(tool: Tool<R>) => boolean;
|
|
14
|
+
export declare const outputTypeScript: <R>(tool: Tool<R>, pretty?: boolean) => string;
|
|
15
|
+
export declare const decodeInput: <R>(tool: Tool<R>, value: unknown) => unknown;
|
|
16
|
+
export declare const decodeOutput: <R>(tool: Tool<R>, value: unknown) => unknown;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { JsonPointer, Schema } from "effect";
|
|
2
|
+
const isEffectSchema = (schema) => Schema.isSchema(schema);
|
|
3
|
+
const renderLiteral = (value) => JSON.stringify(value) ?? "unknown";
|
|
4
|
+
export const identifierSegment = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
5
|
+
const renderKey = (name) => (identifierSegment.test(name) ? name : JSON.stringify(name));
|
|
6
|
+
const effectNumberSentinel = (schema) => schema.type === "string" &&
|
|
7
|
+
Array.isArray(schema.enum) &&
|
|
8
|
+
schema.enum.length === 1 &&
|
|
9
|
+
(schema.enum[0] === "NaN" || schema.enum[0] === "Infinity" || schema.enum[0] === "-Infinity");
|
|
10
|
+
const intersection = (members) => {
|
|
11
|
+
const concrete = members.filter((member) => member !== "unknown");
|
|
12
|
+
if (concrete.length === 0)
|
|
13
|
+
return "unknown";
|
|
14
|
+
if (concrete.length === 1)
|
|
15
|
+
return concrete[0];
|
|
16
|
+
return concrete.map((member) => (member.includes(" | ") ? `(${member})` : member)).join(" & ");
|
|
17
|
+
};
|
|
18
|
+
const MAX_RENDER_DEPTH = 8;
|
|
19
|
+
const hasUnresolvedRef = (schema, definitions, seen = new Set(), visited = new Set()) => {
|
|
20
|
+
if (visited.has(schema))
|
|
21
|
+
return false;
|
|
22
|
+
const nextVisited = new Set([...visited, schema]);
|
|
23
|
+
if (schema.$ref !== undefined) {
|
|
24
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
|
|
25
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
|
|
26
|
+
if (name === undefined || definitions[name] === undefined || seen.has(name))
|
|
27
|
+
return true;
|
|
28
|
+
if (hasUnresolvedRef(definitions[name], definitions, new Set([...seen, name]), nextVisited))
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
...(schema.anyOf ?? []),
|
|
33
|
+
...(schema.oneOf ?? []),
|
|
34
|
+
...(schema.allOf ?? []),
|
|
35
|
+
...Object.values(schema.properties ?? {}),
|
|
36
|
+
...(schema.items === undefined ? [] : [schema.items]),
|
|
37
|
+
...(typeof schema.additionalProperties === "object" ? [schema.additionalProperties] : []),
|
|
38
|
+
].some((item) => hasUnresolvedRef(item, definitions, seen, nextVisited));
|
|
39
|
+
};
|
|
40
|
+
const docTags = (schema) => {
|
|
41
|
+
const tags = [];
|
|
42
|
+
if (schema.deprecated === true)
|
|
43
|
+
tags.push("@deprecated");
|
|
44
|
+
if (schema.default !== undefined) {
|
|
45
|
+
try {
|
|
46
|
+
const rendered = JSON.stringify(schema.default);
|
|
47
|
+
if (rendered !== undefined)
|
|
48
|
+
tags.push(`@default ${rendered}`);
|
|
49
|
+
}
|
|
50
|
+
catch { }
|
|
51
|
+
}
|
|
52
|
+
if (typeof schema.format === "string")
|
|
53
|
+
tags.push(`@format ${schema.format}`);
|
|
54
|
+
if (schema.type === "integer")
|
|
55
|
+
tags.push("@integer");
|
|
56
|
+
if (typeof schema.minimum === "number")
|
|
57
|
+
tags.push(`@minimum ${schema.minimum}`);
|
|
58
|
+
if (typeof schema.maximum === "number")
|
|
59
|
+
tags.push(`@maximum ${schema.maximum}`);
|
|
60
|
+
if (typeof schema.exclusiveMinimum === "number")
|
|
61
|
+
tags.push(`@exclusiveMinimum ${schema.exclusiveMinimum}`);
|
|
62
|
+
if (typeof schema.exclusiveMaximum === "number")
|
|
63
|
+
tags.push(`@exclusiveMaximum ${schema.exclusiveMaximum}`);
|
|
64
|
+
if (typeof schema.multipleOf === "number")
|
|
65
|
+
tags.push(`@multipleOf ${schema.multipleOf}`);
|
|
66
|
+
if (typeof schema.minLength === "number")
|
|
67
|
+
tags.push(`@minLength ${schema.minLength}`);
|
|
68
|
+
if (typeof schema.maxLength === "number")
|
|
69
|
+
tags.push(`@maxLength ${schema.maxLength}`);
|
|
70
|
+
if (typeof schema.pattern === "string")
|
|
71
|
+
tags.push(`@pattern ${schema.pattern}`);
|
|
72
|
+
if (typeof schema.minItems === "number")
|
|
73
|
+
tags.push(`@minItems ${schema.minItems}`);
|
|
74
|
+
if (typeof schema.maxItems === "number")
|
|
75
|
+
tags.push(`@maxItems ${schema.maxItems}`);
|
|
76
|
+
if (schema.uniqueItems === true)
|
|
77
|
+
tags.push("@uniqueItems true");
|
|
78
|
+
return tags;
|
|
79
|
+
};
|
|
80
|
+
const docLines = (schema, width) => {
|
|
81
|
+
const summary = docTags(schema).join(" ");
|
|
82
|
+
const lines = (schema.description ?? "").split("\n").map((line) => line.replace(/\s+$/, ""));
|
|
83
|
+
while (lines.length > 0 && lines[0].trim() === "")
|
|
84
|
+
lines.shift();
|
|
85
|
+
while (lines.length > 0 && lines[lines.length - 1].trim() === "")
|
|
86
|
+
lines.pop();
|
|
87
|
+
const inline = lines.length === 1 ? `${lines[0]}${lines[0].endsWith(".") ? "" : "."} ${summary}` : summary;
|
|
88
|
+
return summary && lines.length === 1 && !summary.includes("\n") && width + inline.length + 7 <= 120
|
|
89
|
+
? [inline]
|
|
90
|
+
: [...lines, ...(summary ? summary.split("\n") : [])];
|
|
91
|
+
};
|
|
92
|
+
// Neutralize `*\/` so model-provided schema text cannot terminate generated documentation.
|
|
93
|
+
const jsdoc = (schema, pad) => {
|
|
94
|
+
const content = docLines(schema, pad.length);
|
|
95
|
+
const types = typeof schema.type === "string" ? [schema.type] : (schema.type ?? []);
|
|
96
|
+
const append = (label, child) => {
|
|
97
|
+
docLines(child, pad.length + label.length + 2).forEach((line, index) => {
|
|
98
|
+
if (index === 0) {
|
|
99
|
+
content.push(`${label}: ${line}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
content.push(line ? ` ${line}` : "");
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
// Document only the immediate contents; recursive labels obscure which level a constraint belongs to.
|
|
106
|
+
if (types.includes("array") && schema.items)
|
|
107
|
+
append("Each item", schema.items);
|
|
108
|
+
if ((types.includes("object") || schema.properties) && typeof schema.additionalProperties === "object") {
|
|
109
|
+
const label = Object.keys(schema.properties ?? {}).length > 0 ? "Each additional value" : "Each value";
|
|
110
|
+
append(label, schema.additionalProperties);
|
|
111
|
+
}
|
|
112
|
+
if (content.length === 0)
|
|
113
|
+
return "";
|
|
114
|
+
const escaped = content.map((line) => line.replaceAll("*/", "* /"));
|
|
115
|
+
if (escaped.length === 1 && pad.length + escaped[0].length + 7 <= 120)
|
|
116
|
+
return `${pad}/** ${escaped[0]} */\n`;
|
|
117
|
+
const body = escaped.map((line) => `${pad} *${line === "" ? "" : ` ${line}`}`).join("\n");
|
|
118
|
+
return `${pad}/**\n${body}\n${pad} */\n`;
|
|
119
|
+
};
|
|
120
|
+
const renderSchema = (schema, ctx, depth = 0, seen = new Set()) => {
|
|
121
|
+
if (depth > MAX_RENDER_DEPTH)
|
|
122
|
+
return "unknown";
|
|
123
|
+
const nested = schema.definitions === undefined && schema.$defs === undefined
|
|
124
|
+
? ctx
|
|
125
|
+
: { ...ctx, definitions: { ...ctx.definitions, ...(schema.definitions ?? {}), ...(schema.$defs ?? {}) } };
|
|
126
|
+
if (schema.$ref) {
|
|
127
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
|
|
128
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
|
|
129
|
+
if (!name || !nested.definitions[name] || seen.has(name))
|
|
130
|
+
return "unknown";
|
|
131
|
+
return intersection([
|
|
132
|
+
renderSchema(nested.definitions[name], nested, depth, new Set([...seen, name])),
|
|
133
|
+
renderSchema({ ...schema, $ref: undefined }, nested, depth + 1, seen),
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
if (schema.const !== undefined)
|
|
137
|
+
return renderLiteral(schema.const);
|
|
138
|
+
if (schema.enum)
|
|
139
|
+
return schema.enum.map(renderLiteral).join(" | ");
|
|
140
|
+
const alternatives = schema.anyOf ?? schema.oneOf;
|
|
141
|
+
if (alternatives) {
|
|
142
|
+
if (alternatives.some((item) => item.type === "number") &&
|
|
143
|
+
alternatives.every((item) => item.type === "number" || effectNumberSentinel(item)))
|
|
144
|
+
return "number";
|
|
145
|
+
if (alternatives.length === 2 &&
|
|
146
|
+
alternatives[0]?.type === "object" &&
|
|
147
|
+
alternatives[0].properties === undefined &&
|
|
148
|
+
alternatives[1]?.type === "array" &&
|
|
149
|
+
alternatives[1].items === undefined) {
|
|
150
|
+
return "{}";
|
|
151
|
+
}
|
|
152
|
+
const members = alternatives.map((item) => renderSchema(item, nested, depth + 1, seen));
|
|
153
|
+
if (members.some((member) => member === "unknown"))
|
|
154
|
+
return "unknown";
|
|
155
|
+
return intersection([
|
|
156
|
+
members.join(" | "),
|
|
157
|
+
renderSchema({ ...schema, anyOf: undefined, oneOf: undefined }, nested, depth + 1, seen),
|
|
158
|
+
]);
|
|
159
|
+
}
|
|
160
|
+
if (schema.allOf) {
|
|
161
|
+
if (schema.allOf.some((item) => hasUnresolvedRef(item, nested.definitions)))
|
|
162
|
+
return "unknown";
|
|
163
|
+
const members = schema.allOf.map((item) => renderSchema(item, nested, depth + 1, seen));
|
|
164
|
+
return intersection([renderSchema({ ...schema, allOf: undefined }, nested, depth + 1, seen), ...members]);
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(schema.type)) {
|
|
167
|
+
return schema.type.map((item) => renderSchema({ ...schema, type: item }, nested, depth + 1, seen)).join(" | ");
|
|
168
|
+
}
|
|
169
|
+
if (schema.type === "string")
|
|
170
|
+
return "string";
|
|
171
|
+
if (schema.type === "number" || schema.type === "integer")
|
|
172
|
+
return "number";
|
|
173
|
+
if (schema.type === "boolean")
|
|
174
|
+
return "boolean";
|
|
175
|
+
if (schema.type === "null")
|
|
176
|
+
return "null";
|
|
177
|
+
if (schema.type === "array")
|
|
178
|
+
return `Array<${renderSchema(schema.items ?? {}, nested, depth + 1, seen)}>`;
|
|
179
|
+
if (schema.type === "object" || schema.properties) {
|
|
180
|
+
const required = new Set(schema.required ?? []);
|
|
181
|
+
const properties = Object.entries(schema.properties ?? {});
|
|
182
|
+
const additional = schema.additionalProperties;
|
|
183
|
+
const indexType = additional && typeof additional === "object" ? renderSchema(additional, nested, depth + 1, seen) : undefined;
|
|
184
|
+
const field = ([name, value]) => `${renderKey(name)}${required.has(name) ? "" : "?"}: ${renderSchema(value, nested, depth + 1, seen)}`;
|
|
185
|
+
if (!ctx.pretty) {
|
|
186
|
+
const fields = properties.map(field);
|
|
187
|
+
if (indexType !== undefined)
|
|
188
|
+
fields.push(`[key: string]: ${indexType}`);
|
|
189
|
+
return fields.length === 0 ? "{}" : `{ ${fields.join("; ")} }`;
|
|
190
|
+
}
|
|
191
|
+
if (properties.length === 0 && indexType === undefined)
|
|
192
|
+
return "{}";
|
|
193
|
+
const pad = " ".repeat(depth + 1);
|
|
194
|
+
const lines = properties.map((entry) => `${jsdoc(entry[1], pad)}${pad}${field(entry)},`);
|
|
195
|
+
if (indexType !== undefined)
|
|
196
|
+
lines.push(`${pad}[key: string]: ${indexType},`);
|
|
197
|
+
return `{\n${lines.join("\n")}\n${" ".repeat(depth)}}`;
|
|
198
|
+
}
|
|
199
|
+
return "unknown";
|
|
200
|
+
};
|
|
201
|
+
export const toTypeScript = (schema, decoded = false, pretty = false) => {
|
|
202
|
+
try {
|
|
203
|
+
const visible = decoded ? Schema.toType(schema) : schema;
|
|
204
|
+
const document = Schema.toJsonSchemaDocument(visible);
|
|
205
|
+
return renderSchema(document.schema, { definitions: document.definitions ?? {}, pretty });
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return "unknown";
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
export const jsonSchemaToTypeScript = (schema, pretty = false) => {
|
|
212
|
+
try {
|
|
213
|
+
return renderSchema(schema, { definitions: {}, pretty });
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return "unknown";
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
export const inputProperties = (tool) => {
|
|
220
|
+
try {
|
|
221
|
+
const document = isEffectSchema(tool.input)
|
|
222
|
+
? Schema.toJsonSchemaDocument(tool.input)
|
|
223
|
+
: {
|
|
224
|
+
schema: tool.input,
|
|
225
|
+
definitions: { ...(tool.input.definitions ?? {}), ...(tool.input.$defs ?? {}) },
|
|
226
|
+
};
|
|
227
|
+
const definitions = document.definitions ?? {};
|
|
228
|
+
let schema = document.schema;
|
|
229
|
+
if (schema.$ref !== undefined) {
|
|
230
|
+
const segment = schema.$ref.match(/^#\/(?:\$defs|definitions)\/([^/]+)$/)?.[1];
|
|
231
|
+
const name = segment === undefined ? undefined : JsonPointer.unescapeToken(segment);
|
|
232
|
+
const resolved = name === undefined ? undefined : definitions[name];
|
|
233
|
+
if (resolved === undefined)
|
|
234
|
+
return [];
|
|
235
|
+
schema = resolved;
|
|
236
|
+
}
|
|
237
|
+
const required = new Set(schema.required ?? []);
|
|
238
|
+
return Object.entries(schema.properties ?? {}).map(([name, value]) => ({
|
|
239
|
+
name,
|
|
240
|
+
description: typeof value.description === "string" ? value.description : undefined,
|
|
241
|
+
required: required.has(name),
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return [];
|
|
246
|
+
}
|
|
247
|
+
};
|
|
248
|
+
export const inputTypeScript = (tool, pretty = false) => isEffectSchema(tool.input) ? toTypeScript(tool.input, false, pretty) : jsonSchemaToTypeScript(tool.input, pretty);
|
|
249
|
+
// Empty object schemas render as `{}` in compact form; anything with properties,
|
|
250
|
+
// an index signature, or union members renders differently, so equality is a
|
|
251
|
+
// conservative emptiness test for both Effect and JSON Schema inputs.
|
|
252
|
+
export const isEmptyInput = (tool) => inputTypeScript(tool) === "{}";
|
|
253
|
+
export const outputTypeScript = (tool, pretty = false) => tool.output === undefined
|
|
254
|
+
? "void"
|
|
255
|
+
: isEffectSchema(tool.output)
|
|
256
|
+
? toTypeScript(tool.output, true, pretty)
|
|
257
|
+
: jsonSchemaToTypeScript(tool.output, pretty);
|
|
258
|
+
export const decodeInput = (tool, value) => isEffectSchema(tool.input) ? Schema.decodeUnknownSync(tool.input)(value) : value;
|
|
259
|
+
export const decodeOutput = (tool, value) => tool.output === undefined
|
|
260
|
+
? undefined
|
|
261
|
+
: isEffectSchema(tool.output)
|
|
262
|
+
? Schema.decodeUnknownSync(tool.output)(value)
|
|
263
|
+
: value;
|
package/dist/tool.d.ts
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
import type { Namespace } from "./namespace.js";
|
|
3
|
+
import type { Tools } from "./tools.js";
|
|
4
|
+
/**
|
|
5
|
+
* JSON Schema subset for model-visible signatures. CodeMode does not validate values against
|
|
6
|
+
* these schemas.
|
|
7
|
+
*/
|
|
8
|
+
export type JsonSchema = {
|
|
9
|
+
readonly type?: string | ReadonlyArray<string>;
|
|
10
|
+
readonly enum?: ReadonlyArray<unknown>;
|
|
11
|
+
readonly const?: unknown;
|
|
12
|
+
readonly anyOf?: ReadonlyArray<JsonSchema>;
|
|
13
|
+
readonly oneOf?: ReadonlyArray<JsonSchema>;
|
|
14
|
+
readonly allOf?: ReadonlyArray<JsonSchema>;
|
|
15
|
+
readonly properties?: Readonly<Record<string, JsonSchema>>;
|
|
16
|
+
readonly required?: ReadonlyArray<string>;
|
|
17
|
+
readonly items?: JsonSchema;
|
|
18
|
+
readonly additionalProperties?: boolean | JsonSchema;
|
|
19
|
+
readonly description?: string;
|
|
20
|
+
readonly default?: unknown;
|
|
21
|
+
readonly format?: string;
|
|
22
|
+
readonly deprecated?: boolean;
|
|
23
|
+
readonly minimum?: number;
|
|
24
|
+
readonly maximum?: number;
|
|
25
|
+
readonly exclusiveMinimum?: number;
|
|
26
|
+
readonly exclusiveMaximum?: number;
|
|
27
|
+
readonly multipleOf?: number;
|
|
28
|
+
readonly minLength?: number;
|
|
29
|
+
readonly maxLength?: number;
|
|
30
|
+
readonly pattern?: string;
|
|
31
|
+
readonly minItems?: number;
|
|
32
|
+
readonly maxItems?: number;
|
|
33
|
+
readonly uniqueItems?: boolean;
|
|
34
|
+
readonly $ref?: string;
|
|
35
|
+
readonly $defs?: Readonly<Record<string, JsonSchema>>;
|
|
36
|
+
readonly definitions?: Readonly<Record<string, JsonSchema>>;
|
|
37
|
+
};
|
|
38
|
+
/** Either a validating Effect Schema or a render-only JSON Schema document. */
|
|
39
|
+
export type SchemaType = Schema.Decoder<unknown> | JsonSchema;
|
|
40
|
+
/** Executable tool exposed through CodeMode's `tools` object. */
|
|
41
|
+
export type Tool<R = never> = {
|
|
42
|
+
readonly _tag: "CodeModeTool";
|
|
43
|
+
readonly description: string;
|
|
44
|
+
readonly input: SchemaType;
|
|
45
|
+
readonly output: SchemaType | undefined;
|
|
46
|
+
readonly execute: (input: unknown) => Effect.Effect<unknown, unknown, R>;
|
|
47
|
+
};
|
|
48
|
+
type InputType<S> = S extends Schema.Decoder<unknown> ? S["Type"] : unknown;
|
|
49
|
+
type ResultType<S> = S extends undefined ? void : S extends Schema.Decoder<unknown> ? S["Encoded"] : unknown;
|
|
50
|
+
/** Options for declaring 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 execute: (input: InputType<I>) => Effect.Effect<ResultType<O>, unknown, R>;
|
|
56
|
+
};
|
|
57
|
+
export declare const isTool: <R = never>(value: Tool<R> | Namespace<R> | Tools<R> | undefined) => value is Tool<R>;
|
|
58
|
+
/**
|
|
59
|
+
* Declares one schema-described tool available to a CodeMode program through `tools.*`.
|
|
60
|
+
*
|
|
61
|
+
* Effect Schemas validate values; JSON Schemas only shape the model-visible signature.
|
|
62
|
+
* Without `output`, results are exposed as `void`. Hosts remain responsible for authorization
|
|
63
|
+
* and durable side effects.
|
|
64
|
+
*/
|
|
65
|
+
export declare const make: <I extends SchemaType, const O extends SchemaType | undefined = undefined, R = never>(options: Options<I, O, R>) => Tool<R>;
|
|
66
|
+
export {};
|
package/dist/tool.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { Effect, Schema } from "effect";
|
|
2
|
+
export const isTool = (value) => value !== undefined && Object.hasOwn(value, "_tag") && value._tag === "CodeModeTool";
|
|
3
|
+
/**
|
|
4
|
+
* Declares one schema-described tool available to a CodeMode program through `tools.*`.
|
|
5
|
+
*
|
|
6
|
+
* Effect Schemas validate values; JSON Schemas only shape the model-visible signature.
|
|
7
|
+
* Without `output`, results are exposed as `void`. Hosts remain responsible for authorization
|
|
8
|
+
* and durable side effects.
|
|
9
|
+
*/
|
|
10
|
+
export const make = (options) => ({
|
|
11
|
+
_tag: "CodeModeTool",
|
|
12
|
+
description: options.description,
|
|
13
|
+
input: options.input,
|
|
14
|
+
output: options.output,
|
|
15
|
+
execute: (input) => options.execute(input),
|
|
16
|
+
});
|
package/dist/tools.d.ts
ADDED
package/dist/tools.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/values.d.ts
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export * as Values from "./values.js";
|
|
2
|
+
import type { Fiber } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* Runtime values the interpreter recognizes by class. Each wraps the host value it stands for,
|
|
5
|
+
* so hosts construct these to hand a value to a program and receive them back unchanged.
|
|
6
|
+
*/
|
|
7
|
+
export declare class Promise {
|
|
8
|
+
readonly fiber: Fiber.Fiber<unknown, unknown>;
|
|
9
|
+
constructor(fiber: Fiber.Fiber<unknown, unknown>);
|
|
10
|
+
}
|
|
11
|
+
export declare class Date {
|
|
12
|
+
time: number;
|
|
13
|
+
constructor(time: number);
|
|
14
|
+
}
|
|
15
|
+
export declare class RegExp {
|
|
16
|
+
readonly regex: globalThis.RegExp;
|
|
17
|
+
constructor(pattern: string, flags: string);
|
|
18
|
+
get lastIndex(): unknown;
|
|
19
|
+
set lastIndex(value: unknown);
|
|
20
|
+
}
|
|
21
|
+
export declare class Map {
|
|
22
|
+
readonly map: globalThis.Map<unknown, unknown>;
|
|
23
|
+
}
|
|
24
|
+
export declare class Set {
|
|
25
|
+
readonly set: globalThis.Set<unknown>;
|
|
26
|
+
}
|
|
27
|
+
export declare class URLSearchParams {
|
|
28
|
+
readonly params: globalThis.URLSearchParams;
|
|
29
|
+
constructor(params: globalThis.URLSearchParams);
|
|
30
|
+
}
|
|
31
|
+
export declare class URL {
|
|
32
|
+
readonly url: globalThis.URL;
|
|
33
|
+
readonly searchParams: URLSearchParams;
|
|
34
|
+
constructor(url: globalThis.URL);
|
|
35
|
+
}
|
|
36
|
+
/** Data-like runtime values; excludes Promise, which never crosses a boundary. */
|
|
37
|
+
export declare const isValue: (value: unknown) => value is Date | RegExp | Map | Set | URL | URLSearchParams;
|