@opencode/codemode 2.0.4 → 2.0.6
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 +5 -2
- package/dist/codemode.d.ts +4 -2
- package/dist/codemode.js +4 -2
- package/dist/interpreter/errors.js +10 -2
- package/dist/interpreter/execute.d.ts +1 -1
- package/dist/interpreter/extensions.js +69 -18
- package/dist/tool-runtime.d.ts +27 -15
- package/dist/tool-runtime.js +48 -40
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -88,8 +88,11 @@ runtime.catalog() // structured tool descriptions
|
|
|
88
88
|
runtime.execute(source) // Effect<CodeMode.Result, never, ToolServices>
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
-
The Effect environment is inferred from the supplied tools. `
|
|
92
|
-
|
|
91
|
+
The Effect environment is inferred from the supplied tools. `hooks` surround every call the program makes into the
|
|
92
|
+
host: `tool.before`/`tool.after` receive `{ name, input }` with decoded input, `extension.before`/`extension.after`
|
|
93
|
+
receive `{ extension, name, args }`. An `after` hook also receives how the call ended (`success` with its value,
|
|
94
|
+
`failure` with its error, or `interrupted`). A failing `before` hook denies the call, and the program catches the
|
|
95
|
+
failure as a thrown error.
|
|
93
96
|
|
|
94
97
|
### `Values`
|
|
95
98
|
|
package/dist/codemode.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { Extension } from "./extension.js";
|
|
|
3
3
|
import { type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js";
|
|
4
4
|
import type { Tools } from "./tools.js";
|
|
5
5
|
/** A tool call admitted during an execution. */
|
|
6
|
-
export type {
|
|
6
|
+
export type { CallResult, ExtensionInvocation, Hooks, ToolCall, ToolDescription, ToolInvocation, } from "./tool-runtime.js";
|
|
7
7
|
/** Signature-construction helpers for host-owned catalog instructions. */
|
|
8
8
|
export { searchSignature, toolExpression } from "./tool-runtime.js";
|
|
9
9
|
/** Resource budgets enforced independently during each CodeMode program execution. */
|
|
@@ -27,9 +27,11 @@ export type ResolvedExecutionLimits = {
|
|
|
27
27
|
readonly maxOutputBytes: number | undefined;
|
|
28
28
|
};
|
|
29
29
|
/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
|
|
30
|
-
export type Options<Provided extends Record<string, unknown> = {}> =
|
|
30
|
+
export type Options<Provided extends Record<string, unknown> = {}> = {
|
|
31
31
|
/** Explicit tools exposed to the program as `tools`. */
|
|
32
32
|
tools?: Provided & Tools<Services<Provided>>;
|
|
33
|
+
/** Hooks around every tool and extension call the program makes; see `Hooks`. */
|
|
34
|
+
hooks?: ToolRuntime.Hooks<Services<Provided>>;
|
|
33
35
|
/** Host functions exposed as globals; see `Extension.make`. */
|
|
34
36
|
extensions?: ReadonlyArray<Extension>;
|
|
35
37
|
/** Resource limits enforced on each execution. */
|
package/dist/codemode.js
CHANGED
|
@@ -71,7 +71,9 @@ export const make = (options = {}) => {
|
|
|
71
71
|
}
|
|
72
72
|
}
|
|
73
73
|
return {
|
|
74
|
-
catalog
|
|
75
|
-
|
|
74
|
+
get catalog() {
|
|
75
|
+
return prepared.catalog;
|
|
76
|
+
},
|
|
77
|
+
execute: (code) => executeProgram(code, prepared, limits, options.hooks ?? {}, (ctx) => extensionGlobals(ctx, extensions)),
|
|
76
78
|
};
|
|
77
79
|
};
|
|
@@ -5,7 +5,7 @@ import { formatLocation, PendingThrow, Throw, sourceLocation, typeError } from "
|
|
|
5
5
|
import { containsRuntimeReference } from "./references.js";
|
|
6
6
|
import { createErrorValue, isErrorType } from "./intrinsics.js";
|
|
7
7
|
import { constructor, methods, prototypeFrom, receiver } from "./native.js";
|
|
8
|
-
import { define, get, hidden, Arr, ErrorObj, Obj } from "./objects.js";
|
|
8
|
+
import { define, get, has, hidden, Arr, ErrorObj, Obj } from "./objects.js";
|
|
9
9
|
import { formatValue } from "../stdlib/console.js";
|
|
10
10
|
import { coerceToString } from "../stdlib/value.js";
|
|
11
11
|
export const normalizeError = (error) => {
|
|
@@ -128,9 +128,17 @@ export const errorGlobal = (type, ctx) => {
|
|
|
128
128
|
const prototype = builtins[type];
|
|
129
129
|
const construct = (args, newTarget) => {
|
|
130
130
|
const proto = prototypeFrom(newTarget, prototype);
|
|
131
|
-
|
|
131
|
+
const created = type === "AggregateError"
|
|
132
132
|
? constructAggregateErrorValue(ctx, args, proto)
|
|
133
133
|
: Effect.sync(() => createErrorValue(proto, args[0] === undefined ? undefined : coerceToString(args[0])));
|
|
134
|
+
// ES2022 `new Error(message, { cause })`: installed only when the options object has the property at all.
|
|
135
|
+
const options = args[type === "AggregateError" ? 2 : 1];
|
|
136
|
+
if (!(options instanceof Obj) || !has(options, "cause"))
|
|
137
|
+
return created;
|
|
138
|
+
return Effect.map(created, (value) => {
|
|
139
|
+
define(value, "cause", get(options, "cause"), hidden);
|
|
140
|
+
return value;
|
|
141
|
+
});
|
|
134
142
|
};
|
|
135
143
|
const ctor = constructor(builtins, prototype, {
|
|
136
144
|
name: type,
|
|
@@ -2,4 +2,4 @@ import { Effect } from "effect";
|
|
|
2
2
|
import type { ResolvedExecutionLimits, Result } from "../codemode.js";
|
|
3
3
|
import { ToolRuntime } from "../tool-runtime.js";
|
|
4
4
|
import { Interpreter } from "./interpreter.js";
|
|
5
|
-
export declare const executeProgram: <R>(code: string, prepared: ToolRuntime.Prepared<R>, limits: ResolvedExecutionLimits, hooks: ToolRuntime.
|
|
5
|
+
export declare const executeProgram: <R>(code: string, prepared: ToolRuntime.Prepared<R>, limits: ResolvedExecutionLimits, hooks: ToolRuntime.Hooks<R>, globals?: (ctx: Interpreter<R>) => ReadonlyArray<readonly [string, unknown]>) => Effect.Effect<Result, never, R>;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { Effect } from "effect";
|
|
2
2
|
import { coerceToString } from "../stdlib/value.js";
|
|
3
|
+
import { hooked } from "../tool-runtime.js";
|
|
3
4
|
import { createErrorValue, isErrorType } from "./intrinsics.js";
|
|
4
5
|
import { MAX_VALUE_DEPTH } from "./limits.js";
|
|
5
|
-
import { Throw, typeError } from "./model.js";
|
|
6
|
+
import { PendingThrow, Throw, typeError } from "./model.js";
|
|
6
7
|
import { fn } from "./native.js";
|
|
7
|
-
import { Callable, define, entries, get, Arr, Bytes, DateObj, ErrorObj, GeneratorObj, MapObj, Obj, PromiseObj, RegExpObj, SetObj, URLObj, URLSearchParamsObj, } from "./objects.js";
|
|
8
|
+
import { Callable, define, entries, get, has, hidden, keys, Arr, Bytes, DateObj, ErrorObj, GeneratorObj, MapObj, Obj, PromiseObj, RegExpObj, SetObj, URLObj, URLSearchParamsObj, } from "./objects.js";
|
|
8
9
|
import { describeValue } from "./references.js";
|
|
9
10
|
/**
|
|
10
11
|
* The global bindings of one run's extensions. Everything crossing the boundary is converted: plain data and
|
|
@@ -42,15 +43,30 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
42
43
|
value instanceof PromiseObj) {
|
|
43
44
|
throw typeError(`${label} contains ${describeValue(value)}, which cannot be passed to an extension.`);
|
|
44
45
|
}
|
|
46
|
+
if (seen.has(value))
|
|
47
|
+
throw typeError(`${label} contains a circular value.`);
|
|
48
|
+
seen.add(value);
|
|
45
49
|
if (value instanceof ErrorObj) {
|
|
46
50
|
const name = coerceToString(get(value, "name"));
|
|
47
51
|
const message = get(value, "message");
|
|
48
52
|
const text = message === undefined ? "" : coerceToString(message);
|
|
49
|
-
|
|
53
|
+
const copied = name === "AggregateError" ? new AggregateError([], text) : new (hostErrors.get(name) ?? Error)(text);
|
|
54
|
+
for (const key of new Set(["cause", ...keys(value)])) {
|
|
55
|
+
if (uncrossed.has(key) || !has(value, key))
|
|
56
|
+
continue;
|
|
57
|
+
const item = crossing(() => next(get(value, key)));
|
|
58
|
+
if (item === left)
|
|
59
|
+
continue;
|
|
60
|
+
Object.defineProperty(copied, key, {
|
|
61
|
+
value: item,
|
|
62
|
+
writable: true,
|
|
63
|
+
configurable: true,
|
|
64
|
+
enumerable: key !== "cause",
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
seen.delete(value);
|
|
68
|
+
return copied;
|
|
50
69
|
}
|
|
51
|
-
if (seen.has(value))
|
|
52
|
-
throw typeError(`${label} contains a circular value.`);
|
|
53
|
-
seen.add(value);
|
|
54
70
|
const copied = value instanceof Arr
|
|
55
71
|
? value.items.map(next)
|
|
56
72
|
: Object.fromEntries(entries(value)
|
|
@@ -67,6 +83,7 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
67
83
|
if (typeof value === "function")
|
|
68
84
|
return wrap(value, label);
|
|
69
85
|
if (value !== null && typeof value === "object") {
|
|
86
|
+
const next = (item, path) => fromHost(item, path, depth + 1, seen);
|
|
70
87
|
if (value instanceof Date)
|
|
71
88
|
return new DateObj(builtins.Date, value.getTime());
|
|
72
89
|
if (value instanceof RegExp)
|
|
@@ -76,14 +93,27 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
76
93
|
if (value instanceof ArrayBuffer)
|
|
77
94
|
return new Bytes(builtins.Uint8Array, new Uint8Array(value.slice(0)));
|
|
78
95
|
if (value instanceof Error) {
|
|
79
|
-
|
|
96
|
+
if (seen.has(value))
|
|
97
|
+
throw typeError(`${label} produced a circular value.`);
|
|
98
|
+
seen.add(value);
|
|
99
|
+
const copied = createErrorValue(builtins[isErrorType(value.name) ? value.name : "Error"], value.message);
|
|
100
|
+
const fields = value;
|
|
101
|
+
for (const key of new Set(["cause", ...Object.keys(value)])) {
|
|
102
|
+
if (uncrossed.has(key) || !(key in value) || typeof fields[key] === "function")
|
|
103
|
+
continue;
|
|
104
|
+
const item = crossing(() => next(fields[key], `${label}.${key}`));
|
|
105
|
+
if (item === left)
|
|
106
|
+
continue;
|
|
107
|
+
define(copied, key, item, key === "cause" ? hidden : undefined);
|
|
108
|
+
}
|
|
109
|
+
seen.delete(value);
|
|
110
|
+
return copied;
|
|
80
111
|
}
|
|
81
112
|
if (value instanceof URL)
|
|
82
113
|
return new URLObj(builtins.URL, builtins.URLSearchParams, new URL(value.href));
|
|
83
114
|
if (value instanceof URLSearchParams) {
|
|
84
115
|
return new URLSearchParamsObj(builtins.URLSearchParams, new URLSearchParams(value));
|
|
85
116
|
}
|
|
86
|
-
const next = (item, path) => fromHost(item, path, depth + 1, seen);
|
|
87
117
|
if (value instanceof Map) {
|
|
88
118
|
const wrapped = new MapObj(builtins.Map);
|
|
89
119
|
for (const [key, item] of value)
|
|
@@ -115,23 +145,44 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
115
145
|
}
|
|
116
146
|
throw typeError(`${label} produced ${describeHost(value)}, which the program cannot hold.`);
|
|
117
147
|
};
|
|
118
|
-
// A host function as a program function
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
148
|
+
// A host function as a program function. Arguments cross in; a global's call runs inside the host's extension
|
|
149
|
+
// hooks, which see the host's own error on failure; then whatever came back, or was thrown, crosses out so the
|
|
150
|
+
// program catches a copy. Functions inside results are part of a value's API and skip the hooks.
|
|
151
|
+
const wrap = (value, label, describe) => fn(builtins, value.name, value.length, (_, values) => {
|
|
152
|
+
const args = values.map((item, index) => toHost(item, `Argument ${index + 1} to ${label}`));
|
|
153
|
+
const hooks = ctx.tools.hooks;
|
|
154
|
+
const settle = (run) => (describe === undefined
|
|
155
|
+
? run
|
|
156
|
+
: hooked(describe(args), hooks["extension.before"], hooks["extension.after"], run)).pipe(Effect.mapError((reason) => new Throw(fromHost(reason, label))), Effect.map((settled) => fromHost(settled, label)));
|
|
123
157
|
let result;
|
|
124
158
|
try {
|
|
125
|
-
result = value.apply(undefined,
|
|
159
|
+
result = value.apply(undefined, args);
|
|
126
160
|
}
|
|
127
161
|
catch (reason) {
|
|
128
|
-
return Effect.fail(
|
|
162
|
+
return settle(Effect.fail(reason));
|
|
129
163
|
}
|
|
130
164
|
if (!(result instanceof Promise))
|
|
131
|
-
return
|
|
132
|
-
return ctx.pending.create(
|
|
165
|
+
return settle(Effect.succeed(result));
|
|
166
|
+
return ctx.pending.create(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason })));
|
|
133
167
|
});
|
|
134
|
-
return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name)]));
|
|
168
|
+
return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name, (args) => ({ extension: extension.name, name, args }))]));
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* An error crosses as its name, message, `cause`, and own enumerable fields, such as Node's `code`, `errno`,
|
|
172
|
+
* `syscall`, and `path`. `stack` stays on its own side, and no field may shadow an Error method. A field that cannot
|
|
173
|
+
* cross (a socket, a handle, a function) is left behind so the error itself always arrives.
|
|
174
|
+
*/
|
|
175
|
+
const uncrossed = new Set(["stack", "constructor", "toString", "__proto__"]);
|
|
176
|
+
const left = Symbol("left behind");
|
|
177
|
+
const crossing = (convert) => {
|
|
178
|
+
try {
|
|
179
|
+
return convert();
|
|
180
|
+
}
|
|
181
|
+
catch (reason) {
|
|
182
|
+
if (reason instanceof PendingThrow)
|
|
183
|
+
return left;
|
|
184
|
+
throw reason;
|
|
185
|
+
}
|
|
135
186
|
};
|
|
136
187
|
const hostErrors = new Map([
|
|
137
188
|
["TypeError", TypeError],
|
package/dist/tool-runtime.d.ts
CHANGED
|
@@ -13,25 +13,36 @@ type ServicesOf<T, Depth extends ReadonlyArray<unknown>> = Depth["length"] exten
|
|
|
13
13
|
export type ToolCall = {
|
|
14
14
|
readonly name: string;
|
|
15
15
|
};
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
/** A tool call the program is making, with its decoded input. */
|
|
17
|
+
export type ToolInvocation = {
|
|
18
18
|
readonly name: string;
|
|
19
19
|
readonly input: unknown;
|
|
20
20
|
};
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
/** A call the program is making to an extension global, with its arguments. */
|
|
22
|
+
export type ExtensionInvocation = {
|
|
23
|
+
readonly extension: string;
|
|
23
24
|
readonly name: string;
|
|
24
|
-
readonly
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
25
|
+
readonly args: ReadonlyArray<unknown>;
|
|
26
|
+
};
|
|
27
|
+
/** How a call ended; `after` hooks observe it and cannot change it. */
|
|
28
|
+
export type CallResult = {
|
|
29
|
+
readonly status: "success";
|
|
30
|
+
readonly value: unknown;
|
|
31
|
+
} | {
|
|
32
|
+
readonly status: "failure";
|
|
33
|
+
readonly error: unknown;
|
|
34
|
+
} | {
|
|
35
|
+
readonly status: "interrupted";
|
|
28
36
|
};
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
readonly
|
|
32
|
-
|
|
33
|
-
readonly
|
|
37
|
+
/** Hooks around every call the program makes into the host. A failing `before` denies the call. */
|
|
38
|
+
export type Hooks<R = never> = {
|
|
39
|
+
readonly "tool.before"?: ((call: ToolInvocation) => Effect.Effect<void, unknown, R>) | undefined;
|
|
40
|
+
readonly "tool.after"?: ((call: ToolInvocation, result: CallResult) => Effect.Effect<void, never, R>) | undefined;
|
|
41
|
+
readonly "extension.before"?: ((call: ExtensionInvocation) => Effect.Effect<void, unknown, R>) | undefined;
|
|
42
|
+
readonly "extension.after"?: ((call: ExtensionInvocation, result: CallResult) => Effect.Effect<void, never, R>) | undefined;
|
|
34
43
|
};
|
|
44
|
+
/** Runs `before`, then `run`, then `after` with how it ended, including when interrupted. */
|
|
45
|
+
export declare const hooked: <Call, A, R>(call: Call, before: ((call: Call) => Effect.Effect<void, unknown, R>) | undefined, after: ((call: Call, result: CallResult) => Effect.Effect<void, never, R>) | undefined, run: Effect.Effect<A, unknown, R>) => Effect.Effect<A, unknown, R>;
|
|
35
46
|
export type ToolDescription = {
|
|
36
47
|
readonly path: string;
|
|
37
48
|
readonly description: string;
|
|
@@ -47,7 +58,7 @@ type ToolNode<R> = {
|
|
|
47
58
|
namespace?: Namespace<R>;
|
|
48
59
|
readonly children: Map<string, ToolNode<R>>;
|
|
49
60
|
};
|
|
50
|
-
/** Tools indexed once per runtime
|
|
61
|
+
/** Tools indexed once per runtime, with discovery materialized on demand. */
|
|
51
62
|
export type Prepared<R = never> = {
|
|
52
63
|
readonly root: ToolNode<R>;
|
|
53
64
|
readonly catalog: ReadonlyArray<ToolDescription>;
|
|
@@ -68,10 +79,11 @@ export declare class ToolRuntimeError extends Error {
|
|
|
68
79
|
/** The tool bridge of one execution. Arguments arrive and results leave as JSON; program values never enter. */
|
|
69
80
|
export type ToolRuntime<R = never> = {
|
|
70
81
|
readonly calls: Array<ToolCall>;
|
|
82
|
+
readonly hooks: Hooks<R>;
|
|
71
83
|
readonly execute: (path: ReadonlyArray<string>, args: Array<Json | undefined>) => Effect.Effect<Json | undefined, unknown, R>;
|
|
72
84
|
readonly search: (args: Array<Json | undefined>) => Effect.Effect<Json | undefined, unknown, R>;
|
|
73
85
|
readonly keys: (path: ReadonlyArray<string>) => ReadonlyArray<string>;
|
|
74
86
|
};
|
|
75
87
|
/** Per-execution call state over tools prepared once for the runtime. */
|
|
76
|
-
export declare const make: <R>(prepared: Prepared<R>, maxToolCalls: number | undefined, hooks?:
|
|
88
|
+
export declare const make: <R>(prepared: Prepared<R>, maxToolCalls: number | undefined, hooks?: Hooks<R>) => ToolRuntime<R>;
|
|
77
89
|
export * as ToolRuntime from "./tool-runtime.js";
|
package/dist/tool-runtime.js
CHANGED
|
@@ -4,6 +4,19 @@ import { decodeInput as decodeToolInput, decodeOutput as decodeToolOutput, ident
|
|
|
4
4
|
import { isNamespace } from "./namespace.js";
|
|
5
5
|
import { isTool } from "./tool.js";
|
|
6
6
|
export const compareText = (left, right) => (left < right ? -1 : left > right ? 1 : 0);
|
|
7
|
+
/** Runs `before`, then `run`, then `after` with how it ended, including when interrupted. */
|
|
8
|
+
export const hooked = (call, before, after, run) => {
|
|
9
|
+
const observed = after === undefined
|
|
10
|
+
? run
|
|
11
|
+
: Effect.onExit(run, (exit) => {
|
|
12
|
+
if (Exit.isSuccess(exit))
|
|
13
|
+
return after(call, { status: "success", value: exit.value });
|
|
14
|
+
if (Cause.hasInterruptsOnly(exit.cause))
|
|
15
|
+
return after(call, { status: "interrupted" });
|
|
16
|
+
return after(call, { status: "failure", error: Cause.squash(exit.cause) });
|
|
17
|
+
});
|
|
18
|
+
return before === undefined ? observed : Effect.andThen(before(call), observed);
|
|
19
|
+
};
|
|
7
20
|
const defaultSearchLimit = 10;
|
|
8
21
|
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
|
|
9
22
|
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
|
|
@@ -67,13 +80,22 @@ const flattenTools = (node, path = [], namespaces = []) => {
|
|
|
67
80
|
...Array.from(node.children).flatMap(([name, child]) => flattenTools(child, [...path, name], next)),
|
|
68
81
|
];
|
|
69
82
|
};
|
|
70
|
-
const describeTool = (visible) =>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
83
|
+
const describeTool = (visible) => {
|
|
84
|
+
let signature;
|
|
85
|
+
return {
|
|
86
|
+
path: visible.path,
|
|
87
|
+
description: visible.tool.description,
|
|
88
|
+
get signature() {
|
|
89
|
+
// Search ranks paths and descriptions first; only returned matches need their schemas rendered.
|
|
90
|
+
// Joining the final fragments avoids retaining the rendering's intermediate string ropes in JSC.
|
|
91
|
+
return (signature ??= [
|
|
92
|
+
toolExpression(visible.path),
|
|
93
|
+
isEmptyInput(visible.tool) ? "()" : `(input: ${inputTypeScript(visible.tool, true)})`,
|
|
94
|
+
`: Promise<${outputTypeScript(visible.tool, true)}>`,
|
|
95
|
+
].join(""));
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
};
|
|
77
99
|
const tokenize = (query) => query
|
|
78
100
|
.replace(/([a-z0-9])([A-Z])/g, "$1 $2")
|
|
79
101
|
.toLowerCase()
|
|
@@ -152,12 +174,20 @@ const toSearchEntry = (visible) => ({
|
|
|
152
174
|
});
|
|
153
175
|
export const prepare = (tools) => {
|
|
154
176
|
const root = toolTrie(tools);
|
|
155
|
-
|
|
156
|
-
|
|
177
|
+
let searchIndex;
|
|
178
|
+
let catalog;
|
|
157
179
|
return {
|
|
158
180
|
root,
|
|
159
|
-
catalog
|
|
160
|
-
|
|
181
|
+
get catalog() {
|
|
182
|
+
return (catalog ??= this.searchIndex.map((entry) => entry.description));
|
|
183
|
+
},
|
|
184
|
+
get searchIndex() {
|
|
185
|
+
// Executing known tools only needs the trie. Render discovery when it is actually read,
|
|
186
|
+
// ordering after canonical-path collisions settle so instruction bytes stay deterministic.
|
|
187
|
+
return (searchIndex ??= flattenTools(root)
|
|
188
|
+
.sort((left, right) => compareText(left.path, right.path))
|
|
189
|
+
.map(toSearchEntry));
|
|
190
|
+
},
|
|
161
191
|
};
|
|
162
192
|
};
|
|
163
193
|
const lookup = (root, segments) => segments.reduce((node, segment) => node?.children.get(segment), root);
|
|
@@ -193,26 +223,9 @@ export class ToolRuntimeError extends Error {
|
|
|
193
223
|
}
|
|
194
224
|
}
|
|
195
225
|
/** Per-execution call state over tools prepared once for the runtime. */
|
|
196
|
-
export const make = (prepared, maxToolCalls, hooks) => {
|
|
226
|
+
export const make = (prepared, maxToolCalls, hooks = {}) => {
|
|
197
227
|
const calls = [];
|
|
198
228
|
const root = prepared.root;
|
|
199
|
-
const searchTool = makeSearchTool(prepared.searchIndex);
|
|
200
|
-
const observeEnd = (effect, call) => {
|
|
201
|
-
const onEnd = hooks?.onToolCallEnd;
|
|
202
|
-
if (onEnd === undefined)
|
|
203
|
-
return effect;
|
|
204
|
-
const startedAt = Date.now();
|
|
205
|
-
return effect.pipe(Effect.onExit((exit) => {
|
|
206
|
-
const durationMs = Date.now() - startedAt;
|
|
207
|
-
if (Exit.isSuccess(exit))
|
|
208
|
-
return onEnd({ ...call, durationMs, outcome: "success" });
|
|
209
|
-
if (Cause.hasInterruptsOnly(exit.cause))
|
|
210
|
-
return onEnd({ ...call, durationMs, outcome: "interrupted" });
|
|
211
|
-
const error = Cause.squash(exit.cause);
|
|
212
|
-
const message = error instanceof Error ? error.message : Cause.pretty(exit.cause);
|
|
213
|
-
return onEnd({ ...call, durationMs, outcome: "failure", message });
|
|
214
|
-
}));
|
|
215
|
-
};
|
|
216
229
|
const recordCall = (call) => {
|
|
217
230
|
if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
|
|
218
231
|
throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`);
|
|
@@ -227,14 +240,8 @@ export const make = (prepared, maxToolCalls, hooks) => {
|
|
|
227
240
|
try: () => decodeToolInput(tool, normalized[0]),
|
|
228
241
|
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."]),
|
|
229
242
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
return calls.length - 1;
|
|
233
|
-
});
|
|
234
|
-
const call = { index, name, input };
|
|
235
|
-
return yield* observeEnd(Effect.gen(function* () {
|
|
236
|
-
if (hooks?.onToolCallStart !== undefined)
|
|
237
|
-
yield* hooks.onToolCallStart(call);
|
|
243
|
+
yield* Effect.sync(() => recordCall({ name }));
|
|
244
|
+
return yield* hooked({ name, input }, hooks["tool.before"], hooks["tool.after"], Effect.gen(function* () {
|
|
238
245
|
const raw = yield* Effect.suspend(() => tool.execute(input)).pipe(Effect.catchCause((cause) => {
|
|
239
246
|
if (Cause.hasInterruptsOnly(cause))
|
|
240
247
|
return Effect.interrupt;
|
|
@@ -250,17 +257,18 @@ export const make = (prepared, maxToolCalls, hooks) => {
|
|
|
250
257
|
},
|
|
251
258
|
catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`),
|
|
252
259
|
});
|
|
253
|
-
})
|
|
260
|
+
}));
|
|
254
261
|
});
|
|
255
262
|
return {
|
|
256
263
|
calls,
|
|
264
|
+
hooks,
|
|
257
265
|
keys: (path) => namespaceKeys(root, path),
|
|
258
|
-
search: (args) => Effect.suspend(() => executeTool("search",
|
|
266
|
+
search: (args) => Effect.suspend(() => executeTool("search", makeSearchTool(prepared.searchIndex), args)),
|
|
259
267
|
execute: (path, args) => Effect.suspend(() => {
|
|
260
268
|
const segments = canonicalSegments(path);
|
|
261
269
|
// Models often write `tools.search(...)` for the bare `search(...)`; honor it unless a tool owns that path.
|
|
262
270
|
if (segments.length === 1 && segments[0] === "search" && lookup(root, segments) === undefined)
|
|
263
|
-
return executeTool("search",
|
|
271
|
+
return executeTool("search", makeSearchTool(prepared.searchIndex), args);
|
|
264
272
|
return executeTool(segments.join("."), resolve(root, path), args);
|
|
265
273
|
}),
|
|
266
274
|
};
|
package/package.json
CHANGED