mirror of
https://github.com/anomalyco/opencode.git
synced 2026-09-08 13:24:36 +00:00
refactor(core): nest code mode catalog (#46541)
This commit is contained in:
parent
02440f6715
commit
000b42d204
10 changed files with 152 additions and 57 deletions
|
|
@ -1,19 +1,25 @@
|
|||
export * as CodeModeCatalog from "./catalog.js"
|
||||
|
||||
import type { Namespace } from "@opencode-ai/schema/tool"
|
||||
import { Schema } from "effect"
|
||||
|
||||
export const Tool = Schema.Struct({
|
||||
path: Schema.String,
|
||||
type: Schema.Literal("tool"),
|
||||
name: Schema.String,
|
||||
description: Schema.String,
|
||||
signature: Schema.String,
|
||||
pinned: Schema.optionalKey(Schema.Boolean),
|
||||
})
|
||||
export type Tool = typeof Tool.Type
|
||||
|
||||
export type Namespace = {
|
||||
readonly type: "namespace"
|
||||
readonly name: string
|
||||
readonly description?: string
|
||||
readonly tools: ReadonlyArray<Tool | Namespace>
|
||||
}
|
||||
|
||||
export type Inventory = {
|
||||
readonly tools: ReadonlyArray<Tool>
|
||||
readonly namespaces?: ReadonlyMap<string, Namespace>
|
||||
readonly tools: ReadonlyArray<Tool | Namespace>
|
||||
}
|
||||
|
||||
const Listing = Schema.Struct({
|
||||
|
|
@ -47,14 +53,15 @@ const INLINE_BUDGET = 2_000
|
|||
// considering shorter listings first until the inline budget is exhausted.
|
||||
export function summarize(inventory: Inventory, options: Options = {}): Summary {
|
||||
const budget = options.budget ?? INLINE_BUDGET
|
||||
const namespaces = [...Map.groupBy(inventory.tools, (tool) => tool.path.split(".", 1)[0] ?? tool.path)]
|
||||
const flattened = flatten(inventory.tools)
|
||||
const namespaces = [...Map.groupBy(flattened.tools, (tool) => tool.path.split(".", 1)[0] ?? tool.path)]
|
||||
.sort(([left], [right]) => {
|
||||
if (left < right) return -1
|
||||
if (left > right) return 1
|
||||
return 0
|
||||
})
|
||||
.map(([name, namespaceEntries]) => {
|
||||
const description = inventory.namespaces?.get(name)?.description
|
||||
const description = flattened.namespaces.get(name)?.description
|
||||
const listings = namespaceEntries
|
||||
.map((entry) => {
|
||||
const firstLine = entry.description.split("\n", 1)[0]?.trim() ?? ""
|
||||
|
|
@ -126,12 +133,35 @@ export function summarize(inventory: Inventory, options: Options = {}): Summary
|
|||
entries: namespace.listings.filter((listing) => namespace.selectedListings.has(listing)),
|
||||
}))
|
||||
return {
|
||||
total: inventory.tools.length,
|
||||
total: flattened.tools.length,
|
||||
shown: namespaceSummaries.reduce((total, namespace) => total + namespace.entries.length, 0),
|
||||
namespaces: namespaceSummaries,
|
||||
}
|
||||
}
|
||||
|
||||
function flatten(entries: ReadonlyArray<Tool | Namespace>, path: ReadonlyArray<string> = []) {
|
||||
const tools: Array<Omit<Tool, "name"> & { readonly path: string }> = []
|
||||
const namespaces = new Map<string, Namespace>()
|
||||
for (const entry of entries) {
|
||||
if (entry.type === "tool") {
|
||||
tools.push({
|
||||
type: "tool",
|
||||
path: [...path, entry.name].join("."),
|
||||
description: entry.description,
|
||||
signature: entry.signature,
|
||||
...(entry.pinned === undefined ? {} : { pinned: entry.pinned }),
|
||||
})
|
||||
continue
|
||||
}
|
||||
const next = [...path, entry.name]
|
||||
namespaces.set(next.join("."), entry)
|
||||
const nested = flatten(entry.tools, next)
|
||||
tools.push(...nested.tools)
|
||||
for (const [name, namespace] of nested.namespaces) namespaces.set(name, namespace)
|
||||
}
|
||||
return { tools, namespaces }
|
||||
}
|
||||
|
||||
export function namespaceLine(namespace: typeof NamespaceSummary.Type) {
|
||||
const count = namespace.count === 1 ? "1 tool" : `${namespace.count} tools`
|
||||
const label =
|
||||
|
|
|
|||
|
|
@ -40,12 +40,14 @@ type CollectedFiles = {
|
|||
readonly files: Array<typeof ExecuteFile.Type>
|
||||
}
|
||||
|
||||
type ToolNode = {
|
||||
tool?: Tool.Tool<never>
|
||||
type Node<T> = {
|
||||
tool?: T
|
||||
namespace?: ToolNamespace
|
||||
readonly children: Map<string, ToolNode>
|
||||
readonly children: Map<string, Node<T>>
|
||||
}
|
||||
|
||||
type ToolNode = Node<Tool.Tool<never>>
|
||||
|
||||
type Tools = {
|
||||
[name: string]: Tool.Tool<never> | Namespace.Namespace<never> | Tools
|
||||
}
|
||||
|
|
@ -162,14 +164,43 @@ export const catalog = (inventory: Inventory) => {
|
|||
.filter((registration) => registration.options?.pinned === true)
|
||||
.map(qualifiedName),
|
||||
)
|
||||
const root: CatalogNode = { children: new Map() }
|
||||
for (const namespace of inventory.namespaces?.values() ?? []) getNode(root, namespace.name).namespace = namespace
|
||||
for (const tool of runtime(inventory, () => Effect.fail(toolError("Execute context is unavailable"))).catalog())
|
||||
getNode(root, tool.path).tool = {
|
||||
type: "tool",
|
||||
name: tool.path.split(".").at(-1) ?? tool.path,
|
||||
description: tool.description,
|
||||
signature: tool.signature,
|
||||
pinned: pinned.has(tool.path),
|
||||
}
|
||||
return {
|
||||
tools: runtime(inventory, () => Effect.fail(toolError("Execute context is unavailable")))
|
||||
.catalog()
|
||||
.map((tool) => ({ ...tool, pinned: pinned.has(tool.path) })),
|
||||
...(inventory.namespaces === undefined ? {} : { namespaces: inventory.namespaces }),
|
||||
tools: renderCatalog(root),
|
||||
} satisfies CodeModeCatalog.Inventory
|
||||
}
|
||||
|
||||
type CatalogNode = Node<CodeModeCatalog.Tool>
|
||||
|
||||
function renderCatalog(root: CatalogNode): ReadonlyArray<CodeModeCatalog.Tool | CodeModeCatalog.Namespace> {
|
||||
return Array.from(root.children)
|
||||
.toSorted(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
|
||||
.flatMap(([name, node]) => {
|
||||
const tools = renderCatalog(node)
|
||||
const namespace =
|
||||
node.namespace === undefined && tools.length === 0
|
||||
? undefined
|
||||
: {
|
||||
type: "namespace" as const,
|
||||
name,
|
||||
...(node.namespace?.description === undefined ? {} : { description: node.namespace.description }),
|
||||
tools,
|
||||
}
|
||||
if (node.tool === undefined) return namespace === undefined ? [] : [namespace]
|
||||
if (namespace === undefined) return [node.tool]
|
||||
return [node.tool, namespace]
|
||||
})
|
||||
}
|
||||
|
||||
function runtime(
|
||||
inventory: Inventory,
|
||||
executeTool: (name: string, tool: Info, input: unknown) => Effect.Effect<unknown, unknown>,
|
||||
|
|
@ -191,9 +222,9 @@ function runtime(
|
|||
return CodeMode.make<typeof tools>({ tools, ...hooks })
|
||||
}
|
||||
|
||||
function getNode(root: ToolNode, path: string) {
|
||||
function getNode<T>(root: Node<T>, path: string) {
|
||||
return path.split(".").reduce((parent, name) => {
|
||||
const child: ToolNode = parent.children.get(name) ?? { children: new Map() }
|
||||
const child: Node<T> = parent.children.get(name) ?? { children: new Map() }
|
||||
parent.children.set(name, child)
|
||||
return child
|
||||
}, root)
|
||||
|
|
|
|||
|
|
@ -10,7 +10,8 @@ describe("CodeMode", () => {
|
|||
it.effect("owns registrations, execute, and catalog materialization", () =>
|
||||
Effect.gen(function* () {
|
||||
const tools = yield* Tool.Service
|
||||
yield* tools.transform((draft) =>
|
||||
yield* tools.transform((draft) => {
|
||||
draft.namespace({ name: "empty", description: "No tools registered yet" })
|
||||
draft.add({
|
||||
name: "echo",
|
||||
description: "Echo text",
|
||||
|
|
@ -18,21 +19,27 @@ describe("CodeMode", () => {
|
|||
output: Schema.String,
|
||||
options: { pinned: true },
|
||||
execute: ({ text }) => Effect.succeed({ output: text }),
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
const snapshot = yield* tools.snapshot()
|
||||
expect(snapshot.definitions.some((tool) => tool.name === "execute")).toBe(true)
|
||||
expect(snapshot.codeModeCatalog).toStrictEqual({
|
||||
tools: [
|
||||
{
|
||||
path: "echo",
|
||||
type: "tool",
|
||||
name: "echo",
|
||||
description: "Echo text",
|
||||
signature: "tools.echo(input: {\n text: string,\n}): Promise<string>",
|
||||
pinned: true,
|
||||
},
|
||||
{
|
||||
type: "namespace",
|
||||
name: "empty",
|
||||
description: "No tools registered yet",
|
||||
tools: [],
|
||||
},
|
||||
],
|
||||
namespaces: new Map(),
|
||||
})
|
||||
}).pipe(
|
||||
Effect.scoped,
|
||||
|
|
|
|||
|
|
@ -3,7 +3,8 @@ import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog"
|
|||
import { CodeModeInstructions } from "@opencode-ai/core/codemode/instructions"
|
||||
|
||||
const entry = (path: string, description: string, signature?: string, pinned = false): CodeModeCatalog.Tool => ({
|
||||
path,
|
||||
type: "tool",
|
||||
name: path,
|
||||
description,
|
||||
signature: signature ?? `tools.${path}(input: {\n q: string,\n}): Promise<string>`,
|
||||
pinned,
|
||||
|
|
@ -15,12 +16,12 @@ const lookup = entry(
|
|||
"tools.orders.lookup(input: {\n id: string,\n}): Promise<{\n id: string,\n status: string,\n}>",
|
||||
)
|
||||
|
||||
const render = (tools: ReadonlyArray<CodeModeCatalog.Tool>, budget?: number) =>
|
||||
const render = (tools: CodeModeCatalog.Inventory["tools"], budget?: number) =>
|
||||
CodeModeInstructions.render(CodeModeCatalog.summarize({ tools }, budget === undefined ? {} : { budget }))
|
||||
|
||||
const update = (
|
||||
previous: ReadonlyArray<CodeModeCatalog.Tool>,
|
||||
current: ReadonlyArray<CodeModeCatalog.Tool>,
|
||||
previous: CodeModeCatalog.Inventory["tools"],
|
||||
current: CodeModeCatalog.Inventory["tools"],
|
||||
budget?: number,
|
||||
) =>
|
||||
CodeModeInstructions.update(
|
||||
|
|
@ -103,10 +104,10 @@ describe("CodeModeCatalog.summarize", () => {
|
|||
const listingCost = Math.round(` - ${tool.signature} // One`.length / 4)
|
||||
const namespaceCost = Math.round(CodeModeCatalog.namespaceLine({ name: "alpha", count: 1, entries: [] }).length / 4)
|
||||
const description = "A namespace description that stays visible beyond the available tool budget"
|
||||
const namespaces = new Map([["alpha", { name: "alpha", description }]])
|
||||
const namespace = { type: "namespace" as const, name: "alpha", description, tools: [tool] }
|
||||
|
||||
expect(CodeModeCatalog.summarize({ tools: [tool] }, { budget: namespaceCost + listingCost }).shown).toBe(1)
|
||||
const catalog = CodeModeCatalog.summarize({ tools: [tool], namespaces }, { budget: namespaceCost + listingCost })
|
||||
const catalog = CodeModeCatalog.summarize({ tools: [namespace] }, { budget: namespaceCost + listingCost })
|
||||
expect(catalog.shown).toBe(0)
|
||||
expect(catalog.namespaces[0]?.description).toBe(description)
|
||||
expect(CodeModeInstructions.render(catalog)).toContain(`- alpha (1 tool, none shown) // ${description}`)
|
||||
|
|
@ -209,12 +210,10 @@ describe("CodeModeInstructions.update", () => {
|
|||
|
||||
test("restates namespace descriptions when they change", () => {
|
||||
const previous = CodeModeCatalog.summarize({
|
||||
tools: [echo],
|
||||
namespaces: new Map([["notes", { name: "notes", description: "Old description" }]]),
|
||||
tools: [{ type: "namespace", name: "notes", description: "Old description", tools: [echo] }],
|
||||
})
|
||||
const current = CodeModeCatalog.summarize({
|
||||
tools: [echo],
|
||||
namespaces: new Map([["notes", { name: "notes", description: "New description" }]]),
|
||||
tools: [{ type: "namespace", name: "notes", description: "New description", tools: [echo] }],
|
||||
})
|
||||
const text = CodeModeInstructions.update(previous, current)
|
||||
expect(text).toContain("This catalog supersedes the previous Code Mode tool catalog.")
|
||||
|
|
|
|||
|
|
@ -10,13 +10,15 @@ import { it } from "../lib/effect"
|
|||
import { readInitial, readUpdate } from "../lib/instructions"
|
||||
|
||||
const echo: CodeModeCatalog.Tool = {
|
||||
path: "notes.echo",
|
||||
type: "tool",
|
||||
name: "notes.echo",
|
||||
description: "Echo text",
|
||||
signature: "tools.notes.echo(input: {\n text: string,\n}): Promise<string>",
|
||||
}
|
||||
|
||||
const lookup: CodeModeCatalog.Tool = {
|
||||
path: "orders.lookup",
|
||||
type: "tool",
|
||||
name: "orders.lookup",
|
||||
description: "Look up an order",
|
||||
signature: "tools.orders.lookup(input: {\n id: string,\n}): Promise<unknown>",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { Agent } from "@opencode-ai/core/agent"
|
||||
import { CodeModeCatalog } from "@opencode-ai/core/codemode/catalog"
|
||||
import type { Permission } from "@opencode-ai/core/permission"
|
||||
import { SessionMessage } from "@opencode-ai/core/session/message"
|
||||
import { toSessionError } from "@opencode-ai/core/session/to-session-error"
|
||||
|
|
@ -16,6 +17,9 @@ export const toolIdentity = {
|
|||
export const toolDefinitions = (registry: Tool.Interface, permissions?: Permission.Ruleset) =>
|
||||
registry.snapshot(permissions).pipe(Effect.map((toolSet) => toolSet.definitions))
|
||||
|
||||
export const codeModeListings = (catalog: CodeModeCatalog.Inventory) =>
|
||||
CodeModeCatalog.summarize(catalog, { budget: Infinity }).namespaces.flatMap((namespace) => namespace.entries)
|
||||
|
||||
export function waitForTool(registry: Tool.Interface, name: string, remaining = 1000): Effect.Effect<void, Error> {
|
||||
return Effect.gen(function* () {
|
||||
if ((yield* toolDefinitions(registry)).some((tool) => tool.name === name)) return
|
||||
|
|
@ -35,7 +39,8 @@ export function waitForCodeModeTool(
|
|||
): Effect.Effect<Tool.Snapshot, Error> {
|
||||
return Effect.gen(function* () {
|
||||
const toolSet = yield* registry.snapshot()
|
||||
if (toolSet.codeModeCatalog?.tools.some((tool) => tool.path === path)) return toolSet
|
||||
if (toolSet.codeModeCatalog && codeModeListings(toolSet.codeModeCatalog).some((tool) => tool.path === path))
|
||||
return toolSet
|
||||
if (remaining === 0) {
|
||||
return yield* Effect.fail(new Error(`Timed out waiting for Code Mode tool: ${path}`))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,7 +46,7 @@ import { imagePassthrough } from "./lib/image"
|
|||
import { location } from "./fixture/location"
|
||||
import { tmpdirScoped } from "./fixture/tmpdir"
|
||||
import { hostEnvironmentLayer, recordingEnvironmentLayer } from "./fixture/environment"
|
||||
import { executeTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
||||
import { codeModeListings, executeTool, toolDefinitions, toolIdentity, waitForTool } from "./lib/tool"
|
||||
|
||||
let assertion: Deferred.Deferred<Permission.AssertInput> | undefined
|
||||
let decision: Effect.Effect<void, Permission.Error> = Effect.void
|
||||
|
|
@ -1764,7 +1764,7 @@ it.effect("advertises MCP output schemas to Code Mode", () =>
|
|||
"direct_media",
|
||||
"execute",
|
||||
])
|
||||
expect(toolSet.codeModeCatalog?.tools.find((tool) => tool.path === "demo.search")?.signature).toContain(
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!).find((tool) => tool.path === "demo.search")?.line).toContain(
|
||||
"ok: boolean",
|
||||
)
|
||||
expect(execute?.description).not.toContain("tools.demo.search")
|
||||
|
|
@ -1784,7 +1784,7 @@ it.effect("forwards the invoking session through direct and Code Mode MCP tools"
|
|||
expect(toolSet.definitions.find((tool) => tool.name === "direct_lookup")?.inputSchema).not.toHaveProperty(
|
||||
"properties.sessionID",
|
||||
)
|
||||
expect(toolSet.codeModeCatalog?.tools.find((tool) => tool.path === "demo.search")?.signature).not.toContain(
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!).find((tool) => tool.path === "demo.search")?.line).not.toContain(
|
||||
"sessionID",
|
||||
)
|
||||
|
||||
|
|
@ -1830,7 +1830,7 @@ it.effect("returns content-only MCP results through Code Mode", () =>
|
|||
yield* registration.flush
|
||||
const toolSet = yield* registry.snapshot()
|
||||
|
||||
expect(toolSet.codeModeCatalog?.tools.some((tool) => tool.path === "demo.status")).toBe(true)
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!).some((tool) => tool.path === "demo.status")).toBe(true)
|
||||
|
||||
const execution = yield* toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_mcp_content_only"),
|
||||
|
|
@ -1916,7 +1916,7 @@ it.effect("waits for permission before calling an MCP tool", () =>
|
|||
const registration = yield* McpTool.Service
|
||||
yield* registration.flush
|
||||
const toolSet = yield* registry.snapshot()
|
||||
expect(toolSet.codeModeCatalog?.tools.some((tool) => tool.path === "demo.search")).toBe(true)
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!).some((tool) => tool.path === "demo.search")).toBe(true)
|
||||
|
||||
const fiber = yield* toolSet
|
||||
.execute({
|
||||
|
|
@ -1960,7 +1960,7 @@ it.effect("does not call MCP when permission is blocked", () =>
|
|||
const registration = yield* McpTool.Service
|
||||
yield* registration.flush
|
||||
const toolSet = yield* registry.snapshot()
|
||||
expect(toolSet.codeModeCatalog?.tools.some((tool) => tool.path === "demo.search")).toBe(true)
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!).some((tool) => tool.path === "demo.search")).toBe(true)
|
||||
|
||||
const execution = yield* toolSet.execute({
|
||||
sessionID: Session.ID.make("ses_mcp_blocked"),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import { Tool } from "@opencode-ai/core/tool"
|
|||
import { Provider } from "@opencode-ai/core/provider"
|
||||
import { Project } from "@opencode-ai/core/project"
|
||||
import { Workspace } from "@opencode-ai/core/workspace"
|
||||
import { codeModeListings } from "../lib/tool"
|
||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||
import { define } from "@opencode-ai/plugin/promise/plugin"
|
||||
import type { Info } from "@opencode-ai/plugin/promise/tool"
|
||||
|
|
@ -981,7 +982,7 @@ describe("fromPromise", () => {
|
|||
|
||||
const snapshot = yield* registry.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual(["acme.hello"])
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual(["acme.hello"])
|
||||
expect(original.definitions.map((tool) => tool.name)).toEqual(["acme_hello", "execute"])
|
||||
expect(
|
||||
yield* snapshot.execute({
|
||||
|
|
|
|||
|
|
@ -118,7 +118,8 @@ const tools = Layer.mock(Tool.Service, {
|
|||
codeModeCatalog: {
|
||||
tools: [
|
||||
{
|
||||
path: "captured.lookup",
|
||||
type: "tool",
|
||||
name: "captured.lookup",
|
||||
description: "Captured Code Mode catalog",
|
||||
signature: "tools.captured.lookup(input: {}): Promise<string>",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ import { State } from "@opencode-ai/core/state"
|
|||
import { Tool } from "@opencode-ai/core/tool"
|
||||
import type { Info } from "@opencode-ai/schema/tool"
|
||||
import { LayerNode } from "@opencode-ai/util/effect/layer-node"
|
||||
import { executeTool, toolDefinitions } from "./lib/tool"
|
||||
import { codeModeListings, executeTool, toolDefinitions } from "./lib/tool"
|
||||
import { Deferred, Effect, Exit, Fiber, Layer, Logger, Schema, SchemaGetter, SchemaIssue, Scope } from "effect"
|
||||
import { TestClock } from "effect/testing"
|
||||
import { z } from "zod"
|
||||
|
|
@ -247,7 +247,9 @@ describe("Tool", () => {
|
|||
yield* update.dispose
|
||||
expect((yield* executeTool(service, call("acme_echo"))).output).toEqual({ text: "refreshed" })
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
expect((yield* service.snapshot()).codeModeCatalog?.tools.map((tool) => tool.path)).toEqual(["hidden"])
|
||||
expect(codeModeListings((yield* service.snapshot()).codeModeCatalog!).map((tool) => tool.path)).toEqual([
|
||||
"hidden",
|
||||
])
|
||||
|
||||
yield* service.transform((draft) =>
|
||||
draft.update("acme_echo", (tool) => {
|
||||
|
|
@ -502,7 +504,7 @@ describe("Tool", () => {
|
|||
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual([
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual([
|
||||
"-lookup",
|
||||
"123",
|
||||
"123._private.-tools.2d_get_scene",
|
||||
|
|
@ -542,7 +544,9 @@ describe("Tool", () => {
|
|||
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["first", "execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual(["invalid__namespace.second"])
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual([
|
||||
"invalid__namespace.second",
|
||||
])
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -560,17 +564,32 @@ describe("Tool", () => {
|
|||
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["registry_direct", "execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual([
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual([
|
||||
"legacy.plain",
|
||||
"registry.search",
|
||||
"registry.search.sales",
|
||||
])
|
||||
expect(snapshot.codeModeCatalog?.namespaces).toEqual(
|
||||
new Map([
|
||||
["registry", { name: "registry", description: "Package publishing and discovery" }],
|
||||
["registry.search", { name: "registry.search", description: "Pricing operations" }],
|
||||
]),
|
||||
)
|
||||
expect(snapshot.codeModeCatalog?.tools).toEqual([
|
||||
{
|
||||
type: "namespace",
|
||||
name: "legacy",
|
||||
tools: [expect.objectContaining({ type: "tool", name: "plain" })],
|
||||
},
|
||||
{
|
||||
type: "namespace",
|
||||
name: "registry",
|
||||
description: "Package publishing and discovery",
|
||||
tools: [
|
||||
expect.objectContaining({ type: "tool", name: "search" }),
|
||||
{
|
||||
type: "namespace",
|
||||
name: "search",
|
||||
description: "Pricing operations",
|
||||
tools: [expect.objectContaining({ type: "tool", name: "sales" })],
|
||||
},
|
||||
],
|
||||
},
|
||||
])
|
||||
const result = yield* snapshot.execute({
|
||||
...call("execute"),
|
||||
call: {
|
||||
|
|
@ -615,7 +634,7 @@ describe("Tool", () => {
|
|||
})
|
||||
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual(["pricing", "pricing.sales"])
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual(["pricing", "pricing.sales"])
|
||||
const result = yield* snapshot.execute({
|
||||
...call("execute"),
|
||||
call: {
|
||||
|
|
@ -664,7 +683,7 @@ describe("Tool", () => {
|
|||
])
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["healthy", "execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools.map((tool) => tool.path)).toEqual(["codemode"])
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!).map((tool) => tool.path)).toEqual(["codemode"])
|
||||
expect((yield* snapshot.execute(call("phone_type")).pipe(Effect.flip)).message).toBe("Unknown tool: phone_type")
|
||||
}).pipe(Effect.provide(Logger.layer([logger])))
|
||||
})
|
||||
|
|
@ -729,7 +748,7 @@ describe("Tool", () => {
|
|||
|
||||
const snapshot = yield* service.snapshot()
|
||||
expect(snapshot.definitions.map((tool) => tool.name)).toEqual(["execute"])
|
||||
expect(snapshot.codeModeCatalog?.tools[0]?.signature).toContain("tools.echo")
|
||||
expect(codeModeListings(snapshot.codeModeCatalog!)[0]?.line).toContain("tools.echo")
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -1192,7 +1211,7 @@ describe("Tool", () => {
|
|||
}).pipe(Scope.provide(scope))
|
||||
const toolSet = yield* service.snapshot()
|
||||
const execute = toolSet.definitions.find((tool) => tool.name === "execute")
|
||||
expect(toolSet.codeModeCatalog?.tools[0]?.signature).toContain("tools.echo")
|
||||
expect(codeModeListings(toolSet.codeModeCatalog!)[0]?.line).toContain("tools.echo")
|
||||
expect(execute?.description).toContain("confined Code Mode runtime")
|
||||
expect(execute?.description).not.toContain("Echo text")
|
||||
yield* Scope.close(scope, Exit.void)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue