refactor(codemode): simplify tools types (#36941)

This commit is contained in:
Aiden Cline
2026-07-14 18:40:43 -05:00
committed by GitHub
parent fca3bca19d
commit 563f6a65de
8 changed files with 77 additions and 93 deletions
+5 -5
View File
@@ -15,7 +15,7 @@ runs, no sandbox required.
The deliberate differences: The deliberate differences:
- **No ambient authority.** No `fetch`, `process`, filesystem, timers, or host globals - only the allowlisted standard - **No ambient authority.** No `fetch`, `process`, filesystem, timers, or host globals - only the allowlisted standard
library and the `tools` tree. library and supplied `tools`.
- **No dynamic code.** No `eval`, `Function`, or module loading. - **No dynamic code.** No `eval`, `Function`, or module loading.
- **Plain-data boundaries.** Tool arguments and program results are JSON-like data. Dates become ISO strings, RegExp, - **Plain-data boundaries.** Tool arguments and program results are JSON-like data. Dates become ISO strings, RegExp,
Map, and Set serialize as `{}`, and promises, functions, and runtime references cannot cross the boundary. Map, and Set serialize as `{}`, and promises, functions, and runtime references cannot cross the boundary.
@@ -33,8 +33,8 @@ generators, and full sparse-array parity) are tracked as unchecked items in the
## Quick Start ## Quick Start
The package is workspace-private (`"@opencode-ai/codemode": "workspace:*"`). Hosts interact with it through `effect` The package is workspace-private (`"@opencode-ai/codemode": "workspace:*"`). Hosts interact with it through `effect`
and should depend on `effect` themselves. Define tools with Effect Schema, then place them in the object tree exposed and should depend on `effect` themselves. Define tools with Effect Schema, then expose them to programs through
to programs as `tools`: `tools`:
```ts ```ts
import { CodeMode, Tool } from "@opencode-ai/codemode" import { CodeMode, Tool } from "@opencode-ai/codemode"
@@ -94,8 +94,8 @@ Effect-returning and must not fail.
### OpenAPI tools ### OpenAPI tools
`OpenAPI.fromSpec` turns an OpenAPI 3.x document into a tool subtree - one tool per operation, namespaced by dotted `OpenAPI.fromSpec` turns an OpenAPI 3.x document into namespaced tools - one tool per operation, using dotted
`operationId`: `operationId` segments as namespaces:
```ts ```ts
const api = OpenAPI.fromSpec({ spec, auth: { resolve } }) const api = OpenAPI.fromSpec({ spec, auth: { resolve } })
+18 -22
View File
@@ -1,7 +1,7 @@
import { Effect, Schema } from "effect" import { Effect, Schema } from "effect"
import { executeWithLimits } from "./interpreter/execute.js" import { executeWithLimits } from "./interpreter/execute.js"
import { type HostTools, type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js" import { type Services, type ToolDescription, ToolRuntime } from "./tool-runtime.js"
import type { Definition } from "./tool.js" import type { Tools } from "./tools.js"
/** A tool call admitted during an execution. */ /** A tool call admitted during an execution. */
export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js" export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
@@ -28,10 +28,6 @@ export type DiscoveryOptions = {
readonly catalogBudget?: number readonly catalogBudget?: number
} }
type ToolTree<R = never> = {
readonly [name: string]: Definition<R> | ToolTree<R>
}
export type ResolvedExecutionLimits = { export type ResolvedExecutionLimits = {
readonly timeoutMs: number | undefined readonly timeoutMs: number | undefined
readonly maxToolCalls: number | undefined readonly maxToolCalls: number | undefined
@@ -39,24 +35,24 @@ export type ResolvedExecutionLimits = {
} }
/** Options for one CodeMode execution. */ /** Options for one CodeMode execution. */
export type ExecuteOptions<Tools extends Record<string, unknown> = {}> = { export type ExecuteOptions<Provided extends Record<string, unknown> = {}> = {
/** Source for one program in the supported JavaScript subset. */ /** Source for one program in the supported JavaScript subset. */
code: string code: string
/** Explicit tool tree exposed to the program as `tools`. */ /** Explicit tools exposed to the program as `tools`. */
tools?: Tools & ToolTree<Services<Tools>> tools?: Provided & Tools<Services<Provided>>
/** Per-execution overrides for the default resource limits. */ /** Per-execution overrides for the default resource limits. */
limits?: ExecutionLimits limits?: ExecutionLimits
/** Observes decoded tool input immediately before tool execution. */ /** Observes decoded tool input immediately before tool execution. */
onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect<void, never, Services<Tools>> onToolCallStart?: (call: ToolRuntime.ToolCallStarted) => Effect.Effect<void, never, Services<Provided>>
/** Observes each admitted tool call as it settles, with outcome and duration. */ /** Observes each admitted tool call as it settles, with outcome and duration. */
onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Tools>> onToolCallEnd?: (call: ToolRuntime.ToolCallEnded) => Effect.Effect<void, never, Services<Provided>>
} }
/** A JSON value that can cross the confined interpreter boundary. */ /** A JSON value that can cross the confined interpreter boundary. */
export type DataValue = Schema.Json export type DataValue = Schema.Json
/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */ /** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
export type Options<Tools extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Tools>, "code"> & { export type Options<Provided extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Provided>, "code"> & {
/** Progressive-disclosure configuration for the agent-facing tool catalog. */ /** Progressive-disclosure configuration for the agent-facing tool catalog. */
readonly discovery?: DiscoveryOptions readonly discovery?: DiscoveryOptions
} }
@@ -117,7 +113,7 @@ export const Result = Schema.Union([Success, Failure])
/** Result of executing a CodeMode program. Program failures are data, not Effect failures. */ /** Result of executing a CodeMode program. Program failures are data, not Effect failures. */
export type Result = typeof Result.Type export type Result = typeof Result.Type
/** Reusable confined runtime over one explicit tool tree. */ /** Reusable confined runtime over explicit tools. */
export type Runtime<R = never> = { export type Runtime<R = never> = {
readonly catalog: () => ReadonlyArray<ToolDescription> readonly catalog: () => ReadonlyArray<ToolDescription>
readonly instructions: () => string readonly instructions: () => string
@@ -138,24 +134,24 @@ const resolveExecutionLimits = (limits?: ExecutionLimits): ResolvedExecutionLimi
}) })
/** Executes one Effect-native CodeMode program without constructing a reusable runtime. */ /** Executes one Effect-native CodeMode program without constructing a reusable runtime. */
export const execute = <const Tools extends Record<string, unknown>>( export const execute = <const Provided extends Record<string, unknown>>(
options: ExecuteOptions<Tools>, options: ExecuteOptions<Provided>,
): Effect.Effect<Result, never, Services<Tools>> => { ): Effect.Effect<Result, never, Services<Provided>> => {
const tools = (options.tools ?? {}) as HostTools<Services<Tools>> const tools = (options.tools ?? {}) as Tools<Services<Provided>>
return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools)) return executeWithLimits(options, resolveExecutionLimits(options.limits), ToolRuntime.searchIndex(tools))
} }
/** Creates an Effect-native runtime over explicit, schema-described tools. */ /** Creates an Effect-native runtime over explicit, schema-described tools. */
export const make = <const Tools extends Record<string, unknown> = {}>( export const make = <const Provided extends Record<string, unknown> = {}>(
options: Options<Tools> = {} as Options<Tools>, options: Options<Provided> = {} as Options<Provided>,
): Runtime<Services<Tools>> => { ): Runtime<Services<Provided>> => {
const tools = (options.tools ?? {}) as HostTools<Services<Tools>> const tools = (options.tools ?? {}) as Tools<Services<Provided>>
const limits = resolveExecutionLimits(options.limits) const limits = resolveExecutionLimits(options.limits)
const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget) const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget)
return { return {
catalog: () => prepared.catalog, catalog: () => prepared.catalog,
instructions: () => prepared.instructions, instructions: () => prepared.instructions,
execute: (code) => executeWithLimits<Tools>({ ...options, code }, limits, prepared.searchIndex), execute: (code) => executeWithLimits<Provided>({ ...options, code }, limits, prepared.searchIndex),
} }
} }
+15 -8
View File
@@ -2,17 +2,18 @@ import { parse } from "acorn"
import { Cause, Effect, Scope } from "effect" import { Cause, Effect, Scope } from "effect"
import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript" import { DiagnosticCategory, ModuleKind, ScriptTarget, flattenDiagnosticMessageText, transpileModule } from "typescript"
import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js" import type { DataValue, Diagnostic, ExecuteOptions, ResolvedExecutionLimits, Result } from "../codemode.js"
import { copyIn, copyOut, ToolRuntime, type HostTools, type Services } from "../tool-runtime.js" import { copyIn, copyOut, ToolRuntime, type Services } from "../tool-runtime.js"
import type { Tools } from "../tools.js"
import { normalizeError } from "./errors.js" import { normalizeError } from "./errors.js"
import { InterpreterRuntimeError, isRecord, type ProgramNode } from "./model.js" import { InterpreterRuntimeError, isRecord, type ProgramNode } from "./model.js"
import { PromiseRuntime } from "./promises.js" import { PromiseRuntime } from "./promises.js"
import { Interpreter } from "./runtime.js" import { Interpreter } from "./runtime.js"
export const executeWithLimits = <const Tools extends Record<string, unknown>>( export const executeWithLimits = <const Provided extends Record<string, unknown>>(
options: ExecuteOptions<Tools>, options: ExecuteOptions<Provided>,
limits: ResolvedExecutionLimits, limits: ResolvedExecutionLimits,
searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"], searchIndex: ToolRuntime.DiscoveryPlan["searchIndex"],
): Effect.Effect<Result, never, Services<Tools>> => { ): Effect.Effect<Result, never, Services<Provided>> => {
if (options.code.trim().length === 0) { if (options.code.trim().length === 0) {
return Effect.succeed({ return Effect.succeed({
ok: false, ok: false,
@@ -24,7 +25,7 @@ export const executeWithLimits = <const Tools extends Record<string, unknown>>(
// Allocate execution state inside suspension so reused Effects never share it. // Allocate execution state inside suspension so reused Effects never share it.
return Effect.suspend(() => { return Effect.suspend(() => {
const tools = ToolRuntime.make( const tools = ToolRuntime.make(
(options.tools ?? {}) as HostTools<Services<Tools>>, (options.tools ?? {}) as Tools<Services<Provided>>,
limits.maxToolCalls, limits.maxToolCalls,
searchIndex, searchIndex,
{ {
@@ -35,15 +36,21 @@ export const executeWithLimits = <const Tools extends Record<string, unknown>>(
const logs: Array<string> = [] const logs: Array<string> = []
const logged = () => (logs.length > 0 ? { logs: [...logs] } : {}) const logged = () => (logs.length > 0 ? { logs: [...logs] } : {})
// Set only after copy-out so timeouts cannot report invalid values as completed. // Set only after copy-out so timeouts cannot report invalid values as completed.
let returned: { value: DataValue; promises: PromiseRuntime<Services<Tools>> } | undefined let returned: { value: DataValue; promises: PromiseRuntime<Services<Provided>> } | undefined
const base = Effect.acquireUseRelease( const base = Effect.acquireUseRelease(
Scope.make("parallel"), Scope.make("parallel"),
(scope) => (scope) =>
Effect.gen(function* () { Effect.gen(function* () {
const program = parseProgram(options.code) const program = parseProgram(options.code)
const promises = new PromiseRuntime<Services<Tools>>(scope) const promises = new PromiseRuntime<Services<Provided>>(scope)
const interpreter = new Interpreter<Services<Tools>>(tools.invoke, tools.search, tools.keys, promises, logs) const interpreter = new Interpreter<Services<Provided>>(
tools.invoke,
tools.search,
tools.keys,
promises,
logs,
)
const value = yield* interpreter.run(program) const value = yield* interpreter.run(program)
const result = copyOut(copyIn(value, "Execution result"), true) as DataValue const result = copyOut(copyIn(value, "Execution result"), true) as DataValue
returned = { value: result, promises } returned = { value: result, promises }
+1 -1
View File
@@ -61,7 +61,7 @@ export type Skipped = {
export type Tools = { [name: string]: Definition<HttpClient.HttpClient> | Tools } export type Tools = { [name: string]: Definition<HttpClient.HttpClient> | Tools }
export type Result = { export type Result = {
/** Tool subtree; the host places it under a key in its `tools` tree. */ /** Namespaced tools; the host places them under a key in its `tools` object. */
readonly tools: Tools readonly tools: Tools
readonly skipped: ReadonlyArray<Skipped> readonly skipped: ReadonlyArray<Skipped>
} }
+30 -54
View File
@@ -9,6 +9,7 @@ import {
outputTypeScript, outputTypeScript,
} from "./tool-schema.js" } from "./tool-schema.js"
import { isDefinition as isToolDefinition, type Definition } from "./tool.js" import { isDefinition as isToolDefinition, type Definition } from "./tool.js"
import type { Tools } from "./tools.js"
import { import {
CodeModeDate, CodeModeDate,
CodeModeMap, CodeModeMap,
@@ -21,28 +22,20 @@ import {
const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4)) const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4))
export type HostTool<R = never> = (...args: Array<unknown>) => Effect.Effect<unknown, unknown, R> export type Services<T> = ServicesOf<T, []>
export type HostTools<R = never> = { type ServicesOf<T, Depth extends ReadonlyArray<unknown>> = Depth["length"] extends 8
[name: string]: HostTool<R> | Definition<R> | HostTools<R>
}
export type Services<Tools> = ServicesOf<Tools, []>
type ServicesOf<Tools, Depth extends ReadonlyArray<unknown>> = Depth["length"] extends 8
? never ? never
: Tools extends (...args: Array<unknown>) => Effect.Effect<unknown, unknown, infer R> : T extends {
readonly _tag: "CodeModeTool"
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R>
}
? R ? R
: Tools extends { : T extends object
readonly _tag: "CodeModeTool" ? string extends keyof T
readonly run: (input: unknown) => Effect.Effect<unknown, unknown, infer R> ? ServicesOf<T[string], [...Depth, unknown]>
} : ServicesOf<T[keyof T], [...Depth, unknown]>
? R : never
: Tools extends object
? string extends keyof Tools
? ServicesOf<Tools[string], [...Depth, unknown]>
: ServicesOf<Tools[keyof Tools], [...Depth, unknown]>
: never
export type ToolCall = { export type ToolCall = {
readonly name: string readonly name: string
@@ -125,7 +118,7 @@ export class ToolRuntimeError extends Error {
} }
} }
const isDefinition = <R>(value: HostTool<R> | Definition<R> | HostTools<R>): value is Definition<R> => const isDefinition = <R>(value: Definition<R> | Tools<R>): value is Definition<R> =>
isToolDefinition<R>(value) isToolDefinition<R>(value)
const runHost = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, ToolError, R> => const runHost = <A, E, R>(effect: Effect.Effect<A, E, R>): Effect.Effect<A, ToolError, R> =>
@@ -282,13 +275,13 @@ export const copyOut = (value: unknown, undefinedAsNull = false): unknown => {
} }
const definitions = <R>( const definitions = <R>(
tools: HostTools<R>, tools: Tools<R>,
path: ReadonlyArray<string> = [], path: ReadonlyArray<string> = [],
): Array<{ path: string; definition: Definition<R> }> => ): Array<{ path: string; definition: Definition<R> }> =>
Object.entries(tools).flatMap(([name, value]) => { Object.entries(tools).flatMap(([name, value]) => {
const next = [...path, name] const next = [...path, name]
if (isDefinition(value)) return [{ path: next.join("."), definition: value }] if (isDefinition(value)) return [{ path: next.join("."), definition: value }]
return typeof value === "function" ? [] : definitions(value, next) return definitions(value, next)
}) })
const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDescription => ({ const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDescription => ({
@@ -297,7 +290,7 @@ const describeDefinition = <R>(path: string, definition: Definition<R>): ToolDes
signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`, signature: `${toolExpression(path)}(input: ${inputTypeScript(definition, true)}): Promise<${outputTypeScript(definition, true)}>`,
}) })
const visibleDefinitions = <R>(tools: HostTools<R>) => const visibleDefinitions = <R>(tools: Tools<R>) =>
definitions(tools).map(({ path, definition }) => ({ definitions(tools).map(({ path, definition }) => ({
path, path,
definition, definition,
@@ -415,11 +408,11 @@ const toSearchEntry = <R>(path: string, definition: Definition<R>, description:
.toLowerCase(), .toLowerCase(),
}) })
export const searchIndex = <R>(tools: HostTools<R>): ReadonlyArray<SearchEntry> => export const searchIndex = <R>(tools: Tools<R>): ReadonlyArray<SearchEntry> =>
visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description)) visibleDefinitions(tools).map(({ path, definition, description }) => toSearchEntry(path, definition, description))
// Budget signatures round-robin so every namespace remains visible. // Budget signatures round-robin so every namespace remains visible.
export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => { export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => {
if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) { if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) {
throw new RangeError("discovery.catalogBudget must be a non-negative safe integer") throw new RangeError("discovery.catalogBudget must be a non-negative safe integer")
} }
@@ -562,43 +555,33 @@ export const prepare = <R>(tools: HostTools<R>, catalogBudget = defaultCatalogBu
} }
} }
const namespaceKeys = <R>(tools: HostTools<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => { const namespaceKeys = <R>(tools: Tools<R>, path: ReadonlyArray<string>): ReadonlyArray<string> => {
let value: HostTool<R> | Definition<R> | HostTools<R> = tools let value: Definition<R> | Tools<R> = tools
for (const segment of path) { for (const segment of path) {
if ( if (isBlockedMember(segment) || isDefinition(value) || !Object.hasOwn(value, segment)) {
isBlockedMember(segment) ||
typeof value === "function" ||
isDefinition(value) ||
!Object.hasOwn(value, segment)
) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [ throw new ToolRuntimeError("UnknownTool", `Unknown tool namespace '${path.join(".")}'.`, [
"Object.keys(tools) lists the available namespaces; search({ query }) finds described tools.", "Object.keys(tools) lists the available namespaces; search({ query }) finds described tools.",
]) ])
} }
value = value[segment] as HostTool<R> | Definition<R> | HostTools<R> value = value[segment] as Definition<R> | Tools<R>
} }
if (typeof value === "function" || isDefinition(value)) return [] if (isDefinition(value)) return []
return Object.keys(value) return Object.keys(value)
} }
const resolve = <R>(tools: HostTools<R>, path: ReadonlyArray<string>): HostTool<R> | Definition<R> => { const resolve = <R>(tools: Tools<R>, path: ReadonlyArray<string>): Definition<R> => {
let value: HostTool<R> | Definition<R> | HostTools<R> = tools let value: Definition<R> | Tools<R> = tools
for (const segment of path) { for (const segment of path) {
if ( if (isBlockedMember(segment) || isDefinition(value) || !Object.hasOwn(value, segment)) {
isBlockedMember(segment) ||
typeof value === "function" ||
isDefinition(value) ||
!Object.hasOwn(value, segment)
) {
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [ throw new ToolRuntimeError("UnknownTool", `Unknown tool '${path.join(".")}'.`, [
"Use search({ query }) to find available described tools.", "Use search({ query }) to find available described tools.",
]) ])
} }
value = value[segment] as HostTool<R> | Definition<R> | HostTools<R> value = value[segment] as Definition<R> | Tools<R>
} }
if (typeof value !== "function" && !isDefinition(value)) { if (!isDefinition(value)) {
throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`) throw new ToolRuntimeError("UnknownTool", `Tool '${path.join(".")}' is not callable.`)
} }
@@ -614,7 +597,7 @@ export type ToolRuntime<R = never> = {
} }
export const make = <R>( export const make = <R>(
tools: HostTools<R>, tools: Tools<R>,
maxToolCalls: number | undefined, maxToolCalls: number | undefined,
searchIndex: ReadonlyArray<SearchEntry>, searchIndex: ReadonlyArray<SearchEntry>,
hooks?: ToolCallHooks<R>, hooks?: ToolCallHooks<R>,
@@ -701,14 +684,7 @@ export const make = <R>(
const name = path.join(".") const name = path.join(".")
const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`))) const externalArgs = args.map((arg) => copyOut(copyIn(arg, `Arguments for tool '${name}'`)))
const tool = resolve(tools, path) const tool = resolve(tools, path)
if (isDefinition(tool)) return yield* invokeDefinition(name, tool, externalArgs) return yield* invokeDefinition(name, tool, externalArgs)
const index = yield* recordAndObserve(name, externalArgs)
return yield* observeEnd(
Effect.gen(function* () {
return yield* decodeOutput(yield* runHost(Effect.suspend(() => tool(...externalArgs))), name)
}),
{ index, name, input: externalArgs },
)
}), }),
} }
} }
+1 -1
View File
@@ -29,7 +29,7 @@ export type JsonSchema = {
/** Either a validating Effect Schema or a render-only JSON Schema document. */ /** Either a validating Effect Schema or a render-only JSON Schema document. */
export type SchemaType = Schema.Decoder<unknown> | JsonSchema export type SchemaType = Schema.Decoder<unknown> | JsonSchema
/** Schema-backed tool definition consumed by a CodeMode tool tree. */ /** Schema-backed tool definition exposed through CodeMode's `tools` object. */
export type Definition<R = never> = { export type Definition<R = never> = {
readonly _tag: "CodeModeTool" readonly _tag: "CodeModeTool"
readonly description: string readonly description: string
+5
View File
@@ -0,0 +1,5 @@
import type { Definition } from "./tool.js"
export type Tools<R = never> = {
readonly [name: string]: Definition<R> | Tools<R>
}
+2 -2
View File
@@ -3,7 +3,7 @@ import { Effect, Schema } from "effect"
import { CodeMode, Tool } from "../src/index.js" import { CodeMode, Tool } from "../src/index.js"
// Key enumeration: Object.keys and for...in share one surface over plain objects, arrays // Key enumeration: Object.keys and for...in share one surface over plain objects, arrays
// (index strings), and tool references (namespace/tool names from the host tool tree), so a // (index strings), and tool references (namespace/tool names from the supplied tools), so a
// model can discover what it may call instead of guessing names from the instructions. The // model can discover what it may call instead of guessing names from the instructions. The
// motivating transcript: `Object.keys(tools)` failed with the generic plain-objects-only // motivating transcript: `Object.keys(tools)` failed with the generic plain-objects-only
// message and `for (const key in tools)` was unsupported syntax, forcing blind guesses. // message and `for (const key in tools)` was unsupported syntax, forcing blind guesses.
@@ -137,7 +137,7 @@ describe("for...in", () => {
).toBe("only") ).toBe("only")
}) })
test("enumerates namespaces and tools from the callable tool tree", async () => { test("enumerates namespaces and tools from the supplied tools", async () => {
expect( expect(
await value(` await value(`
const names = [] const names = []