feat(core): render CodeMode catalog deltas from structured snapshots (#38183)
This commit is contained in:
parent
c64d813347
commit
4605308be2
19 changed files with 565 additions and 431 deletions
|
|
@ -72,7 +72,6 @@ Dots in tool names create namespaces: `{ "issues.list": tool }` and `{ issues: {
|
|||
const runtime = CodeMode.make({ tools, limits: { timeoutMs: 30_000 } })
|
||||
|
||||
runtime.catalog() // structured tool descriptions
|
||||
runtime.instructions() // model-facing syntax and tool guide
|
||||
runtime.execute(source) // Effect<CodeMode.Result, never, ToolServices>
|
||||
```
|
||||
|
||||
|
|
@ -145,13 +144,13 @@ safe refusal to the model; its optional cause remains private.
|
|||
|
||||
## Discovery
|
||||
|
||||
Generated instructions contain a tool catalog with a default budget of 2,000 estimated tokens. Configure it with
|
||||
`discovery: { catalogBudget }`. Every namespace remains visible, and the instructions say whether the catalog is
|
||||
complete or partial.
|
||||
`runtime.catalog()` returns structured descriptors — exact path, description, and generated TypeScript signature — for
|
||||
every visible tool. Hosts render their own model-facing instructions from these descriptors; `CodeMode.searchSignature`
|
||||
and `CodeMode.toolExpression(path)` supply the exact callable forms.
|
||||
|
||||
The synchronous `search(...)` built-in is always available and advertised when the catalog is partial. It supports
|
||||
exact-path lookup, namespace-scoped search, empty-query browsing, and pagination, and returns callable paths with full
|
||||
signatures. Search counts toward `maxToolCalls`.
|
||||
The synchronous `search(...)` built-in is always available. It supports exact-path lookup, namespace-scoped search,
|
||||
empty-query browsing, and pagination, and returns callable paths with full signatures. Search counts toward
|
||||
`maxToolCalls`.
|
||||
|
||||
## Execution Limits
|
||||
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import type { Tools } from "./tools.js"
|
|||
|
||||
/** A tool call admitted during an execution. */
|
||||
export type { ToolCall, ToolCallEnded, ToolCallHooks, ToolCallStarted, ToolDescription } from "./tool-runtime.js"
|
||||
/** Signature-construction helpers for host-owned catalog instructions. */
|
||||
export { searchSignature, toolExpression } from "./tool-runtime.js"
|
||||
|
||||
/** Resource budgets enforced independently during each CodeMode program execution. */
|
||||
export type ExecutionLimits = {
|
||||
|
|
@ -22,12 +24,6 @@ export type ExecutionLimits = {
|
|||
readonly maxOutputBytes?: number
|
||||
}
|
||||
|
||||
/** Controls how much of the tool catalog is inlined in agent instructions. */
|
||||
export type DiscoveryOptions = {
|
||||
/** Approximate token budget (chars/4, default 2000) for full catalog entries. */
|
||||
readonly catalogBudget?: number
|
||||
}
|
||||
|
||||
export type ResolvedExecutionLimits = {
|
||||
readonly timeoutMs: number | undefined
|
||||
readonly maxToolCalls: number | undefined
|
||||
|
|
@ -52,10 +48,7 @@ export type ExecuteOptions<Provided extends Record<string, unknown> = {}> = {
|
|||
export type DataValue = Schema.Json
|
||||
|
||||
/** Configuration shared by `CodeMode.make` and `CodeMode.execute`. */
|
||||
export type Options<Provided extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Provided>, "code"> & {
|
||||
/** Progressive-disclosure configuration for the agent-facing tool catalog. */
|
||||
readonly discovery?: DiscoveryOptions
|
||||
}
|
||||
export type Options<Provided extends Record<string, unknown> = {}> = Omit<ExecuteOptions<Provided>, "code">
|
||||
|
||||
/** Schema for a host tool input containing CodeMode source. */
|
||||
export const Input = Schema.Struct({ code: Schema.String })
|
||||
|
|
@ -116,7 +109,6 @@ export type Result = typeof Result.Type
|
|||
/** Reusable confined runtime over explicit tools. */
|
||||
export type Runtime<R = never> = {
|
||||
readonly catalog: () => ReadonlyArray<ToolDescription>
|
||||
readonly instructions: () => string
|
||||
readonly execute: (code: string) => Effect.Effect<Result, never, R>
|
||||
}
|
||||
|
||||
|
|
@ -147,11 +139,10 @@ export const make = <const Provided extends Record<string, unknown> = {}>(
|
|||
): Runtime<Services<Provided>> => {
|
||||
const tools = (options.tools ?? {}) as Tools<Services<Provided>>
|
||||
const limits = resolveExecutionLimits(options.limits)
|
||||
const prepared = ToolRuntime.prepare(tools, options.discovery?.catalogBudget)
|
||||
const prepared = ToolRuntime.prepare(tools)
|
||||
|
||||
return {
|
||||
catalog: () => prepared.catalog,
|
||||
instructions: () => prepared.instructions,
|
||||
execute: (code) => executeWithLimits<Provided>({ ...options, code }, limits, prepared.searchIndex),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * as CodeMode from "./codemode.js"
|
||||
export * as Tool from "./tool.js"
|
||||
export * as OpenAPI from "./openapi/index.js"
|
||||
export { searchSignature, toolExpression } from "./codemode.js"
|
||||
export { ToolError, toolError } from "./tool-error.js"
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import {
|
|||
CodeModeURLSearchParams,
|
||||
} from "./values.js"
|
||||
|
||||
const estimateTokens = (input: string) => Math.max(0, Math.round(input.length / 4))
|
||||
const compareText = (left: string, right: string) => (left < right ? -1 : left > right ? 1 : 0)
|
||||
|
||||
export type Services<T> = ServicesOf<T, []>
|
||||
|
|
@ -70,7 +69,6 @@ export type ToolDescription = {
|
|||
|
||||
export type SafeObject = Record<string, unknown>
|
||||
|
||||
const defaultCatalogBudget = 2_000
|
||||
const defaultSearchLimit = 10
|
||||
const PositiveInt = Schema.Int.check(Schema.isGreaterThan(0))
|
||||
const NonNegativeInt = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0))
|
||||
|
|
@ -90,7 +88,7 @@ const SearchOutput = Schema.Struct({
|
|||
remaining: NonNegativeInt,
|
||||
next: Schema.NullOr(Schema.Struct({ offset: NonNegativeInt })),
|
||||
})
|
||||
const toolExpression = (path: string) =>
|
||||
export const toolExpression = (path: string) =>
|
||||
"tools" +
|
||||
path
|
||||
.split(".")
|
||||
|
|
@ -339,7 +337,6 @@ const visibleTools = <R>(tools: Tools<R>) =>
|
|||
|
||||
export type DiscoveryPlan = {
|
||||
readonly catalog: ReadonlyArray<ToolDescription>
|
||||
readonly instructions: string
|
||||
readonly searchIndex: ReadonlyArray<SearchEntry>
|
||||
}
|
||||
|
||||
|
|
@ -423,17 +420,12 @@ const makeSearchTool = (searchIndex: ReadonlyArray<SearchEntry>): Tool => ({
|
|||
}),
|
||||
})
|
||||
|
||||
const searchSignature = (() => {
|
||||
/** Exact callable signature of the built-in `search` function, for host-owned instructions. */
|
||||
export const searchSignature = (() => {
|
||||
const tool = makeSearchTool([])
|
||||
return `search(input: ${inputTypeScript(tool, true)}): ${outputTypeScript(tool, true)}`
|
||||
})()
|
||||
|
||||
const catalogLine = (tool: ToolDescription) => {
|
||||
const line = tool.description.split("\n", 1)[0]!.trim()
|
||||
const description = line.length > 120 ? line.slice(0, 119) + "..." : line
|
||||
return description === "" ? ` - ${tool.signature}` : ` - ${tool.signature} // ${description}`
|
||||
}
|
||||
|
||||
const toSearchEntry = <R>(path: string, tool: Tool<R>, description: ToolDescription): SearchEntry => ({
|
||||
description,
|
||||
namespace: path.split(".", 1)[0]!,
|
||||
|
|
@ -451,146 +443,10 @@ const toSearchEntry = <R>(path: string, tool: Tool<R>, description: ToolDescript
|
|||
export const searchIndex = <R>(tools: Tools<R>): ReadonlyArray<SearchEntry> =>
|
||||
visibleTools(tools).map(({ path, tool, description }) => toSearchEntry(path, tool, description))
|
||||
|
||||
// Budget signatures round-robin so every namespace remains visible.
|
||||
export const prepare = <R>(tools: Tools<R>, catalogBudget = defaultCatalogBudget): DiscoveryPlan => {
|
||||
if (!Number.isSafeInteger(catalogBudget) || catalogBudget < 0) {
|
||||
throw new RangeError("discovery.catalogBudget must be a non-negative safe integer")
|
||||
}
|
||||
export const prepare = <R>(tools: Tools<R>): DiscoveryPlan => {
|
||||
const visible = visibleTools(tools)
|
||||
const described = visible.map(({ description }) => description)
|
||||
|
||||
const namespaces = new Map<string, Array<ToolDescription>>()
|
||||
for (const tool of described) {
|
||||
const [namespace = tool.path] = tool.path.split(".")
|
||||
const group = namespaces.get(namespace) ?? []
|
||||
group.push(tool)
|
||||
namespaces.set(namespace, group)
|
||||
}
|
||||
const ordered = [...namespaces].sort(([left], [right]) => compareText(left, right))
|
||||
|
||||
const selections = ordered.map(([namespace, group]) => ({
|
||||
namespace,
|
||||
picked: new Set<ToolDescription>(),
|
||||
queue: [...group].sort(
|
||||
(left, right) =>
|
||||
estimateTokens(catalogLine(left)) - estimateTokens(catalogLine(right)) || compareText(left.path, right.path),
|
||||
),
|
||||
}))
|
||||
let used = 0
|
||||
let active = selections.filter((selection) => selection.queue.length > 0)
|
||||
while (active.length > 0) {
|
||||
const stillActive: typeof active = []
|
||||
for (const selection of active) {
|
||||
const tool = selection.queue[0]!
|
||||
const cost = estimateTokens(catalogLine(tool))
|
||||
if (used + cost > catalogBudget) continue
|
||||
selection.queue.shift()
|
||||
selection.picked.add(tool)
|
||||
used += cost
|
||||
if (selection.queue.length > 0) stillActive.push(selection)
|
||||
}
|
||||
active = stillActive
|
||||
}
|
||||
const shown = new Map<string, ReadonlySet<ToolDescription>>(
|
||||
selections.map(({ namespace, picked }) => [namespace, picked]),
|
||||
)
|
||||
const totalShown = selections.reduce((total, { picked }) => total + picked.size, 0)
|
||||
const complete = totalShown === described.length
|
||||
|
||||
const empty = described.length === 0
|
||||
|
||||
const intro = [
|
||||
empty
|
||||
? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime."
|
||||
: complete
|
||||
? "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the tools listed below; surrounding agent tools are not available."
|
||||
: "This is a restricted JavaScript language for calling tools, not a general-purpose runtime. Inside the confined interpreter, `tools` contains the tools listed or searchable below; surrounding agent tools are not available.",
|
||||
...(empty
|
||||
? []
|
||||
: ["Do not infer or normalize tool names; use only exact signatures shown below or returned by search."]),
|
||||
]
|
||||
|
||||
const workflow = empty
|
||||
? []
|
||||
: [
|
||||
"",
|
||||
"## Workflow",
|
||||
"",
|
||||
...(complete
|
||||
? [
|
||||
"1. Pick a tool from the list under `## Available tools` - each line is the exact call signature; use it as-is rather than guessing segments.",
|
||||
"2. Call it using the exact signature shown: `const result = await tools.<namespace>.<tool>(input)`; bracket notation and quotes are part of the path.",
|
||||
"3. Return only the fields you need from structured results; narrow unknown results before reading fields, and avoid returning large raw payloads.",
|
||||
]
|
||||
: [
|
||||
'1. If needed, discover tools with the built-in search function: `return search({ query: "<intent + key nouns>" })`.',
|
||||
"2. In the next execution, copy a returned path exactly, call it, and return only the needed fields.",
|
||||
]),
|
||||
]
|
||||
|
||||
const rules = empty
|
||||
? []
|
||||
: [
|
||||
"",
|
||||
"## Rules",
|
||||
"",
|
||||
complete
|
||||
? "- Only tools listed here are available; surrounding agent tools are not implicitly exposed."
|
||||
: "- Only tools listed here or returned by the built-in `search` function are available; surrounding agent tools are not implicitly exposed.",
|
||||
"- Filter, aggregate, and transform collections in code - never return them raw or call a tool per item across messages.",
|
||||
"- A result typed `Promise<unknown>` may be structured data or text. Before reading fields, check that it is a non-null object and not an array; otherwise handle the returned text or primitive directly.",
|
||||
'- Run independent calls in parallel: `await Promise.all(items.map((item) => tools.<namespace>.<tool>(item)))`, or use `tools.<namespace>["tool-name"](item)` when the listed signature uses bracket notation.',
|
||||
"- Execution ends when the program returns; pending promises are interrupted, so await every call whose completion matters.",
|
||||
"- `Object.keys(tools)` lists namespaces; `Object.keys(tools.<namespace>)` lists its tools; `for...in` works on both.",
|
||||
...(complete
|
||||
? []
|
||||
: [
|
||||
'- Browse one namespace: `search({ query: "", namespace: "<name>" })`.',
|
||||
"- If search returns `next`, repeat the same search with `offset: next.offset`.",
|
||||
]),
|
||||
]
|
||||
|
||||
const language = [
|
||||
"",
|
||||
"## Language",
|
||||
"",
|
||||
"Use common JavaScript data operations, functions, control flow, selected standard-library methods, and awaited tool calls. Built-ins include Date, RegExp, Map, Set, URL, URLSearchParams, and URI encoding helpers.",
|
||||
"Modules/imports, classes, timers, fetch, eval, prototype access, and unlisted methods are unavailable. Use tools for external operations. Use await with try/catch.",
|
||||
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||
]
|
||||
|
||||
const toolSection: Array<string> = [""]
|
||||
if (empty) {
|
||||
toolSection.push("## Available tools", "", "No tools are currently available.")
|
||||
} else {
|
||||
toolSection.push(
|
||||
complete
|
||||
? "## Available tools (COMPLETE list - every tool is shown below with its full call signature)"
|
||||
: `## Available tools (PARTIAL - ${totalShown} of ${described.length} shown; find the rest with search(...))`,
|
||||
"",
|
||||
)
|
||||
for (const [namespace, group] of ordered) {
|
||||
const picked = shown.get(namespace)!
|
||||
const count = `${group.length} tool${group.length === 1 ? "" : "s"}`
|
||||
const label =
|
||||
picked.size === group.length
|
||||
? count
|
||||
: picked.size === 0
|
||||
? `${count}, none shown`
|
||||
: `${count}, ${picked.size} shown`
|
||||
toolSection.push(`- ${namespace} (${label})`)
|
||||
for (const tool of group) if (picked.has(tool)) toolSection.push(catalogLine(tool))
|
||||
}
|
||||
if (!complete) {
|
||||
toolSection.push("", "Search returns complete callable signatures:", `- ${searchSignature}`)
|
||||
}
|
||||
}
|
||||
|
||||
const lines = [...intro, ...workflow, ...rules, ...language, ...toolSection]
|
||||
return {
|
||||
catalog: described,
|
||||
instructions: lines.join("\n"),
|
||||
catalog: visible.map(({ description }) => description),
|
||||
searchIndex: visible.map(({ path, tool, description }) => toSearchEntry(path, tool, description)),
|
||||
}
|
||||
}
|
||||
|
|
@ -612,7 +468,7 @@ const resolve = <R>(root: ToolNode<R>, path: ReadonlyArray<string>): Tool<R> =>
|
|||
const node = lookup(root, segments)
|
||||
if (node === undefined) {
|
||||
throw new ToolRuntimeError("UnknownTool", `Unknown tool '${segments.join(".")}'.`, [
|
||||
"Use search({ query }) to find available described tools.",
|
||||
"The tool may have been removed or renamed. Use search to find available tools.",
|
||||
])
|
||||
}
|
||||
if (node.tool === undefined) {
|
||||
|
|
@ -685,7 +541,11 @@ export const make = <R>(
|
|||
const input = yield* Effect.try({
|
||||
try: () => decodeToolInput(tool, externalArgs[0]),
|
||||
catch: (cause) =>
|
||||
new ToolRuntimeError("InvalidToolInput", `Invalid input for tool '${name}': ${String(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."],
|
||||
),
|
||||
})
|
||||
const index = yield* recordAndObserve(name, input)
|
||||
return yield* observeEnd(
|
||||
|
|
|
|||
|
|
@ -592,7 +592,7 @@ describe("CodeMode public contract", () => {
|
|||
expect(second).toStrictEqual({ ok: true, value: 1, logs: ["hi"], toolCalls: [{ name: "host.echo" }] })
|
||||
})
|
||||
|
||||
test("inlines a COMPLETE small catalog and keeps search registered but unadvertised", async () => {
|
||||
test("describes the catalog and keeps the search built-in registered", async () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
expect(runtime.catalog()).toStrictEqual([
|
||||
{
|
||||
|
|
@ -601,16 +601,7 @@ describe("CodeMode public contract", () => {
|
|||
signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
|
||||
},
|
||||
])
|
||||
expect(runtime.instructions()).toContain("Available tools (COMPLETE list")
|
||||
expect(runtime.instructions()).toContain("- orders (1 tool)")
|
||||
expect(runtime.instructions()).toContain(
|
||||
" - tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}> // Look up an order by ID",
|
||||
)
|
||||
// A fully inlined catalog does not advertise search in the instructions...
|
||||
expect(runtime.instructions()).not.toContain("search(")
|
||||
|
||||
// ...but the search built-in stays available, so a speculative call still works with the
|
||||
// same signature as the inline catalog.
|
||||
const result = await Effect.runPromise(runtime.execute(`return search({ query: "order" })`))
|
||||
expect(result.ok).toBe(true)
|
||||
if (result.ok) {
|
||||
|
|
@ -646,22 +637,7 @@ describe("CodeMode public contract", () => {
|
|||
const second = CodeMode.make({ tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } } })
|
||||
|
||||
expect(first.catalog()).toStrictEqual(second.catalog())
|
||||
expect(first.instructions()).toBe(second.instructions())
|
||||
expect(first.catalog().map((tool) => tool.path)).toEqual(["alpha.alpha", "alpha.zeta", "zeta.alpha", "zeta.zeta"])
|
||||
|
||||
for (const catalogBudget of [0, 10, 20, 40]) {
|
||||
expect(
|
||||
CodeMode.make({
|
||||
tools: { zeta: { zeta, alpha }, alpha: { zeta, alpha } },
|
||||
discovery: { catalogBudget },
|
||||
}).instructions(),
|
||||
).toBe(
|
||||
CodeMode.make({
|
||||
tools: { alpha: { alpha, zeta }, zeta: { alpha, zeta } },
|
||||
discovery: { catalogBudget },
|
||||
}).instructions(),
|
||||
)
|
||||
}
|
||||
})
|
||||
|
||||
test("renders bracket notation for tool names that are not JavaScript identifiers", async () => {
|
||||
|
|
@ -680,9 +656,6 @@ describe("CodeMode public contract", () => {
|
|||
signature: 'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
|
||||
},
|
||||
])
|
||||
expect(runtime.instructions()).toContain(
|
||||
'tools.context7["resolve-library-id"](input: {\n libraryName: string,\n}): Promise<string>',
|
||||
)
|
||||
|
||||
const search = await Effect.runPromise(runtime.execute(`return search({ query: "resolve library id" })`))
|
||||
expect(search.ok).toBe(true)
|
||||
|
|
@ -713,88 +686,6 @@ describe("CodeMode public contract", () => {
|
|||
if (exact.ok) expect(exact.value).toMatchObject({ remaining: 0, next: null })
|
||||
})
|
||||
|
||||
test("instructions use markdown sections with placeholder-only call forms", () => {
|
||||
const runtime = CodeMode.make({ tools })
|
||||
const instructions = runtime.instructions()
|
||||
// Sections in order: workflow at the top, catalog at the bottom.
|
||||
expect(instructions).toContain("## Workflow")
|
||||
expect(instructions).toContain("## Rules")
|
||||
expect(instructions).toContain("## Language")
|
||||
expect(instructions.indexOf("## Workflow")).toBeLessThan(instructions.indexOf("## Rules"))
|
||||
expect(instructions.indexOf("## Rules")).toBeLessThan(instructions.indexOf("## Language"))
|
||||
expect(instructions.indexOf("## Language")).toBeLessThan(
|
||||
instructions.indexOf("\n## Available tools (COMPLETE list"),
|
||||
)
|
||||
expect(instructions).not.toContain("JSON.parse(res)")
|
||||
expect(instructions).toContain("Return only the fields you need")
|
||||
expect(instructions).toContain("avoid returning large raw payloads")
|
||||
expect(instructions).toContain("Do not infer or normalize tool names")
|
||||
expect(instructions).toContain("bracket notation and quotes are part of the path")
|
||||
expect(instructions).toContain("surrounding agent tools are not available")
|
||||
expect(instructions).toContain("Only tools listed here are available")
|
||||
// Placeholders use generic namespace/tool/field names only - no fabricated real tools
|
||||
// and no real catalog tools cherry-picked into example lines.
|
||||
expect(instructions).toContain("`const result = await tools.<namespace>.<tool>(input)`")
|
||||
expect(instructions).toContain("Return only the fields you need from structured results")
|
||||
expect(instructions).toContain("check that it is a non-null object and not an array")
|
||||
expect(instructions).not.toContain("result.<field>")
|
||||
expect(instructions).not.toContain("data.<field>")
|
||||
expect(instructions).not.toContain("total_count")
|
||||
expect(instructions).not.toContain("list_issues")
|
||||
expect(instructions).not.toContain("tools.orders.lookup({")
|
||||
// COMPLETE: step 1 picks from the inlined list; search is not advertised.
|
||||
expect(instructions).toContain("1. Pick a tool from the list under `## Available tools`")
|
||||
expect(instructions).not.toContain("Browse one namespace")
|
||||
|
||||
const partial = CodeMode.make({ tools, discovery: { catalogBudget: 0 } }).instructions()
|
||||
// PARTIAL: the workflow starts with search (with query-style guidance that is clearly
|
||||
// a query string, never a tool name) and the browse-namespace rule appears.
|
||||
expect(partial).toContain(
|
||||
'1. If needed, discover tools with the built-in search function: `return search({ query: "<intent + key nouns>" })`.',
|
||||
)
|
||||
expect(partial).toContain("In the next execution, copy a returned path exactly")
|
||||
expect(partial).toContain("Only tools listed here or returned by the built-in `search` function")
|
||||
expect(partial).toContain('- Browse one namespace: `search({ query: "", namespace: "<name>" })`.')
|
||||
expect(partial).toContain("repeat the same search with `offset: next.offset`")
|
||||
expect(partial).toContain(" limit?: number,\n offset?: number,")
|
||||
expect(partial).not.toContain("total_count")
|
||||
expect(partial).not.toContain("tools.orders.lookup({")
|
||||
})
|
||||
|
||||
test("the language section describes the restricted runtime without overclaiming", () => {
|
||||
const instructions = CodeMode.make({ tools }).instructions()
|
||||
expect(instructions).toContain("restricted JavaScript language for calling tools")
|
||||
expect(instructions).toContain("not a general-purpose runtime")
|
||||
expect(instructions).not.toContain("Standard modern JavaScript works")
|
||||
expect(instructions).not.toContain("TypeScript type annotations")
|
||||
for (const missing of ["Modules/imports", "classes", "fetch"]) {
|
||||
expect(instructions).toContain(missing)
|
||||
}
|
||||
expect(instructions).not.toContain("generators")
|
||||
expect(instructions).not.toContain("new Promise(...) are unavailable")
|
||||
expect(instructions).not.toContain("promise chaining")
|
||||
expect(instructions).toContain("URL, URLSearchParams, and URI encoding helpers")
|
||||
expect(instructions).not.toContain("host globals")
|
||||
expect(instructions).toContain("Use tools for external operations")
|
||||
expect(instructions).toContain(
|
||||
"Prefer explicit `return`; otherwise only the final top-level expression becomes the result.",
|
||||
)
|
||||
expect(instructions).toContain(
|
||||
"Dates and URLs serialize to strings at data boundaries; Map/Set/RegExp/URLSearchParams serialize to `{}`.",
|
||||
)
|
||||
})
|
||||
|
||||
test("zero tools keep minimal sections and the no-tools notice", () => {
|
||||
const runtime = CodeMode.make({})
|
||||
const instructions = runtime.instructions()
|
||||
expect(instructions).toContain("No tools are currently available.")
|
||||
expect(instructions).toContain("## Language")
|
||||
expect(instructions).toContain("## Available tools")
|
||||
expect(instructions).not.toContain("## Workflow")
|
||||
expect(instructions).not.toContain("## Rules")
|
||||
expect(instructions).not.toContain("search(")
|
||||
})
|
||||
|
||||
test("uses one ranked search returning complete tools for large catalogs", async () => {
|
||||
const upload = Tool.make({
|
||||
description: "Upload one readable local file to the current Discord thread",
|
||||
|
|
@ -810,13 +701,7 @@ describe("CodeMode public contract", () => {
|
|||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: { thread: { uploadFile: upload, generateImage: generate }, orders: { lookup } },
|
||||
discovery: { catalogBudget: 0 },
|
||||
})
|
||||
expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 3 shown; find the rest with search(...))")
|
||||
expect(runtime.instructions()).toContain("- thread (2 tools, none shown)")
|
||||
expect(runtime.instructions()).toContain("- orders (1 tool, none shown)")
|
||||
expect(runtime.instructions()).toContain("Search returns complete callable signatures:\n- search(input: {")
|
||||
expect(runtime.instructions()).not.toMatch(/tools\.thread\.uploadFile\(input/)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
runtime.execute(`
|
||||
|
|
@ -1101,64 +986,6 @@ describe("CodeMode public contract", () => {
|
|||
if (exhausted.ok) expect(exhausted.value).toStrictEqual({ items: [], remaining: 0, next: null })
|
||||
})
|
||||
|
||||
test("inlines round-robin across namespaces so one expensive namespace cannot starve the rest", () => {
|
||||
const cheap = Tool.make({
|
||||
description: "Cheap",
|
||||
input: Schema.Struct({ q: Schema.String }),
|
||||
output: Schema.String,
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const expensive = Tool.make({
|
||||
description:
|
||||
"An expensive tool whose description alone consumes far more than the remaining inline catalog byte budget for this runtime",
|
||||
input: Schema.Struct({
|
||||
someRatherLongParameterName: Schema.String,
|
||||
anotherEvenLongerParameterName: Schema.Number,
|
||||
}),
|
||||
output: Schema.String,
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
// Round 1 places alpha.cheap (~17 estimated tokens) and beta.cheap (~17); in round 2
|
||||
// alpha.expensive does not fit, which marks only alpha done - it must NOT prevent
|
||||
// other namespaces from inlining (beta already got its line in the same round).
|
||||
const runtime = CodeMode.make({
|
||||
tools: { alpha: { cheap, expensive }, beta: { cheap } },
|
||||
discovery: { catalogBudget: 40 },
|
||||
})
|
||||
|
||||
const instructions = runtime.instructions()
|
||||
expect(instructions).toContain("Available tools (PARTIAL - 2 of 3 shown; find the rest with search(...))")
|
||||
expect(instructions).toContain("- alpha (2 tools, 1 shown)")
|
||||
expect(instructions).toContain(" - tools.alpha.cheap(input: {\n q: string,\n}): Promise<string> // Cheap")
|
||||
expect(instructions).not.toContain("tools.alpha.expensive(")
|
||||
// Fully shown namespaces read cleanly (no "shown" annotation).
|
||||
expect(instructions).toContain("- beta (1 tool)")
|
||||
expect(instructions).toContain(" - tools.beta.cheap(input: {\n q: string,\n}): Promise<string> // Cheap")
|
||||
expect(instructions).toContain("Search returns complete callable signatures:\n- search(input: {")
|
||||
})
|
||||
|
||||
test("charges inline JSDoc against the catalog token budget", () => {
|
||||
const documented = Tool.make({
|
||||
description: "Look up a record",
|
||||
input: {
|
||||
type: "object",
|
||||
properties: {
|
||||
id: { type: "string", description: "A detailed identifier description. ".repeat(20) },
|
||||
},
|
||||
required: ["id"],
|
||||
} as const,
|
||||
execute: () => Effect.succeed("ok"),
|
||||
})
|
||||
const runtime = CodeMode.make({
|
||||
tools: { records: { lookup: documented } },
|
||||
discovery: { catalogBudget: 40 },
|
||||
})
|
||||
|
||||
expect(runtime.catalog()[0]?.signature).toContain("/** A detailed identifier description.")
|
||||
expect(runtime.instructions()).toContain("Available tools (PARTIAL - 0 of 1 shown; find the rest with search(...))")
|
||||
expect(runtime.instructions()).not.toContain("tools.records.lookup(input:")
|
||||
})
|
||||
|
||||
test("decodes tool input and output before exposing either side", async () => {
|
||||
const observed: Array<unknown> = []
|
||||
const transformed = Tool.make({
|
||||
|
|
@ -1219,7 +1046,7 @@ describe("CodeMode public contract", () => {
|
|||
expect(result).toStrictEqual({ ok: true, value: null, toolCalls: [] })
|
||||
})
|
||||
|
||||
test("rejects invalid configuration and discovery limits", async () => {
|
||||
test("rejects invalid configuration and search limits", async () => {
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: 0 } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { timeoutMs: Number.POSITIVE_INFINITY } })).toThrow(
|
||||
RangeError,
|
||||
|
|
@ -1227,13 +1054,8 @@ describe("CodeMode public contract", () => {
|
|||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxToolCalls: -1 } })).toThrow(RangeError)
|
||||
expect(() => CodeMode.execute({ code: "return 1", limits: { maxOutputBytes: -1 } })).toThrow(RangeError)
|
||||
|
||||
expect(() => CodeMode.make({ tools, discovery: { catalogBudget: -1 } })).toThrow(RangeError)
|
||||
|
||||
const result = await Effect.runPromise(
|
||||
CodeMode.make({
|
||||
tools,
|
||||
discovery: { catalogBudget: 0 },
|
||||
}).execute(`return search({ query: "order", limit: 0.5 })`),
|
||||
CodeMode.make({ tools }).execute(`return search({ query: "order", limit: 0.5 })`),
|
||||
)
|
||||
expect(result.ok).toBe(false)
|
||||
if (result.ok) return
|
||||
|
|
|
|||
|
|
@ -395,15 +395,15 @@ describe("JSDoc signatures in catalogs and search results", () => {
|
|||
}
|
||||
})
|
||||
|
||||
test("the inline catalog uses the same JSDoc signatures", async () => {
|
||||
const instructions = runtime.instructions()
|
||||
test("the catalog uses the same JSDoc signatures as search", async () => {
|
||||
const catalog = runtime.catalog()
|
||||
const github = (await search("list issues repository")).items.find(
|
||||
({ path }) => path === "tools.github.list_issues",
|
||||
)!
|
||||
const orders = (await search("look up order")).items.find(({ path }) => path === "tools.orders.lookup")!
|
||||
expect(instructions).toContain(` - ${github.signature} // List issues in a repository`)
|
||||
expect(instructions).toContain(` - ${orders.signature} // Look up an order`)
|
||||
expect(instructions).toContain("/** Repository owner */")
|
||||
expect(catalog.map(({ signature }) => signature)).toContain(github.signature)
|
||||
expect(catalog.map(({ signature }) => signature)).toContain(orders.signature)
|
||||
expect(github.signature).toContain("/** Repository owner */")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -423,16 +423,10 @@ describe("non-identifier tool paths", () => {
|
|||
})
|
||||
const runtime = CodeMode.make({ tools: { context7: { "resolve-library-id": resolveLibrary } } })
|
||||
|
||||
test("inline catalog uses bracket notation for dashed tool names", () => {
|
||||
const instructions = runtime.instructions()
|
||||
|
||||
expect(instructions).toContain(
|
||||
test("catalog signatures use bracket notation for dashed tool names", () => {
|
||||
expect(runtime.catalog()[0]?.signature).toBe(
|
||||
'tools.context7["resolve-library-id"](input: {\n query: string,\n libraryName: string,\n}): Promise<unknown>',
|
||||
)
|
||||
expect(instructions).toContain("Do not infer or normalize tool names")
|
||||
expect(instructions).toContain("bracket notation and quotes are part of the path")
|
||||
expect(instructions).not.toContain("tools.context7.resolve-library-id")
|
||||
expect(instructions).not.toContain("tools.context7.resolve_library_id")
|
||||
})
|
||||
|
||||
test("search results return callable bracket-notation paths and signatures", async () => {
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ describe("dotted tool names", () => {
|
|||
expect(catalog).toHaveLength(1)
|
||||
expect(catalog[0]?.path).toBe("api.issues.list")
|
||||
expect(catalog[0]?.signature).toStartWith("tools.api.issues.list(input:")
|
||||
expect(runtime.instructions()).toContain("tools.api.issues.list(input:")
|
||||
})
|
||||
|
||||
test("the advertised dotted path is executable", async () => {
|
||||
|
|
@ -86,6 +85,9 @@ describe("callable namespaces", () => {
|
|||
const diagnostic = await failure(runtime, `return await tools.issues.missing({})`)
|
||||
expect(diagnostic.kind).toBe("UnknownTool")
|
||||
expect(diagnostic.message).toContain("Unknown tool 'issues.missing'")
|
||||
expect(diagnostic.suggestions).toEqual([
|
||||
"The tool may have been removed or renamed. Use search to find available tools.",
|
||||
])
|
||||
})
|
||||
|
||||
test("a namespace without its own tool stays non-callable", async () => {
|
||||
|
|
@ -96,6 +98,31 @@ describe("callable namespaces", () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe("tool input diagnostics", () => {
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
"notes.echo": Tool.make({
|
||||
description: "Echo text",
|
||||
input: Schema.Struct({ text: Schema.String }),
|
||||
output: Schema.String,
|
||||
execute: ({ text }) => Effect.succeed(text),
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
||||
test("a schema mismatch suggests searching for the current signature", async () => {
|
||||
const diagnostic = await failure(runtime, `return await tools.notes.echo({ message: "hello" })`)
|
||||
expect(diagnostic.kind).toBe("InvalidToolInput")
|
||||
expect(diagnostic.suggestions).toEqual(["The signature may have changed. Use search to get the current signature."])
|
||||
})
|
||||
|
||||
test("a wrong argument count keeps the existing error without a stale-signature hint", async () => {
|
||||
const diagnostic = await failure(runtime, `return await tools.notes.echo()`)
|
||||
expect(diagnostic.kind).toBe("InvalidToolInput")
|
||||
expect(diagnostic.suggestions).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe("blocked member names on tool paths", () => {
|
||||
const runtime = CodeMode.make({
|
||||
tools: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue