@opencode/codemode 2.0.3 → 2.0.5

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.
Files changed (72) hide show
  1. package/README.md +5 -2
  2. package/dist/codemode.d.ts +7 -2
  3. package/dist/codemode.js +15 -2
  4. package/dist/data.d.ts +14 -21
  5. package/dist/data.js +72 -140
  6. package/dist/extension.d.ts +11 -0
  7. package/dist/extension.js +9 -0
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.js +1 -0
  10. package/dist/interpreter/callback.d.ts +20 -0
  11. package/dist/interpreter/{runner.js → callback.js} +14 -14
  12. package/dist/interpreter/errors.d.ts +12 -5
  13. package/dist/interpreter/errors.js +66 -54
  14. package/dist/interpreter/execute.d.ts +2 -2
  15. package/dist/interpreter/execute.js +15 -15
  16. package/dist/interpreter/extensions.d.ts +8 -0
  17. package/dist/interpreter/extensions.js +160 -0
  18. package/dist/interpreter/generators.d.ts +2 -3
  19. package/dist/interpreter/generators.js +10 -10
  20. package/dist/interpreter/globals.d.ts +4 -12
  21. package/dist/interpreter/globals.js +56 -49
  22. package/dist/interpreter/interpreter.d.ts +34 -0
  23. package/dist/interpreter/{runtime.js → interpreter.js} +250 -215
  24. package/dist/interpreter/intrinsics.d.ts +5 -5
  25. package/dist/interpreter/intrinsics.js +16 -10
  26. package/dist/interpreter/limits.d.ts +10 -0
  27. package/dist/interpreter/limits.js +19 -0
  28. package/dist/interpreter/model.d.ts +27 -14
  29. package/dist/interpreter/model.js +22 -14
  30. package/dist/interpreter/native.d.ts +12 -13
  31. package/dist/interpreter/native.js +17 -16
  32. package/dist/interpreter/objects.d.ts +55 -48
  33. package/dist/interpreter/objects.js +64 -44
  34. package/dist/interpreter/promises.d.ts +13 -15
  35. package/dist/interpreter/promises.js +89 -85
  36. package/dist/interpreter/references.d.ts +1 -2
  37. package/dist/interpreter/references.js +18 -16
  38. package/dist/interpreter/scope.js +6 -6
  39. package/dist/openapi/types.d.ts +1 -1
  40. package/dist/stdlib/array.d.ts +3 -4
  41. package/dist/stdlib/array.js +115 -110
  42. package/dist/stdlib/bytes.d.ts +10 -0
  43. package/dist/stdlib/bytes.js +228 -0
  44. package/dist/stdlib/collections.d.ts +4 -4
  45. package/dist/stdlib/collections.js +89 -89
  46. package/dist/stdlib/console.d.ts +5 -3
  47. package/dist/stdlib/console.js +30 -37
  48. package/dist/stdlib/date.d.ts +2 -2
  49. package/dist/stdlib/date.js +35 -35
  50. package/dist/stdlib/json.d.ts +3 -3
  51. package/dist/stdlib/json.js +29 -67
  52. package/dist/stdlib/math.d.ts +3 -3
  53. package/dist/stdlib/math.js +17 -21
  54. package/dist/stdlib/number.d.ts +3 -3
  55. package/dist/stdlib/number.js +32 -36
  56. package/dist/stdlib/object.d.ts +5 -5
  57. package/dist/stdlib/object.js +59 -62
  58. package/dist/stdlib/regexp.d.ts +7 -8
  59. package/dist/stdlib/regexp.js +40 -48
  60. package/dist/stdlib/string.d.ts +2 -2
  61. package/dist/stdlib/string.js +110 -88
  62. package/dist/stdlib/url.d.ts +4 -6
  63. package/dist/stdlib/url.js +86 -90
  64. package/dist/stdlib/value.d.ts +3 -3
  65. package/dist/stdlib/value.js +26 -24
  66. package/dist/stdlib/web.d.ts +4 -4
  67. package/dist/stdlib/web.js +22 -8
  68. package/dist/tool-runtime.d.ts +38 -19
  69. package/dist/tool-runtime.js +70 -48
  70. package/package.json +1 -1
  71. package/dist/interpreter/runner.d.ts +0 -28
  72. package/dist/interpreter/runtime.d.ts +0 -19
@@ -1,10 +1,22 @@
1
1
  import { Cause, Effect, Exit, Formatter, Schema } from "effect";
2
- import { fromData, toData, ToolRuntimeError } from "./data.js";
3
2
  import { toolError } from "./tool-error.js";
