chore: generate
This commit is contained in:
parent
cb93114424
commit
83c638eaac
20 changed files with 2137 additions and 1207 deletions
|
|
@ -206,10 +206,7 @@ export const prepare = Effect.fn("LLMRequestPrep.prepare")(function* (input: Pre
|
|||
})
|
||||
|
||||
function resolveTools(input: Pick<PrepareInput, "tools" | "agent" | "permission" | "user">) {
|
||||
const visible = Permission.visibleTools(
|
||||
input.tools,
|
||||
Permission.merge(input.agent.permission, input.permission ?? []),
|
||||
)
|
||||
const visible = Permission.visibleTools(input.tools, Permission.merge(input.agent.permission, input.permission ?? []))
|
||||
return Record.filter(visible, (_, k) => input.user.tools?.[k] !== false)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -104,7 +104,8 @@ export function groupByServer(
|
|||
const byLongest = [...servers].sort((a, b) => b.length - a.length)
|
||||
const groups = new Map<string, CatalogEntry[]>()
|
||||
for (const key of Object.keys(mcpTools).sort((a, b) => a.localeCompare(b))) {
|
||||
const server = byLongest.find((name) => key.startsWith(name + "_")) ?? (key.includes("_") ? key.slice(0, key.indexOf("_")) : key)
|
||||
const server =
|
||||
byLongest.find((name) => key.startsWith(name + "_")) ?? (key.includes("_") ? key.slice(0, key.indexOf("_")) : key)
|
||||
const local = server && key.startsWith(server + "_") ? key.slice(server.length + 1) : key
|
||||
const def = mcpDefs[key]
|
||||
const entry: CatalogEntry = {
|
||||
|
|
@ -129,7 +130,9 @@ export function buildCatalog(
|
|||
mcpDefs: Record<string, MCPToolDef>,
|
||||
servers: readonly string[],
|
||||
): CatalogEntry[] {
|
||||
return [...groupByServer(mcpTools, servers, mcpDefs).values()].flat().filter((entry) => entry.tool.execute !== undefined)
|
||||
return [...groupByServer(mcpTools, servers, mcpDefs).values()]
|
||||
.flat()
|
||||
.filter((entry) => entry.tool.execute !== undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -334,7 +337,8 @@ export const CodeModeTool = Tool.define(
|
|||
const collect = (attachment: Attachment) => void attachments.push(attachment)
|
||||
// Stream the current call list to the UI. Sent on every status change so the
|
||||
// tool part shows each child call appearing and resolving while the program runs.
|
||||
const publish = () => ctx.metadata({ title: CODE_MODE_TOOL, metadata: { toolCalls: calls.map((c) => ({ ...c })) } })
|
||||
const publish = () =>
|
||||
ctx.metadata({ title: CODE_MODE_TOOL, metadata: { toolCalls: calls.map((c) => ({ ...c })) } })
|
||||
|
||||
// One CodeMode tool per MCP tool, running the same shared middle as legacy
|
||||
// per-tool registration (McpInvoke.invoke: plugin before hook → permission
|
||||
|
|
|
|||
|
|
@ -276,7 +276,10 @@ const layer = Layer.effect(
|
|||
// fresh per turn so it tracks live tool-list changes. Hard-denied tools (the shared
|
||||
// Permission.visibleTools predicate over the agent's ruleset) never enter the
|
||||
// catalog, its inlined signatures, or the in-program search index.
|
||||
const describeCodeMode = Effect.fn("ToolRegistry.describeCodeMode")(function* (agent: Agent.Info, permission?: PermissionV1.Ruleset) {
|
||||
const describeCodeMode = Effect.fn("ToolRegistry.describeCodeMode")(function* (
|
||||
agent: Agent.Info,
|
||||
permission?: PermissionV1.Ruleset,
|
||||
) {
|
||||
const visible = Permission.visibleTools(yield* mcp.tools(), Permission.merge(agent.permission, permission ?? []))
|
||||
const servers = Object.keys(yield* mcp.clients()).map(McpCatalog.sanitize)
|
||||
return catalogInstructions(visible, yield* mcp.defs(), servers)
|
||||
|
|
@ -341,7 +344,6 @@ const layer = Layer.effect(
|
|||
}),
|
||||
)
|
||||
|
||||
|
||||
function isZodType(value: unknown): value is z.ZodType {
|
||||
return typeof value === "object" && value !== null && "_zod" in value
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,8 +96,7 @@ function resolveTools(trigger?: Plugin.Interface["trigger"]) {
|
|||
Layer.mergeAll(
|
||||
Layer.mock(Permission.Service, { ask: () => Effect.void }),
|
||||
Layer.mock(Plugin.Service, {
|
||||
trigger:
|
||||
trigger ?? (((_name, _input, output) => Effect.succeed(output)) as Plugin.Interface["trigger"]),
|
||||
trigger: trigger ?? (((_name, _input, output) => Effect.succeed(output)) as Plugin.Interface["trigger"]),
|
||||
}),
|
||||
Layer.mock(Truncate.Service, {
|
||||
output: (text: string) => Effect.succeed({ content: text, truncated: false as const }),
|
||||
|
|
|
|||
|
|
@ -21,8 +21,7 @@ import type { Tool as AITool } from "ai"
|
|||
import { Effect, Layer } from "effect"
|
||||
|
||||
// A 1x1 transparent PNG, base64-encoded, used to exercise image attachments.
|
||||
const PNG =
|
||||
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
const PNG = "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg=="
|
||||
|
||||
const SERVER = "fixtures"
|
||||
|
||||
|
|
@ -163,8 +162,8 @@ async function buildTool() {
|
|||
// this real in-memory server listed — the same snapshot shape the live service returns.
|
||||
const layer = Layer.mergeAll(
|
||||
Layer.mock(Plugin.Service, {
|
||||
trigger: (((_name: unknown, _input: unknown, output: unknown) =>
|
||||
Effect.succeed(output)) as Plugin.Interface["trigger"]),
|
||||
trigger: ((_name: unknown, _input: unknown, output: unknown) =>
|
||||
Effect.succeed(output)) as Plugin.Interface["trigger"],
|
||||
}),
|
||||
Layer.mock(Truncate.Service, {
|
||||
output: (text: string) => Effect.succeed({ content: text, truncated: false as const }),
|
||||
|
|
@ -196,9 +195,7 @@ describe("code mode integration (real MCP server)", () => {
|
|||
test("the appended catalog inlines full signatures with real MCP schemas", () => {
|
||||
expect(description).toContain("Available tools (COMPLETE list")
|
||||
expect(description).toContain("- fixtures (4 tools)")
|
||||
expect(description).toContain(
|
||||
"tools.fixtures.add(input: { a: number; b: number }): Promise<{ sum: number }>",
|
||||
)
|
||||
expect(description).toContain("tools.fixtures.add(input: { a: number; b: number }): Promise<{ sum: number }>")
|
||||
expect(description).toContain("tools.fixtures.get_text(input: { name: string }): Promise<unknown>")
|
||||
expect(description).toContain("// Add two numbers and return the structured sum")
|
||||
// Small catalog: everything is inline, so no discovery tool is advertised.
|
||||
|
|
|
|||
|
|
@ -193,7 +193,9 @@ describe("code mode execute", () => {
|
|||
// never cherry-picks a catalog tool or fabricates result fields.
|
||||
expect(description).toContain("## Workflow")
|
||||
expect(description).toContain("1. Pick a tool from the list under `## Available tools`")
|
||||
expect(description).toContain('`const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string')
|
||||
expect(description).toContain(
|
||||
'`const data = typeof res === "string" ? JSON.parse(res) : res` — most tools return JSON as a string',
|
||||
)
|
||||
expect(description).toContain("Return only the fields you need")
|
||||
expect(description).not.toContain("total_count")
|
||||
})
|
||||
|
|
@ -249,7 +251,9 @@ describe("code mode execute", () => {
|
|||
expect(description).toContain("tools.$codemode.search(")
|
||||
// PARTIAL catalogs put search first in the workflow and advertise namespace browsing.
|
||||
expect(description).toContain("1. Find a tool (skip when it is already listed below)")
|
||||
expect(description).toContain('- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.')
|
||||
expect(description).toContain(
|
||||
'- Browse one namespace: `await tools.$codemode.search({ query: "", namespace: "<name>" })`.',
|
||||
)
|
||||
expect(description).not.toContain("total_count")
|
||||
// All op lines cost the same estimated tokens (chars/4 rounds away the 1- vs 3-digit
|
||||
// name difference), so the path tiebreak decides: the lexicographically-first ops made
|
||||
|
|
@ -290,7 +294,10 @@ describe("code mode execute", () => {
|
|||
linear_search: mcpTool("search", () => ""),
|
||||
})
|
||||
const output = await Effect.runPromise(
|
||||
tool.execute({ code: "const namespaces = Object.keys(tools); return { namespaces, count: namespaces.length }" }, ctx),
|
||||
tool.execute(
|
||||
{ code: "const namespaces = Object.keys(tools); return { namespaces, count: namespaces.length }" },
|
||||
ctx,
|
||||
),
|
||||
)
|
||||
expect(JSON.parse(output.output)).toEqual({ namespaces: ["github", "linear"], count: 2 })
|
||||
})
|
||||
|
|
@ -565,9 +572,7 @@ describe("code mode execute", () => {
|
|||
const tool = await build({
|
||||
shot_take: mcpTool("take", () => ({ content: [{ type: "image", data: "PNGDATA", mimeType: "image/png" }] })),
|
||||
})
|
||||
const out = await Effect.runPromise(
|
||||
tool.execute({ code: "await tools.shot.take({}); return 'captured'" }, ctx),
|
||||
)
|
||||
const out = await Effect.runPromise(tool.execute({ code: "await tools.shot.take({}); return 'captured'" }, ctx))
|
||||
expect(out.output).toBe("captured")
|
||||
expect(out.attachments).toHaveLength(1)
|
||||
})
|
||||
|
|
@ -690,9 +695,7 @@ describe("code mode permission visibility", () => {
|
|||
expect(called).toEqual([])
|
||||
|
||||
// The rest of the namespace still works.
|
||||
const allowed = await Effect.runPromise(
|
||||
tool.execute({ code: "return await tools.github.list_issues({})" }, ctx),
|
||||
)
|
||||
const allowed = await Effect.runPromise(tool.execute({ code: "return await tools.github.list_issues({})" }, ctx))
|
||||
expect(allowed.metadata.error).toBeUndefined()
|
||||
expect(allowed.output).toBe("ok")
|
||||
})
|
||||
|
|
@ -706,9 +709,7 @@ describe("code mode permission visibility", () => {
|
|||
["github"],
|
||||
[askRule("github_list_issues")],
|
||||
)
|
||||
const out = await Effect.runPromise(
|
||||
tool.execute({ code: "return await tools.github.list_issues({})" }, askCtx),
|
||||
)
|
||||
const out = await Effect.runPromise(tool.execute({ code: "return await tools.github.list_issues({})" }, askCtx))
|
||||
expect(out.output).toBe("ok")
|
||||
expect(asked).toEqual(["github_list_issues"])
|
||||
})
|
||||
|
|
@ -733,16 +734,21 @@ describe("toSandboxResult", () => {
|
|||
|
||||
test("prefers structuredContent over text", () => {
|
||||
const { collect } = collector()
|
||||
expect(toSandboxResult({ structuredContent: { x: 1 }, content: [{ type: "text", text: "hi" }] }, collect)).toEqual(
|
||||
{ x: 1 },
|
||||
)
|
||||
expect(toSandboxResult({ structuredContent: { x: 1 }, content: [{ type: "text", text: "hi" }] }, collect)).toEqual({
|
||||
x: 1,
|
||||
})
|
||||
})
|
||||
|
||||
test("joins text content when no structured content is present", () => {
|
||||
const { collect } = collector()
|
||||
expect(
|
||||
toSandboxResult(
|
||||
{ content: [{ type: "text", text: "one" }, { type: "text", text: "two" }] },
|
||||
{
|
||||
content: [
|
||||
{ type: "text", text: "one" },
|
||||
{ type: "text", text: "two" },
|
||||
],
|
||||
},
|
||||
collect,
|
||||
),
|
||||
).toBe("one\ntwo")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue