@opencode-ai/codemode 0.0.0-beta-17492
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 +168 -0
- package/dist/codemode.d.ts +148 -0
- package/dist/codemode.js +70 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +5 -0
- package/dist/interpreter/errors.d.ts +9 -0
- package/dist/interpreter/errors.js +91 -0
- package/dist/interpreter/execute.d.ts +4 -0
- package/dist/interpreter/execute.js +178 -0
- package/dist/interpreter/iterator.d.ts +13 -0
- package/dist/interpreter/iterator.js +4 -0
- package/dist/interpreter/methods.d.ts +17 -0
- package/dist/interpreter/methods.js +1026 -0
- package/dist/interpreter/model.d.ts +151 -0
- package/dist/interpreter/model.js +186 -0
- package/dist/interpreter/promises.d.ts +29 -0
- package/dist/interpreter/promises.js +253 -0
- package/dist/interpreter/references.d.ts +6 -0
- package/dist/interpreter/references.js +114 -0
- package/dist/interpreter/runtime.d.ts +98 -0
- package/dist/interpreter/runtime.js +2351 -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/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 +588 -0
- package/dist/openapi/types.d.ts +122 -0
- package/dist/openapi/types.js +2 -0
- package/dist/stdlib/collections.d.ts +4 -0
- package/dist/stdlib/collections.js +57 -0
- package/dist/stdlib/console.d.ts +2 -0
- package/dist/stdlib/console.js +126 -0
- package/dist/stdlib/date.d.ts +7 -0
- package/dist/stdlib/date.js +186 -0
- package/dist/stdlib/json.d.ts +6 -0
- package/dist/stdlib/json.js +124 -0
- package/dist/stdlib/math.d.ts +12 -0
- package/dist/stdlib/math.js +157 -0
- package/dist/stdlib/number.d.ts +6 -0
- package/dist/stdlib/number.js +76 -0
- package/dist/stdlib/object.d.ts +7 -0
- package/dist/stdlib/object.js +100 -0
- package/dist/stdlib/promise.d.ts +2 -0
- package/dist/stdlib/promise.js +1 -0
- package/dist/stdlib/regexp.d.ts +11 -0
- package/dist/stdlib/regexp.js +106 -0
- package/dist/stdlib/string.d.ts +4 -0
- package/dist/stdlib/string.js +48 -0
- package/dist/stdlib/url.d.ts +12 -0
- package/dist/stdlib/url.js +84 -0
- package/dist/stdlib/value.d.ts +12 -0
- package/dist/stdlib/value.js +120 -0
- package/dist/tool-error.d.ts +11 -0
- package/dist/tool-error.js +9 -0
- package/dist/tool-runtime.d.ts +68 -0
- package/dist/tool-runtime.js +390 -0
- package/dist/tool-schema.d.ts +15 -0
- package/dist/tool-schema.js +213 -0
- package/dist/tool.d.ts +55 -0
- package/dist/tool.js +21 -0
- package/dist/tools.d.ts +4 -0
- package/dist/tools.js +1 -0
- package/dist/values.d.ts +31 -0
- package/dist/values.js +50 -0
- package/package.json +46 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
export const errorConstructors = new Set([
|
|
2
|
+
"Error",
|
|
3
|
+
"TypeError",
|
|
4
|
+
"RangeError",
|
|
5
|
+
"SyntaxError",
|
|
6
|
+
"ReferenceError",
|
|
7
|
+
"EvalError",
|
|
8
|
+
"URIError",
|
|
9
|
+
"AggregateError",
|
|
10
|
+
]);
|
|
11
|
+
export const valueConstructors = new Set(["Date", "RegExp", "Map", "Set", "URL", "URLSearchParams"]);
|
|
12
|
+
export const compoundOperators = new Set(["+=", "-=", "*=", "/=", "%=", "**=", "&=", "|=", "^=", "<<=", ">>=", ">>>="]);
|
|
13
|
+
const ErrorBrand = Symbol("codemode.error");
|
|
14
|
+
export const createErrorValue = (name, message) => {
|
|
15
|
+
const value = Object.assign(Object.create(null), { name, message });
|
|
16
|
+
Object.defineProperty(value, ErrorBrand, { value: name });
|
|
17
|
+
return value;
|
|
18
|
+
};
|
|
19
|
+
export const createAggregateErrorValue = (errors, message) => Object.assign(createErrorValue("AggregateError", message), { errors });
|
|
20
|
+
export const errorBrandName = (value) => value !== null && typeof value === "object"
|
|
21
|
+
? value[ErrorBrand]
|
|
22
|
+
: undefined;
|
|
23
|
+
export const boundedData = (value, label) => copyIn(value, label, true);
|
|
24
|
+
export const coerceToString = (value) => {
|
|
25
|
+
if (value === null)
|
|
26
|
+
return "null";
|
|
27
|
+
if (value === undefined)
|
|
28
|
+
return "undefined";
|
|
29
|
+
if (value instanceof CodeModeDate)
|
|
30
|
+
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : "Invalid Date";
|
|
31
|
+
if (value instanceof CodeModeRegExp)
|
|
32
|
+
return `/${value.regex.source}/${value.regex.flags}`;
|
|
33
|
+
if (value instanceof CodeModeMap)
|
|
34
|
+
return "[object Map]";
|
|
35
|
+
if (value instanceof CodeModeSet)
|
|
36
|
+
return "[object Set]";
|
|
37
|
+
if (value instanceof CodeModeURL)
|
|
38
|
+
return value.url.href;
|
|
39
|
+
if (value instanceof CodeModeURLSearchParams)
|
|
40
|
+
return value.params.toString();
|
|
41
|
+
if (errorBrandName(value) !== undefined) {
|
|
42
|
+
// Match Error.prototype.toString: "name: message", or just one when the other is empty.
|
|
43
|
+
const error = value;
|
|
44
|
+
const name = typeof error.name === "string" ? error.name : "Error";
|
|
45
|
+
const message = typeof error.message === "string" ? error.message : "";
|
|
46
|
+
if (message === "")
|
|
47
|
+
return name;
|
|
48
|
+
if (name === "")
|
|
49
|
+
return message;
|
|
50
|
+
return `${name}: ${message}`;
|
|
51
|
+
}
|
|
52
|
+
if (typeof value === "object") {
|
|
53
|
+
return Array.isArray(value)
|
|
54
|
+
? value.map((item) => (item === null || item === undefined ? "" : coerceToString(item))).join(",")
|
|
55
|
+
: "[object Object]";
|
|
56
|
+
}
|
|
57
|
+
return String(value);
|
|
58
|
+
};
|
|
59
|
+
export const coerceToNumber = (value) => {
|
|
60
|
+
if (value instanceof CodeModeDate)
|
|
61
|
+
return value.time;
|
|
62
|
+
if (isCodeModeValue(value))
|
|
63
|
+
return Number.NaN;
|
|
64
|
+
// Arrays coerce through our own string coercion: host Number(array) joins with host
|
|
65
|
+
// ToPrimitive, which throws on the null-prototype objects the interpreter produces.
|
|
66
|
+
if (Array.isArray(value))
|
|
67
|
+
return Number(coerceToString(value));
|
|
68
|
+
return value !== null && typeof value === "object" ? Number.NaN : Number(value);
|
|
69
|
+
};
|
|
70
|
+
export const invokeCoercion = (ref, args, node) => {
|
|
71
|
+
// Native: Number() is 0 and String() is "", unlike their undefined-argument forms; the
|
|
72
|
+
// other coercers match native through the undefined-argument path below.
|
|
73
|
+
if (args.length === 0) {
|
|
74
|
+
if (ref.name === "Number")
|
|
75
|
+
return 0;
|
|
76
|
+
if (ref.name === "String")
|
|
77
|
+
return "";
|
|
78
|
+
}
|
|
79
|
+
const raw = args[0];
|
|
80
|
+
// Error values are plain SafeObjects; the boundedData path below would strip their brand.
|
|
81
|
+
if (ref.name === "String" && errorBrandName(raw) !== undefined)
|
|
82
|
+
return coerceToString(raw);
|
|
83
|
+
if (isCodeModeValue(raw)) {
|
|
84
|
+
if (ref.name === "Boolean")
|
|
85
|
+
return true;
|
|
86
|
+
if (ref.name === "Number")
|
|
87
|
+
return coerceToNumber(raw);
|
|
88
|
+
if (ref.name === "String")
|
|
89
|
+
return coerceToString(raw);
|
|
90
|
+
if (ref.name === "isFinite")
|
|
91
|
+
return Number.isFinite(coerceToNumber(raw));
|
|
92
|
+
if (ref.name === "isNaN")
|
|
93
|
+
return Number.isNaN(coerceToNumber(raw));
|
|
94
|
+
if (ref.name === "parseInt")
|
|
95
|
+
return parseInt(coerceToString(raw));
|
|
96
|
+
return parseFloat(coerceToString(raw));
|
|
97
|
+
}
|
|
98
|
+
const value = boundedData(raw, `${ref.name} input`);
|
|
99
|
+
if (ref.name === "Number")
|
|
100
|
+
return coerceToNumber(value);
|
|
101
|
+
if (ref.name === "Boolean")
|
|
102
|
+
return Boolean(value);
|
|
103
|
+
if (ref.name === "isFinite")
|
|
104
|
+
return Number.isFinite(coerceToNumber(value));
|
|
105
|
+
if (ref.name === "isNaN")
|
|
106
|
+
return Number.isNaN(coerceToNumber(value));
|
|
107
|
+
if (ref.name === "parseInt") {
|
|
108
|
+
const radix = args[1];
|
|
109
|
+
if (radix !== undefined && typeof radix !== "number") {
|
|
110
|
+
throw new InterpreterRuntimeError("parseInt expects a numeric radix.", node);
|
|
111
|
+
}
|
|
112
|
+
return parseInt(coerceToString(value), radix);
|
|
113
|
+
}
|
|
114
|
+
if (ref.name === "parseFloat")
|
|
115
|
+
return parseFloat(coerceToString(value));
|
|
116
|
+
return coerceToString(value);
|
|
117
|
+
};
|
|
118
|
+
import { CoercionFunction, InterpreterRuntimeError } from "../interpreter/model.js";
|
|
119
|
+
import { copyIn } from "../tool-runtime.js";
|
|
120
|
+
import { isCodeModeValue, CodeModeDate, CodeModeMap, CodeModeRegExp, CodeModeSet, CodeModeURL, CodeModeURLSearchParams, } from "../values.js";
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
declare const ToolError_base: Schema.Class<ToolError, Schema.TaggedStruct<"ToolError", {
|
|
3
|
+
readonly message: Schema.String;
|
|
4
|
+
readonly cause: Schema.optionalKey<Schema.Defect>;
|
|
5
|
+
}>, import("effect/Cause").YieldableError>;
|
|
6
|
+
/** Safe operational refusal from a standard tool pack, reported as `ToolFailure`. */
|
|
7
|
+
export declare class ToolError extends ToolError_base {
|
|
8
|
+
}
|
|
9
|
+
/** Creates a tool refusal whose message is safe to include in an execution diagnostic. */
|
|
10
|
+
export declare const toolError: (message: string, cause?: unknown) => ToolError;
|
|
11
|
+
export {};
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Schema } from "effect";
|
|
2
|
+
/** Safe operational refusal from a standard tool pack, reported as `ToolFailure`. */
|
|
3
|
+
export class ToolError extends Schema.TaggedErrorClass()("ToolError", {
|
|
4
|
+
message: Schema.String,
|
|
5
|
+
cause: Schema.optionalKey(Schema.Defect()),
|
|
6
|
+
}) {
|
|
7
|
+
}
|
|
8
|
+
/** Creates a tool refusal whose message is safe to include in an execution diagnostic. */
|
|
9
|
+
export const toolError = (message, cause) => new ToolError({ message, ...(cause === undefined ? {} : { cause }) });
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import type { Tools } from "./tools.js";
|
|
3
|
+
export type Services<T> = ServicesOf<T, []>;
|
|
4
|
+
type ServicesOf<T, Depth extends ReadonlyArray<unknown>> = Depth["length"] extends 8 ? never : T extends {
|
|
5
|
+
readonly _tag: "CodeModeTool";
|
|
6
|
+
readonly execute: (input: unknown) => Effect.Effect<unknown, unknown, infer R>;
|
|
7
|
+
} ? R : T extends object ? string extends keyof T ? ServicesOf<T[string], [...Depth, unknown]> : ServicesOf<T[keyof T], [...Depth, unknown]> : never;
|
|
8
|
+
export type ToolCall = {
|
|
9
|
+
readonly name: string;
|
|
10
|
+
};
|
|
11
|
+
export type ToolCallStarted = {
|
|
12
|
+
readonly index: number;
|
|
13
|
+
readonly name: string;
|
|
14
|
+
readonly input: unknown;
|
|
15
|
+
};
|
|
16
|
+
export type ToolCallEnded = {
|
|
17
|
+
readonly index: number;
|
|
18
|
+
readonly name: string;
|
|
19
|
+
readonly input: unknown;
|
|
20
|
+
readonly durationMs: number;
|
|
21
|
+
readonly outcome: "success" | "failure" | "interrupted";
|
|
22
|
+
readonly message?: string;
|
|
23
|
+
};
|
|
24
|
+
export type ToolCallHooks<R = never> = {
|
|
25
|
+
readonly onToolCallStart?: ((call: ToolCallStarted) => Effect.Effect<void, never, R>) | undefined;
|
|
26
|
+
readonly onToolCallEnd?: ((call: ToolCallEnded) => Effect.Effect<void, never, R>) | undefined;
|
|
27
|
+
};
|
|
28
|
+
export type ToolDescription = {
|
|
29
|
+
readonly path: string;
|
|
30
|
+
readonly description: string;
|
|
31
|
+
readonly signature: string;
|
|
32
|
+
};
|
|
33
|
+
export type SafeObject = Record<string, unknown>;
|
|
34
|
+
export declare const toolExpression: (path: string) => string;
|
|
35
|
+
export declare class ToolReference {
|
|
36
|
+
readonly path: ReadonlyArray<string>;
|
|
37
|
+
constructor(path: ReadonlyArray<string>);
|
|
38
|
+
}
|
|
39
|
+
export declare class ToolRuntimeError extends Error {
|
|
40
|
+
readonly kind: "UnknownTool" | "InvalidToolInput" | "InvalidToolOutput" | "InvalidDataValue" | "ToolCallLimitExceeded";
|
|
41
|
+
readonly suggestions: ReadonlyArray<string>;
|
|
42
|
+
constructor(kind: "UnknownTool" | "InvalidToolInput" | "InvalidToolOutput" | "InvalidDataValue" | "ToolCallLimitExceeded", message: string, suggestions?: ReadonlyArray<string>);
|
|
43
|
+
}
|
|
44
|
+
export declare const isBlockedMember: (name: string) => boolean;
|
|
45
|
+
export declare const copyIn: (value: unknown, label: string, preserveCodeModeValues?: boolean) => unknown;
|
|
46
|
+
export type CopyOutMode = "json" | "nullify";
|
|
47
|
+
export declare const copyOut: (value: unknown, mode: CopyOutMode) => unknown;
|
|
48
|
+
export type DiscoveryPlan = {
|
|
49
|
+
readonly catalog: ReadonlyArray<ToolDescription>;
|
|
50
|
+
readonly searchIndex: ReadonlyArray<SearchEntry>;
|
|
51
|
+
};
|
|
52
|
+
export type SearchEntry = {
|
|
53
|
+
readonly description: ToolDescription;
|
|
54
|
+
readonly searchText: string;
|
|
55
|
+
};
|
|
56
|
+
/** Exact callable signature of the built-in `search` function, for host-owned instructions. */
|
|
57
|
+
export declare const searchSignature: string;
|
|
58
|
+
export declare const searchIndex: <R>(tools: Tools<R>) => ReadonlyArray<SearchEntry>;
|
|
59
|
+
export declare const prepare: <R>(tools: Tools<R>) => DiscoveryPlan;
|
|
60
|
+
export type ToolRuntime<R = never> = {
|
|
61
|
+
readonly root: ToolReference;
|
|
62
|
+
readonly calls: Array<ToolCall>;
|
|
63
|
+
readonly execute: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>;
|
|
64
|
+
readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>;
|
|
65
|
+
readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>;
|
|
66
|
+
};
|
|
67
|
+
export declare const make: <R>(tools: Tools<R>, maxToolCalls: number | undefined, searchIndex: ReadonlyArray<SearchEntry>, hooks?: ToolCallHooks<R>) => ToolRuntime<R>;
|
|
68
|
+
export * as ToolRuntime from "./tool-runtime.js";
|
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
import { Cause, Effect, Exit, Schema } from "effect";
|
|
2
|
+
import { ToolError, toolError } from "./tool-error.js";
|
|
3
|
+
import { decodeInput as decodeToolInput, decodeOutput as decodeToolOutput, identifierSegment, inputProperties, inputTypeScript, outputTypeScript, } from "./tool-schema.js";
|
|
4
|
+
import { isTool } from "./tool.js";
|
|
5
|
+
import { CodeModeDate, CodeModeMap, CodeModePromise, CodeModeRegExp, CodeModeSet, CodeModeURL, CodeModeURLSearchParams, } from "./values.js";
|
|
6
|
+
const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
7
|
+
const defaultSearchLimit = 10;
|
|
8
|
+
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
9
|
+
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
10
|
+
const SearchInput = Schema.Struct({
|
|
11
|
+
query: Schema.optionalKey(Schema.String),
|
|
12
|
+
namespace: Schema.optionalKey(Schema.String),
|
|
13
|
+
limit: Schema.optionalKey(PositiveInt),
|
|
14
|
+
offset: Schema.optionalKey(NonNegativeInt),
|
|
15
|
+
});
|
|
16
|
+
const SearchItem = Schema.Struct({
|
|
17
|
+
path: Schema.String,
|
|
18
|
+
description: Schema.String,
|
|
19
|
+
signature: Schema.String,
|
|
20
|
+
});
|
|
21
|
+
const SearchOutput = Schema.Struct({
|
|
22
|
+
items: Schema.Array(SearchItem),
|
|
23
|
+
remaining: NonNegativeInt,
|
|
24
|
+
next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })),
|
|
25
|
+
});
|
|
26
|
+
export const toolExpression = (path) => "tools" +
|
|
27
|
+
path
|
|
28
|
+
.split(".")
|
|
29
|
+
.map((segment) => (identifierSegment.test(segment) ? `.${segment}` : `[${JSON.stringify(segment)}]`))
|
|
30
|
+
.join("");
|
|
31
|
+
export class ToolReference {
|
|
32
|
+
path;
|
|
33
|
+
constructor(path) {
|
|
34
|
+
this.path = path;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
const MAX_VALUE_DEPTH = 32;
|
|
38
|
+
export class ToolRuntimeError extends Error {
|
|
39
|
+
kind;
|
|
40
|
+
suggestions;
|
|
41
|
+
constructor(kind, message, suggestions = []) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.kind = kind;
|
|
44
|
+
this.suggestions = suggestions;
|
|
45
|
+
this.name = "ToolRuntimeError";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const runHost = (effect) => effect.pipe(Effect.catchCause((cause) => {
|
|
49
|
+
if (Cause.hasInterruptsOnly(cause))
|
|
50
|
+
return Effect.interrupt;
|
|
51
|
+
const error = Cause.squash(cause);
|
|
52
|
+
return Effect.fail(error instanceof ToolError ? error : toolError("Tool execution failed", error));
|
|
53
|
+
}));
|
|
54
|
+
const blockedMemberNames = new Set(["__proto__", "constructor", "prototype"]);
|
|
55
|
+
export const isBlockedMember = (name) => blockedMemberNames.has(name);
|
|
56
|
+
// Checkpoint mode preserves CodeMode values; boundary mode JSON-normalizes them.
|
|
57
|
+
export const copyIn = (value, label, preserveCodeModeValues = false) => copyBounded(value, label, 0, new Set(), preserveCodeModeValues);
|
|
58
|
+
const copyBounded = (value, label, depth, seen, preserveCodeModeValues) => {
|
|
59
|
+
if (depth > MAX_VALUE_DEPTH) {
|
|
60
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} exceeds the maximum value depth of ${MAX_VALUE_DEPTH}.`);
|
|
61
|
+
}
|
|
62
|
+
if (value === null ||
|
|
63
|
+
value === undefined ||
|
|
64
|
+
typeof value === "string" ||
|
|
65
|
+
typeof value === "boolean" ||
|
|
66
|
+
typeof value === "number") {
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
if (typeof value !== "object") {
|
|
70
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain data only.`);
|
|
71
|
+
}
|
|
72
|
+
if (value instanceof CodeModePromise) {
|
|
73
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} contains an un-awaited Promise; await tool calls (e.g. \`const result = await tools.ns.tool(...)\`) before using their results.`);
|
|
74
|
+
}
|
|
75
|
+
if (preserveCodeModeValues) {
|
|
76
|
+
if (value instanceof CodeModeDate ||
|
|
77
|
+
value instanceof CodeModeRegExp ||
|
|
78
|
+
value instanceof CodeModeMap ||
|
|
79
|
+
value instanceof CodeModeSet ||
|
|
80
|
+
value instanceof CodeModeURL ||
|
|
81
|
+
value instanceof CodeModeURLSearchParams) {
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
84
|
+
if (value instanceof Date)
|
|
85
|
+
return new CodeModeDate(value.getTime());
|
|
86
|
+
if (value instanceof RegExp)
|
|
87
|
+
return new CodeModeRegExp(value.source, value.flags);
|
|
88
|
+
if (value instanceof Map) {
|
|
89
|
+
const wrapped = new CodeModeMap();
|
|
90
|
+
for (const [key, item] of value.entries()) {
|
|
91
|
+
wrapped.map.set(copyBounded(key, label, depth + 1, seen, true), copyBounded(item, label, depth + 1, seen, true));
|
|
92
|
+
}
|
|
93
|
+
return wrapped;
|
|
94
|
+
}
|
|
95
|
+
if (value instanceof Set) {
|
|
96
|
+
const wrapped = new CodeModeSet();
|
|
97
|
+
for (const item of value.values())
|
|
98
|
+
wrapped.set.add(copyBounded(item, label, depth + 1, seen, true));
|
|
99
|
+
return wrapped;
|
|
100
|
+
}
|
|
101
|
+
if (value instanceof URL)
|
|
102
|
+
return new CodeModeURL(new URL(value.href));
|
|
103
|
+
if (value instanceof URLSearchParams)
|
|
104
|
+
return new CodeModeURLSearchParams(new URLSearchParams(value));
|
|
105
|
+
}
|
|
106
|
+
if (value instanceof CodeModeDate) {
|
|
107
|
+
return Number.isFinite(value.time) ? new Date(value.time).toISOString() : null;
|
|
108
|
+
}
|
|
109
|
+
if (value instanceof Date) {
|
|
110
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : null;
|
|
111
|
+
}
|
|
112
|
+
if (value instanceof CodeModeURL)
|
|
113
|
+
return value.url.href;
|
|
114
|
+
if (value instanceof URL)
|
|
115
|
+
return value.href;
|
|
116
|
+
if (value instanceof CodeModeRegExp ||
|
|
117
|
+
value instanceof CodeModeMap ||
|
|
118
|
+
value instanceof CodeModeSet ||
|
|
119
|
+
value instanceof CodeModeURLSearchParams ||
|
|
120
|
+
value instanceof RegExp ||
|
|
121
|
+
value instanceof Map ||
|
|
122
|
+
value instanceof Set ||
|
|
123
|
+
value instanceof URLSearchParams) {
|
|
124
|
+
return Object.create(null);
|
|
125
|
+
}
|
|
126
|
+
if (seen.has(value)) {
|
|
127
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} contains a circular value.`);
|
|
128
|
+
}
|
|
129
|
+
seen.add(value);
|
|
130
|
+
if (Array.isArray(value)) {
|
|
131
|
+
const copied = value.map((item) => copyBounded(item, label, depth + 1, seen, preserveCodeModeValues));
|
|
132
|
+
if (preserveCodeModeValues) {
|
|
133
|
+
// Checkpoint copies retain array metadata that boundary copies omit.
|
|
134
|
+
for (const [key, item] of Object.entries(value)) {
|
|
135
|
+
if (Object.hasOwn(copied, key))
|
|
136
|
+
continue;
|
|
137
|
+
if (isBlockedMember(key)) {
|
|
138
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`);
|
|
139
|
+
}
|
|
140
|
+
Reflect.set(copied, key, copyBounded(item, label, depth + 1, seen, true));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
seen.delete(value);
|
|
144
|
+
return copied;
|
|
145
|
+
}
|
|
146
|
+
const prototype = Object.getPrototypeOf(value);
|
|
147
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
148
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} must contain plain objects only.`);
|
|
149
|
+
}
|
|
150
|
+
const copied = Object.create(null);
|
|
151
|
+
for (const [key, item] of Object.entries(value)) {
|
|
152
|
+
if (isBlockedMember(key)) {
|
|
153
|
+
throw new ToolRuntimeError("InvalidDataValue", `${label} contains blocked property '${key}'.`);
|
|
154
|
+
}
|
|
155
|
+
copied[key] = copyBounded(item, label, depth + 1, seen, preserveCodeModeValues);
|
|
156
|
+
}
|
|
157
|
+
seen.delete(value);
|
|
158
|
+
return copied;
|
|
159
|
+
};
|
|
160
|
+
export const copyOut = (value, mode) => {
|
|
161
|
+
if (value === undefined && mode === "nullify")
|
|
162
|
+
return null;
|
|
163
|
+
if (typeof value === "number" && !Number.isFinite(value)) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
if (Array.isArray(value)) {
|
|
167
|
+
// Array.from densifies holes so sparse arrays normalize at the boundary like JSON does.
|
|
168
|
+
return Array.from(value, (item) => {
|
|
169
|
+
const copied = copyOut(item, mode);
|
|
170
|
+
return copied === undefined && mode === "json" ? null : copied;
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
if (value !== null && typeof value === "object" && !(value instanceof ToolReference)) {
|
|
174
|
+
return Object.fromEntries(Object.entries(value)
|
|
175
|
+
.map(([key, item]) => [key, copyOut(item, mode)])
|
|
176
|
+
.filter(([, item]) => !(item === undefined && mode === "json")));
|
|
177
|
+
}
|
|
178
|
+
return value;
|
|
179
|
+
};
|
|
180
|
+
const toolTrie = (tools) => {
|
|
181
|
+
const root = { children: new Map() };
|
|
182
|
+
const insert = (node, group) => {
|
|
183
|
+
for (const [name, value] of Object.entries(group)) {
|
|
184
|
+
let current = node;
|
|
185
|
+
for (const segment of name.split(".")) {
|
|
186
|
+
if (segment === "")
|
|
187
|
+
throw new TypeError(`Tool name '${name}' contains an empty segment.`);
|
|
188
|
+
const child = current.children.get(segment) ?? { children: new Map() };
|
|
189
|
+
current.children.set(segment, child);
|
|
190
|
+
current = child;
|
|
191
|
+
}
|
|
192
|
+
if (isTool(value))
|
|
193
|
+
current.tool = value;
|
|
194
|
+
else
|
|
195
|
+
insert(current, value);
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
insert(root, tools);
|
|
199
|
+
return root;
|
|
200
|
+
};
|
|
201
|
+
const canonicalSegments = (path) => path.flatMap((segment) => segment.split("."));
|
|
202
|
+
const flattenTools = (node, path = []) => [
|
|
203
|
+
...(node.tool === undefined ? [] : [{ path: path.join("."), tool: node.tool }]),
|
|
204
|
+
...Array.from(node.children, ([name, child]) => flattenTools(child, [...path, name])).flat(),
|
|
205
|
+
];
|
|
206
|
+
const describeTool = (path, tool) => ({
|
|
207
|
+
path,
|
|
208
|
+
description: tool.description,
|
|
209
|
+
signature: `${toolExpression(path)}(input: ${inputTypeScript(tool, true)}): Promise<${outputTypeScript(tool, true)}>`,
|
|
210
|
+
});
|
|
211
|
+
// Discovery bytes are durable instructions, so order only after canonical-path collisions settle.
|
|
212
|
+
const visibleTools = (tools) => flattenTools(toolTrie(tools))
|
|
213
|
+
.sort((left, right) => compareText(left.path, right.path))
|
|
214
|
+
.map(({ path, tool }) => ({
|
|
215
|
+
path,
|
|
216
|
+
tool,
|
|
217
|
+
description: describeTool(path, tool),
|
|
218
|
+
}));
|
|
219
|
+
const tokenize = (query) => query
|
|
220
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
221
|
+
.toLowerCase()
|
|
222
|
+
.split(/[^a-z0-9]+/)
|
|
223
|
+
.filter((term) => term.length > 0 && term !== "*");
|
|
224
|
+
const termForms = (term) => {
|
|
225
|
+
const forms = [term];
|
|
226
|
+
if (term.endsWith("es") && term.length > 3)
|
|
227
|
+
forms.push(term.slice(0, -2));
|
|
228
|
+
if (term.endsWith("s") && term.length > 2)
|
|
229
|
+
forms.push(term.slice(0, -1));
|
|
230
|
+
return forms;
|
|
231
|
+
};
|
|
232
|
+
const makeSearchTool = (searchIndex) => ({
|
|
233
|
+
_tag: "CodeModeTool",
|
|
234
|
+
description: "Search available tools",
|
|
235
|
+
input: SearchInput,
|
|
236
|
+
output: SearchOutput,
|
|
237
|
+
execute: (input) => Effect.sync(() => {
|
|
238
|
+
const request = input;
|
|
239
|
+
const query = request.query ?? "";
|
|
240
|
+
const offset = request.offset ?? 0;
|
|
241
|
+
const scoped = request.namespace === undefined
|
|
242
|
+
? searchIndex
|
|
243
|
+
: searchIndex.filter((entry) => entry.description.path === request.namespace ||
|
|
244
|
+
entry.description.path.startsWith(`${request.namespace}.`));
|
|
245
|
+
const trimmed = query.trim();
|
|
246
|
+
const pathQuery = trimmed.startsWith("tools.") ? trimmed.slice("tools.".length) : trimmed;
|
|
247
|
+
const exact = pathQuery === ""
|
|
248
|
+
? undefined
|
|
249
|
+
: scoped.find((entry) => entry.description.path === pathQuery || toolExpression(entry.description.path) === trimmed);
|
|
250
|
+
const terms = tokenize(query).map(termForms);
|
|
251
|
+
const ranked = exact !== undefined
|
|
252
|
+
? [exact]
|
|
253
|
+
: scoped
|
|
254
|
+
.map((entry) => {
|
|
255
|
+
const path = entry.description.path.toLowerCase();
|
|
256
|
+
const description = entry.description.description.toLowerCase();
|
|
257
|
+
const score = terms.reduce((total, forms) => total +
|
|
258
|
+
(forms.some((form) => path === form || path.endsWith(`.${form}`)) ? 20 : 0) +
|
|
259
|
+
(forms.some((form) => path.includes(form)) ? 8 : 0) +
|
|
260
|
+
(forms.some((form) => description.includes(form)) ? 4 : 0) +
|
|
261
|
+
(forms.some((form) => entry.searchText.includes(form)) ? 2 : 0), 0);
|
|
262
|
+
return { entry, score };
|
|
263
|
+
})
|
|
264
|
+
.filter(({ score }) => terms.length === 0 || score > 0)
|
|
265
|
+
.sort((left, right) => right.score - left.score || compareText(left.entry.description.path, right.entry.description.path))
|
|
266
|
+
.map(({ entry }) => entry);
|
|
267
|
+
const items = ranked.slice(offset, offset + (request.limit ?? defaultSearchLimit)).map(({ description }) => ({
|
|
268
|
+
...description,
|
|
269
|
+
path: toolExpression(description.path),
|
|
270
|
+
}));
|
|
271
|
+
const remaining = Math.max(0, ranked.length - offset - items.length);
|
|
272
|
+
return {
|
|
273
|
+
items,
|
|
274
|
+
remaining,
|
|
275
|
+
next: remaining > 0 ? { offset: offset + items.length } : null,
|
|
276
|
+
};
|
|
277
|
+
}),
|
|
278
|
+
});
|
|
279
|
+
/** Exact callable signature of the built-in `search` function, for host-owned instructions. */
|
|
280
|
+
export const searchSignature = (() => {
|
|
281
|
+
const tool = makeSearchTool([]);
|
|
282
|
+
return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`;
|
|
283
|
+
})();
|
|
284
|
+
const toSearchEntry = (path, tool, description) => ({
|
|
285
|
+
description,
|
|
286
|
+
searchText: [
|
|
287
|
+
path,
|
|
288
|
+
tool.description,
|
|
289
|
+
...inputProperties(tool).flatMap(({ name, description: property }) => property === undefined ? [name] : [name, property]),
|
|
290
|
+
]
|
|
291
|
+
.join("\n")
|
|
292
|
+
.toLowerCase(),
|
|
293
|
+
});
|
|
294
|
+
export const searchIndex = (tools) => visibleTools(tools).map(({ path, tool, description }) => toSearchEntry(path, tool, description));
|
|
295
|
+
export const prepare = (tools) => {
|
|
296
|
+
const visible = visibleTools(tools);
|
|
297
|
+
return {
|
|
298
|
+
catalog: visible.map(({ description }) => description),
|
|
299
|
+
searchIndex: visible.map(({ path, tool, description }) => toSearchEntry(path, tool, description)),
|
|
300
|
+
};
|
|
301
|
+
};
|
|
302
|
+
const lookup = (root, segments) => segments.reduce((node, segment) => node?.children.get(segment), root);
|
|
303
|
+
const namespaceKeys = (root, path) => {
|
|
304
|
+
const segments = canonicalSegments(path);
|
|
305
|
+
const node = lookup(root, segments);
|
|
306
|
+
if (node === undefined) {
|
|
307
|
+
throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${segments.join(".")}'.`);
|
|
308
|
+
}
|
|
309
|
+
return Array.from(node.children.keys());
|
|
310
|
+
};
|
|
311
|
+
const resolve = (root, path) => {
|
|
312
|
+
const segments = canonicalSegments(path);
|
|
313
|
+
const node = lookup(root, segments);
|
|
314
|
+
if (node === undefined) {
|
|
315
|
+
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
|
|
316
|
+
"The tool may have been removed or renamed. Use search to find available tools.",
|
|
317
|
+
]);
|
|
318
|
+
}
|
|
319
|
+
if (node.tool === undefined) {
|
|
320
|
+
throw new ToolRuntimeError("UnknownTool", `Tool '${segments.join(".")}' is not callable.`);
|
|
321
|
+
}
|
|
322
|
+
return node.tool;
|
|
323
|
+
};
|
|
324
|
+
export const make = (tools, maxToolCalls, searchIndex, hooks) => {
|
|
325
|
+
const calls = [];
|
|
326
|
+
const root = toolTrie(tools);
|
|
327
|
+
const searchTool = makeSearchTool(searchIndex);
|
|
328
|
+
const observeEnd = (effect, call) => {
|
|
329
|
+
const onEnd = hooks?.onToolCallEnd;
|
|
330
|
+
if (onEnd === undefined)
|
|
331
|
+
return effect;
|
|
332
|
+
const startedAt = Date.now();
|
|
333
|
+
return effect.pipe(Effect.onExit((exit) => {
|
|
334
|
+
const durationMs = Date.now() - startedAt;
|
|
335
|
+
if (Exit.isSuccess(exit))
|
|
336
|
+
return onEnd({ ...call, durationMs, outcome: "success" });
|
|
337
|
+
if (Cause.hasInterruptsOnly(exit.cause))
|
|
338
|
+
return onEnd({ ...call, durationMs, outcome: "interrupted" });
|
|
339
|
+
const error = Cause.squash(exit.cause);
|
|
340
|
+
const message = error instanceof ToolError || error instanceof ToolRuntimeError ? error.message : "Tool execution failed";
|
|
341
|
+
return onEnd({ ...call, durationMs, outcome: "failure", message });
|
|
342
|
+
}));
|
|
343
|
+
};
|
|
344
|
+
const decodeOutput = (value, name) => Effect.try({
|
|
345
|
+
try: () => copyIn(value, `Result from tool '${name}'`),
|
|
346
|
+
catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`),
|
|
347
|
+
});
|
|
348
|
+
const recordCall = (call) => {
|
|
349
|
+
if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
|
|
350
|
+
throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`);
|
|
351
|
+
}
|
|
352
|
+
calls.push(call);
|
|
353
|
+
};
|
|
354
|
+
const executeTool = (name, tool, externalArgs) => Effect.gen(function* () {
|
|
355
|
+
if (externalArgs.length !== 1)
|
|
356
|
+
throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects exactly one input object.`);
|
|
357
|
+
const input = yield* Effect.try({
|
|
358
|
+
try: () => decodeToolInput(tool, externalArgs[0]),
|
|
359
|
+
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."]),
|
|
360
|
+
});
|
|
361
|
+
const index = yield* Effect.sync(() => {
|
|
362
|
+
recordCall({ name });
|
|
363
|
+
return calls.length - 1;
|
|
364
|
+
});
|
|
365
|
+
const call = { index, name, input };
|
|
366
|
+
return yield* observeEnd(Effect.gen(function* () {
|
|
367
|
+
if (hooks?.onToolCallStart !== undefined)
|
|
368
|
+
yield* hooks.onToolCallStart(call);
|
|
369
|
+
const raw = yield* runHost(Effect.suspend(() => tool.execute(input)));
|
|
370
|
+
const result = yield* Effect.try({
|
|
371
|
+
try: () => decodeToolOutput(tool, raw),
|
|
372
|
+
catch: () => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}'.`),
|
|
373
|
+
});
|
|
374
|
+
return yield* decodeOutput(result, name);
|
|
375
|
+
}), call);
|
|
376
|
+
});
|
|
377
|
+
return {
|
|
378
|
+
root: new ToolReference([]),
|
|
379
|
+
calls,
|
|
380
|
+
keys: (path) => namespaceKeys(root, path),
|
|
381
|
+
search: (args) => Effect.suspend(() => executeTool("search", searchTool, args.map((arg) => copyOut(copyIn(arg, "Arguments for tool 'search'"), "json")))),
|
|
382
|
+
execute: (path, args) => Effect.gen(function* () {
|
|
383
|
+
const name = canonicalSegments(path).join(".");
|
|
384
|
+
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`), "json"));
|
|
385
|
+
const tool = resolve(root, path);
|
|
386
|
+
return yield* executeTool(name, tool, externalArgs);
|
|
387
|
+
}),
|
|
388
|
+
};
|
|
389
|
+
};
|
|
390
|
+
export * as ToolRuntime from "./tool-runtime.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
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 outputTypeScript: <R>(tool: Tool<R>, pretty?: boolean) => string;
|
|
14
|
+
export declare const decodeInput: <R>(tool: Tool<R>, value: unknown) => unknown;
|
|
15
|
+
export declare const decodeOutput: <R>(tool: Tool<R>, value: unknown) => unknown;
|