4
3
  import { decodeInput as decodeToolInput, decodeOutput as decodeToolOutput, identifierSegment, inputProperties, inputTypeScript, isEmptyInput, outputTypeScript, } from "./tool-schema.js";
5
4
  import { isNamespace } from "./namespace.js";
6
5
  import { isTool } from "./tool.js";
7
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
+ };
8
20
  const defaultSearchLimit = 10;
9
21
  const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0));
10
22
  const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0));
@@ -68,13 +80,22 @@ const flattenTools = (node, path = [], namespaces = []) => {
68
80
  ...Array.from(node.children).flatMap(([name, child]) => flattenTools(child, [...path, name], next)),
69
81
  ];
70
82
  };
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
- });
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
+ };
78
99
  const tokenize = (query) => query
79
100
  .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
80
101
  .toLowerCase()
@@ -153,12 +174,20 @@ const toSearchEntry = (visible) => ({
153
174
  });
154
175
  export const prepare = (tools) => {
155
176
  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));
177
+ let searchIndex;
178
+ let catalog;
158
179
  return {
159
180
  root,
160
- catalog: visible.map(describeTool),
161
- searchIndex: visible.map(toSearchEntry),
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
+ },
162
191
  };
163
192
  };
164
193
  const lookup = (root, segments) => segments.reduce((node, segment) => node?.children.get(segment), root);
@@ -183,49 +212,36 @@ const resolve = (root, path) => {
183
212
  }
184
213
  return node.tool;
185
214
  };
215
+ export class ToolRuntimeError extends Error {
216
+ kind;
217
+ suggestions;
218
+ constructor(kind, message, suggestions = []) {
219
+ super(message);
220
+ this.kind = kind;
221
+ this.suggestions = suggestions;
222
+ this.name = "ToolRuntimeError";
223
+ }
224
+ }
186
225
  /** Per-execution call state over tools prepared once for the runtime. */
187
- export const make = (prepared, prototypes, maxToolCalls, hooks) => {
226
+ export const make = (prepared, maxToolCalls, hooks = {}) => {
188
227
  const calls = [];
189
228
  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
229
  const recordCall = (call) => {
208
230
  if (maxToolCalls !== undefined && calls.length >= maxToolCalls) {
209
231
  throw new ToolRuntimeError("ToolCallLimitExceeded", `Execution exceeded its tool-call limit of ${maxToolCalls}.`);
210
232
  }
211
233
  calls.push(call);
212
234
  };
213
- const executeTool = (name, tool, externalArgs) => Effect.gen(function* () {
214
- const normalized = externalArgs.length === 0 ? [{}] : externalArgs;
235
+ const executeTool = (name, tool, args) => Effect.gen(function* () {
236
+ const normalized = args.length === 0 ? [{}] : args;
215
237
  if (normalized.length !== 1)
216
238
  throw new ToolRuntimeError("InvalidToolInput", `Tool '${name}' expects at most one input object.`);
217
239
  const input = yield* Effect.try({
218
240
  try: () => decodeToolInput(tool, normalized[0]),
219
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."]),
220
242
  });
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);
243
+ yield* Effect.sync(() => recordCall({ name }));
244
+ return yield* hooked({ name, input }, hooks["tool.before"], hooks["tool.after"], Effect.gen(function* () {
229
245
  const raw = yield* Effect.suspend(() => tool.execute(input)).pipe(Effect.catchCause((cause) => {
230
246
  if (Cause.hasInterruptsOnly(cause))
231
247
  return Effect.interrupt;
@@ -233,21 +249,27 @@ export const make = (prepared, prototypes, maxToolCalls, hooks) => {
233
249
  .map((error) => (error.cause ? Formatter.format(error) : error.message || error.name))
234
250
  .join("\n")));
235
251
  }));
252
+ // The same round trip a tool result would take through text: exact JSON.stringify semantics.
236
253
  return yield* Effect.try({
237
- try: () => fromData(prototypes, decodeToolOutput(tool, raw), `Result from tool '${name}'`),
254
+ try: () => {
255
+ const text = JSON.stringify(decodeToolOutput(tool, raw));
256
+ return text === undefined ? undefined : JSON.parse(text);
257
+ },
238
258
  catch: (cause) => new ToolRuntimeError("InvalidToolOutput", `Invalid output from tool '${name}': ${cause}`),
239
259
  });
240
- }), call);
260
+ }));
241
261
  });
242
262
  return {
243
263
  calls,
264
+ hooks,
244
265
  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);
