feat(cli): Support debug tool calling directly in CLI. (#6564)
This commit is contained in:
@@ -1,6 +1,14 @@
|
|||||||
import { EOL } from "os"
|
import { EOL } from "os"
|
||||||
import { basename } from "path"
|
import { basename } from "path"
|
||||||
import { Agent } from "../../../agent/agent"
|
import { Agent } from "../../../agent/agent"
|
||||||
|
import { Provider } from "../../../provider/provider"
|
||||||
|
import { Session } from "../../../session"
|
||||||
|
import type { MessageV2 } from "../../../session/message-v2"
|
||||||
|
import { Identifier } from "../../../id/id"
|
||||||
|
import { ToolRegistry } from "../../../tool/registry"
|
||||||
|
import { Instance } from "../../../project/instance"
|
||||||
|
import { PermissionNext } from "../../../permission/next"
|
||||||
|
import { iife } from "../../../util/iife"
|
||||||
import { bootstrap } from "../../bootstrap"
|
import { bootstrap } from "../../bootstrap"
|
||||||
import { cmd } from "../cmd"
|
import { cmd } from "../cmd"
|
||||||
|
|
||||||
@@ -8,11 +16,20 @@ export const AgentCommand = cmd({
|
|||||||
command: "agent <name>",
|
command: "agent <name>",
|
||||||
describe: "show agent configuration details",
|
describe: "show agent configuration details",
|
||||||
builder: (yargs) =>
|
builder: (yargs) =>
|
||||||
yargs.positional("name", {
|
yargs
|
||||||
type: "string",
|
.positional("name", {
|
||||||
demandOption: true,
|
type: "string",
|
||||||
description: "Agent name",
|
demandOption: true,
|
||||||
}),
|
description: "Agent name",
|
||||||
|
})
|
||||||
|
.option("tool", {
|
||||||
|
type: "string",
|
||||||
|
description: "Tool id to execute",
|
||||||
|
})
|
||||||
|
.option("params", {
|
||||||
|
type: "string",
|
||||||
|
description: "Tool params as JSON or a JS object literal",
|
||||||
|
}),
|
||||||
async handler(args) {
|
async handler(args) {
|
||||||
await bootstrap(process.cwd(), async () => {
|
await bootstrap(process.cwd(), async () => {
|
||||||
const agentName = args.name as string
|
const agentName = args.name as string
|
||||||
@@ -23,7 +40,127 @@ export const AgentCommand = cmd({
|
|||||||
)
|
)
|
||||||
process.exit(1)
|
process.exit(1)
|
||||||
}
|
}
|
||||||
process.stdout.write(JSON.stringify(agent, null, 2) + EOL)
|
const availableTools = await getAvailableTools(agent)
|
||||||
|
const resolvedTools = await resolveTools(agent, availableTools)
|
||||||
|
const toolID = args.tool as string | undefined
|
||||||
|
if (toolID) {
|
||||||
|
const tool = availableTools.find((item) => item.id === toolID)
|
||||||
|
if (!tool) {
|
||||||
|
process.stderr.write(`Tool ${toolID} not found for agent ${agentName}` + EOL)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
if (resolvedTools[toolID] === false) {
|
||||||
|
process.stderr.write(`Tool ${toolID} is disabled for agent ${agentName}` + EOL)
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
const params = parseToolParams(args.params as string | undefined)
|
||||||
|
const ctx = await createToolContext(agent)
|
||||||
|
const result = await tool.execute(params, ctx)
|
||||||
|
process.stdout.write(JSON.stringify({ tool: toolID, input: params, result }, null, 2) + EOL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const output = {
|
||||||
|
...agent,
|
||||||
|
tools: resolvedTools,
|
||||||
|
}
|
||||||
|
process.stdout.write(JSON.stringify(output, null, 2) + EOL)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function getAvailableTools(agent: Agent.Info) {
|
||||||
|
const providerID = agent.model?.providerID ?? (await Provider.defaultModel()).providerID
|
||||||
|
return ToolRegistry.tools(providerID, agent)
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveTools(agent: Agent.Info, availableTools: Awaited<ReturnType<typeof getAvailableTools>>) {
|
||||||
|
const disabled = PermissionNext.disabled(
|
||||||
|
availableTools.map((tool) => tool.id),
|
||||||
|
agent.permission,
|
||||||
|
)
|
||||||
|
const resolved: Record<string, boolean> = {}
|
||||||
|
for (const tool of availableTools) {
|
||||||
|
resolved[tool.id] = !disabled.has(tool.id)
|
||||||
|
}
|
||||||
|
return resolved
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseToolParams(input?: string) {
|
||||||
|
if (!input) return {}
|
||||||
|
const trimmed = input.trim()
|
||||||
|
if (trimmed.length === 0) return {}
|
||||||
|
|
||||||
|
const parsed = iife(() => {
|
||||||
|
try {
|
||||||
|
return JSON.parse(trimmed)
|
||||||
|
} catch (jsonError) {
|
||||||
|
try {
|
||||||
|
return new Function(`return (${trimmed})`)()
|
||||||
|
} catch (evalError) {
|
||||||
|
throw new Error(
|
||||||
|
`Failed to parse --params. Use JSON or a JS object literal. JSON error: ${jsonError}. Eval error: ${evalError}.`,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
||||||
|
throw new Error("Tool params must be an object.")
|
||||||
|
}
|
||||||
|
return parsed as Record<string, unknown>
|
||||||
|
}
|
||||||
|
|
||||||
|
async function createToolContext(agent: Agent.Info) {
|
||||||
|
const session = await Session.create({ title: `Debug tool run (${agent.name})` })
|
||||||
|
const messageID = Identifier.ascending("message")
|
||||||
|
const model = agent.model ?? (await Provider.defaultModel())
|
||||||
|
const now = Date.now()
|
||||||
|
const message: MessageV2.Assistant = {
|
||||||
|
id: messageID,
|
||||||
|
sessionID: session.id,
|
||||||
|
role: "assistant",
|
||||||
|
time: {
|
||||||
|
created: now,
|
||||||
|
},
|
||||||
|
parentID: messageID,
|
||||||
|
modelID: model.modelID,
|
||||||
|
providerID: model.providerID,
|
||||||
|
mode: "debug",
|
||||||
|
agent: agent.name,
|
||||||
|
path: {
|
||||||
|
cwd: Instance.directory,
|
||||||
|
root: Instance.worktree,
|
||||||
|
},
|
||||||
|
cost: 0,
|
||||||
|
tokens: {
|
||||||
|
input: 0,
|
||||||
|
output: 0,
|
||||||
|
reasoning: 0,
|
||||||
|
cache: {
|
||||||
|
read: 0,
|
||||||
|
write: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
await Session.updateMessage(message)
|
||||||
|
|
||||||
|
const ruleset = PermissionNext.merge(agent.permission, session.permission ?? [])
|
||||||
|
|
||||||
|
return {
|
||||||
|
sessionID: session.id,
|
||||||
|
messageID,
|
||||||
|
callID: Identifier.ascending("part"),
|
||||||
|
agent: agent.name,
|
||||||
|
abort: new AbortController().signal,
|
||||||
|
metadata: () => {},
|
||||||
|
async ask(req: Omit<PermissionNext.Request, "id" | "sessionID" | "tool">) {
|
||||||
|
for (const pattern of req.patterns) {
|
||||||
|
const rule = PermissionNext.evaluate(req.permission, pattern, ruleset)
|
||||||
|
if (rule.action === "deny") {
|
||||||
|
throw new PermissionNext.DeniedError(ruleset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user