@opencode/codemode 0.0.0-dev-19673 → 0.0.0-dev-19676
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 +1 -1
- package/dist/interpreter/execute.d.ts +1 -1
- package/dist/interpreter/extensions.js +15 -10
- package/dist/tool-runtime.d.ts +26 -14
- package/dist/tool-runtime.js +18 -26
- 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
|
@@ -72,6 +72,6 @@ export const make = (options = {}) => {
|
|
|
72
72
|
}
|
|
73
73
|
return {
|
|
74
74
|
catalog: prepared.catalog,
|
|
75
|
-
execute: (code) => executeProgram(code, prepared, limits, options, (ctx) => extensionGlobals(ctx, extensions)),
|
|
75
|
+
execute: (code) => executeProgram(code, prepared, limits, options.hooks ?? {}, (ctx) => extensionGlobals(ctx, extensions)),
|
|
76
76
|
};
|
|
77
77
|
};
|
|
@@ -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,5 +1,6 @@
|
|
|
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
6
|
import { Throw, typeError } from "./model.js";
|
|
@@ -115,23 +116,27 @@ export const extensionGlobals = (ctx, extensions) => {
|
|
|
115
116
|
}
|
|
116
117
|
throw typeError(`${label} produced ${describeHost(value)}, which the program cannot hold.`);
|
|
117
118
|
};
|
|
118
|
-
// A host function as a program function
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const
|
|
119
|
+
// A host function as a program function. Arguments cross in; a global's call runs inside the host's extension
|
|
120
|
+
// hooks, which see the host's own error on failure; then whatever came back, or was thrown, crosses out so the
|
|
121
|
+
// program catches a copy. Functions inside results are part of a value's API and skip the hooks.
|
|
122
|
+
const wrap = (value, label, describe) => fn(builtins, value.name, value.length, (_, values) => {
|
|
123
|
+
const args = values.map((item, index) => toHost(item, `Argument ${index + 1} to ${label}`));
|
|
124
|
+
const hooks = ctx.tools.hooks;
|
|
125
|
+
const settle = (run) => (describe === undefined
|
|
126
|
+
? run
|
|
127
|
+
: 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
128
|
let result;
|
|
124
129
|
try {
|
|
125
|
-
result = value.apply(undefined,
|
|
130
|
+
result = value.apply(undefined, args);
|
|
126
131
|
}
|
|
127
132
|
catch (reason) {
|
|
128
|
-
return Effect.fail(
|
|
133
|
+
return settle(Effect.fail(reason));
|
|
129
134
|
}
|
|
130
135
|
if (!(result instanceof Promise))
|
|
131
|
-
return
|
|
132
|
-
return ctx.pending.create(
|
|
136
|
+
return settle(Effect.succeed(result));
|
|
137
|
+
return ctx.pending.create(settle(Effect.tryPromise({ try: () => result, catch: (reason) => reason })));
|
|
133
138
|
});
|
|
134
|
-
return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name)]));
|
|
139
|
+
return extensions.flatMap((extension) => Object.entries(extension.globals).map(([name, value]) => [name, wrap(value, name, (args) => ({ extension: extension.name, name, args }))]));
|
|
135
140
|
};
|
|
136
141
|
const hostErrors = new Map([
|
|
137
142
|
["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;
|
|
@@ -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));
|
|
@@ -193,26 +206,10 @@ export class ToolRuntimeError extends Error {
|
|
|
193
206
|
}
|
|
194
207
|
}
|
|
195
208
|
/** Per-execution call state over tools prepared once for the runtime. */
|
|
196
|
-
export const make = (prepared, maxToolCalls, hooks) => {
|
|
209
|
+
export const make = (prepared, maxToolCalls, hooks = {}) => {
|
|
197
210
|
const calls = [];
|
|
198
211
|
const root = prepared.root;
|
|
199
212
|
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
213
|
const recordCall = (call) => {
|
|
217
214
|
if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
|
|
218
215
|
throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`);
|
|
@@ -227,14 +224,8 @@ export const make = (prepared, maxToolCalls, hooks) => {
|
|
|
227
224
|
try: () => decodeToolInput(tool, normalized[0]),
|
|
228
225
|
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
226
|
});
|
|
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);
|
|
227
|
+
yield* Effect.sync(() => recordCall({ name }));
|
|
228
|
+
return yield* hooked({ name, input }, hooks["tool.before"], hooks["tool.after"], Effect.gen(function* () {
|
|
238
229
|
const raw = yield* Effect.suspend(() => tool.execute(input)).pipe(Effect.catchCause((cause) => {
|
|
239
230
|
if (Cause.hasInterruptsOnly(cause))
|
|
240
231
|
return Effect.interrupt;
|
|
@@ -250,10 +241,11 @@ export const make = (prepared, maxToolCalls, hooks) => {
|
|
|
250
241
|
},
|
|
251
242
|
catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`),
|
|
252
243
|
});
|
|
253
|
-
})
|
|
244
|
+
}));
|
|
254
245
|
});
|
|
255
246
|
return {
|
|
256
247
|
calls,
|
|
248
|
+
hooks,
|
|
257
249
|
keys: (path) => namespaceKeys(root, path),
|
|
258
250
|
search: (args) => Effect.suspend(() => executeTool("search", searchTool, args)),
|
|
259
251
|
execute: (path, args) => Effect.suspend(() => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package.json",
|
|
3
3
|
"name": "@opencode/codemode",
|
|
4
|
-
"version": "0.0.0-dev-
|
|
4
|
+
"version": "0.0.0-dev-19676",
|
|
5
5
|
"description": "Effect-native confined code execution over schema-described tools",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"license": "MIT",
|