266
+ search: (args) => Effect.suspend(() => executeTool("search", makeSearchTool(prepared.searchIndex), args)),
267
+ execute: (path, args) => Effect.suspend(() => {
268
+ const segments = canonicalSegments(path);
269
+ // Models often write `tools.search(...)` for the bare `search(...)`; honor it unless a tool owns that path.
270
+ if (segments.length === 1 && segments[0] === "search" && lookup(root, segments) === undefined)
271
+ return executeTool("search", makeSearchTool(prepared.searchIndex), args);
272
+ return executeTool(segments.join("."), resolve(root, path), args);
251
273
  }),
252
274
  };
253
275
  };
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": "2.0.3",
4
+ "version": "2.0.5",
5
5
  "description": "Effect-native confined code execution over schema-described tools",
6
6
  "type": "module",
7
7
  "license": "MIT",
@@ -1,28 +0,0 @@
1
- import { Effect } from "effect";
2
- import type { Prototypes } from "./intrinsics.js";
3
- import { type AstNode } from "./model.js";
4
- import { Callable, ProgramPromise } from "./objects.js";
5
- export type IteratorCursor<R> = {
6
- readonly next: Effect.Effect<{
7
- readonly done: boolean;
8
- readonly value: unknown;
9
- }, unknown, R>;
10
- readonly close: Effect.Effect<void, unknown, R>;
11
- };
12
- /** Everything a native function needs from the realm: calling back into the program and its intrinsic objects. */
13
- export type Runner<R> = {
14
- readonly invokeCallable: (callable: unknown, thisValue: unknown, args: Array<unknown>, node: AstNode) => Effect.Effect<unknown, unknown, R>;
15
- readonly settlePromise: (promise: ProgramPromise) => Effect.Effect<unknown, unknown, never>;
16
- readonly syncIterator: (value: unknown, node: AstNode) => Effect.Effect<IteratorCursor<R> | undefined, unknown, R>;
17
- readonly prototypes: Prototypes;
18
- };
19
- export declare const preserveConsumerError: <A, R>(cursor: IteratorCursor<R>, effect: Effect.Effect<A, unknown, R>) => Effect.Effect<A, unknown, R>;
20
- /**
21
- * ToPrimitive: calls `valueOf`/`toString` in hint order and returns the first primitive result. Dates treat the
22
- * default hint as "string", like their `Symbol.toPrimitive`.
23
- */
24
- export declare const toPrimitive: <R>(runner: Runner<R>, value: unknown, hint: "number" | "string" | "default", node: AstNode) => Effect.Effect<unknown, unknown, R>;
25
- export declare const toPrimitiveString: <R>(runner: Runner<R>, value: unknown, node: AstNode) => Effect.Effect<string, unknown, R>;
26
- export declare const toPrimitiveNumber: <R>(runner: Runner<R>, value: unknown, node: AstNode) => Effect.Effect<number, unknown, R>;
27
- export declare const isSupportedCallback: (value: unknown) => value is Callable;
28
- export declare const applyCollectionCallback: <R>(runner: Runner<R>, callback: unknown, name: string, node: AstNode) => ((args: Array<unknown>) => Effect.Effect<unknown, unknown, R>);
@@ -1,19 +0,0 @@
1
- import type { Program } from "acorn";
2
- import { Effect } from "effect";
3
- import type { Prototypes } from "./intrinsics.js";
4
- import { type Host } from "./globals.js";
5
- import { type Runner } from "./runner.js";
6
- import { PromiseRuntime } from "./promises.js";
7
- /** One program execution: the tool bridge, promise scheduler, captured logs, and the global scope built once. */
8
- export declare class Runtime<R> {
9
- readonly executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>;
10
- readonly search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>;
11
- readonly toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>;
12
- readonly promises: PromiseRuntime<R>;
13
- readonly prototypes: Prototypes;
14
- readonly logs: Array<string>;
15
- readonly runner: Runner<R>;
16
- private readonly root;
17
- constructor(executeTool: (path: ReadonlyArray<string>, args: Array<unknown>) => Effect.Effect<unknown, unknown, R>, search: (args: Array<unknown>) => Effect.Effect<unknown, unknown, R>, toolKeys: (path: ReadonlyArray<string>) => ReadonlyArray<string>, promises: PromiseRuntime<R>, prototypes: Prototypes, logs?: Array<string>, extraGlobals?: (host: Host<R>) => ReadonlyArray<readonly [string, unknown]>);
18
- run(program: Program): Effect.Effect<unknown, unknown, R>;
19
- }