refactor(tools): unify tool APIs and result handling (#38367)
This commit is contained in:
parent
8cac010bac
commit
79c1544072
133 changed files with 3602 additions and 2770 deletions
|
|
@ -28,7 +28,7 @@ export type TurnControl = {
|
|||
type ToolState = {
|
||||
readonly name: string
|
||||
input: ToolInput
|
||||
structured: Record<string, unknown>
|
||||
metadata: Record<string, unknown>
|
||||
content: ToolContent
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export type TurnStart =
|
|||
| { readonly type: "compaction"; readonly id: string }
|
||||
|
||||
function emptyToolState(): ToolState {
|
||||
return { name: "tool", input: {}, structured: {}, content: [] }
|
||||
return { name: "tool", input: {}, metadata: {}, content: [] }
|
||||
}
|
||||
|
||||
export async function streamTurn(input: {
|
||||
|
|
@ -120,7 +120,7 @@ export async function streamTurn(input: {
|
|||
}
|
||||
if (event.type === "session.tool.input.started") {
|
||||
assistantMessageID = event.data.assistantMessageID
|
||||
tools.set(event.data.callID, { name: event.data.name, input: {}, structured: {}, content: [] })
|
||||
tools.set(event.data.callID, { name: event.data.name, input: {}, metadata: {}, content: [] })
|
||||
await update({
|
||||
sessionUpdate: "tool_call",
|
||||
...pendingToolCall({
|
||||
|
|
@ -151,15 +151,13 @@ export async function streamTurn(input: {
|
|||
if (event.type === "session.tool.progress") {
|
||||
const current = tools.get(event.data.callID)
|
||||
if (!current) continue
|
||||
current.structured = event.data.structured
|
||||
current.content = event.data.content
|
||||
current.metadata = event.data.metadata
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
...runningToolUpdate({
|
||||
toolCallId: event.data.callID,
|
||||
toolName: current.name,
|
||||
state: { input: current.input },
|
||||
content: current.content,
|
||||
cwd: input.cwd,
|
||||
}),
|
||||
})
|
||||
|
|
@ -175,7 +173,7 @@ export async function streamTurn(input: {
|
|||
cwd: input.cwd,
|
||||
toolName: current.name,
|
||||
toolInput: current.input,
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata ?? {},
|
||||
}).catch(() => {})
|
||||
await update({
|
||||
sessionUpdate: "tool_call_update",
|
||||
|
|
@ -183,9 +181,8 @@ export async function streamTurn(input: {
|
|||
toolCallId: event.data.callID,
|
||||
toolName: current.name,
|
||||
input: current.input,
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
}),
|
||||
})
|
||||
continue
|
||||
|
|
@ -199,7 +196,7 @@ export async function streamTurn(input: {
|
|||
toolCallId: event.data.callID,
|
||||
toolName: current.name,
|
||||
input: current.input,
|
||||
structured: event.data.metadata ?? current.structured,
|
||||
metadata: event.data.metadata ?? current.metadata,
|
||||
content: event.data.content ?? current.content,
|
||||
error: event.data.error.message,
|
||||
cwd: input.cwd,
|
||||
|
|
@ -342,9 +339,8 @@ async function replayMessage(
|
|||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
input: part.state.input,
|
||||
structured: part.state.structured,
|
||||
metadata: part.state.metadata,
|
||||
content: part.state.content,
|
||||
result: part.state.result,
|
||||
}),
|
||||
},
|
||||
})
|
||||
|
|
@ -358,7 +354,6 @@ async function replayMessage(
|
|||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
state: { input: part.state.input },
|
||||
content: part.state.content,
|
||||
cwd,
|
||||
}),
|
||||
},
|
||||
|
|
@ -373,7 +368,7 @@ async function replayMessage(
|
|||
toolCallId: part.id,
|
||||
toolName: part.name,
|
||||
input: part.state.input,
|
||||
structured: part.state.structured,
|
||||
metadata: part.state.metadata,
|
||||
content: part.state.content,
|
||||
error: part.state.error.message,
|
||||
cwd,
|
||||
|
|
|
|||
|
|
@ -58,11 +58,11 @@ export async function syncEditedFiles(input: {
|
|||
readonly cwd: string
|
||||
readonly toolName: string
|
||||
readonly toolInput: ToolInput
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
readonly metadata: Readonly<Record<string, unknown>>
|
||||
}) {
|
||||
if (!input.writeTextFile || !input.connection.writeTextFile || toToolKind(input.toolName) !== "edit") return
|
||||
const files = Array.isArray(input.structured.files)
|
||||
? input.structured.files.flatMap((file): string[] => {
|
||||
const files = Array.isArray(input.metadata.files)
|
||||
? input.metadata.files.flatMap((file): string[] => {
|
||||
if (!file || typeof file !== "object") return []
|
||||
const path = Reflect.get(file, "file")
|
||||
return typeof path === "string" ? [path] : []
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { isAbsolute, resolve } from "node:path"
|
||||
import type { ToolCall, ToolCallContent, ToolCallLocation, ToolCallUpdate, ToolKind } from "@agentclientprotocol/sdk"
|
||||
import { readDisplayText } from "@opencode-ai/tui/mini/tool"
|
||||
|
||||
export type ToolInput = Record<string, unknown>
|
||||
export type ToolContent = ReadonlyArray<
|
||||
|
|
@ -100,11 +101,12 @@ export function completedToolUpdate(input: {
|
|||
readonly toolName: string
|
||||
readonly input: ToolInput
|
||||
readonly content: ToolContent
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
readonly result?: unknown
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
}): ToolCallUpdate {
|
||||
const normalized = toolContent(input.content)
|
||||
const read = input.toolName.toLocaleLowerCase() === "read" ? readDisplayText(input.structured) : undefined
|
||||
// Read's model content is a JSON page envelope; show the clean text instead.
|
||||
const firstText = input.content.find((part) => part.type === "text")
|
||||
const read = input.toolName.toLocaleLowerCase() === "read" && firstText ? readDisplayText(firstText.text) : undefined
|
||||
const images = normalized.filter((part) => part.type === "content" && part.content.type === "image")
|
||||
const primary =
|
||||
read === undefined
|
||||
|
|
@ -128,8 +130,7 @@ export function completedToolUpdate(input: {
|
|||
status: "completed",
|
||||
content: [...primary, ...diff, ...images],
|
||||
rawOutput: {
|
||||
structured: input.structured,
|
||||
...(input.result === undefined ? {} : { result: input.result }),
|
||||
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -138,8 +139,8 @@ export function errorToolUpdate(input: {
|
|||
readonly toolCallId: string
|
||||
readonly toolName: string
|
||||
readonly input: ToolInput
|
||||
readonly content: ToolContent
|
||||
readonly structured: Readonly<Record<string, unknown>>
|
||||
readonly content?: ToolContent
|
||||
readonly metadata?: Readonly<Record<string, unknown>>
|
||||
readonly error: string
|
||||
readonly cwd?: string
|
||||
}): ToolCallUpdate {
|
||||
|
|
@ -150,8 +151,11 @@ export function errorToolUpdate(input: {
|
|||
title: toolTitle(input.toolName, input.input, undefined),
|
||||
locations: toLocations(input.toolName, input.input, input.cwd),
|
||||
rawInput: rawInput(input.toolName, input.input, input.cwd),
|
||||
content: [...toolContent(input.content), { type: "content", content: { type: "text", text: input.error } }],
|
||||
rawOutput: { structured: input.structured, error: input.error },
|
||||
content: [...toolContent(input.content ?? []), { type: "content", content: { type: "text", text: input.error } }],
|
||||
rawOutput: {
|
||||
...(input.metadata === undefined ? {} : { metadata: input.metadata }),
|
||||
error: input.error,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -164,21 +168,6 @@ function toolContent(content: ToolContent): ToolCallContent[] {
|
|||
})
|
||||
}
|
||||
|
||||
function readDisplayText(structured: Readonly<Record<string, unknown>>) {
|
||||
if (typeof structured.content === "string") {
|
||||
if (structured.type === "text-page" || structured.encoding === "utf8") return structured.content
|
||||
}
|
||||
if (!Array.isArray(structured.entries)) return undefined
|
||||
return structured.entries
|
||||
.flatMap((entry): string[] => {
|
||||
if (typeof entry === "string") return [entry]
|
||||
if (!entry || typeof entry !== "object") return []
|
||||
const path = Reflect.get(entry, "path")
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function toolTitle(toolName: string, input: ToolInput, fallback: string | undefined) {
|
||||
if (isShell(toolName)) return stringValue(input.command) ?? stringValue(input.cmd) ?? fallback ?? toolName
|
||||
return fallback || toolName
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ import {
|
|||
Reference,
|
||||
Skill,
|
||||
} from "@opencode-ai/plugin/v2"
|
||||
import { Tool } from "@opencode-ai/plugin/v2/tool"
|
||||
|
||||
const key = Symbol.for("opencode.plugin.v2.promise")
|
||||
;(globalThis as typeof globalThis & { [key]?: unknown })[key] = {
|
||||
|
|
@ -23,4 +24,5 @@ const key = Symbol.for("opencode.plugin.v2.promise")
|
|||
Provider,
|
||||
Reference,
|
||||
Skill,
|
||||
Tool,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import type {
|
|||
import { SessionMessage } from "@opencode-ai/schema/session-message"
|
||||
import { EOL } from "node:os"
|
||||
import { readFile } from "node:fs/promises"
|
||||
import { toolOutputText, type MiniToolPart } from "@opencode-ai/tui/mini/tool"
|
||||
import { nonEmptyToolContent, toolOutputText, type MiniToolPart } from "@opencode-ai/tui/mini/tool"
|
||||
import { UI } from "./ui"
|
||||
|
||||
type Model = {
|
||||
|
|
@ -55,7 +55,7 @@ type ToolState = StartedPart & {
|
|||
raw?: string
|
||||
provider?: unknown
|
||||
providerState?: SessionMessageAssistantTool["providerState"]
|
||||
structured: Record<string, JsonValue>
|
||||
metadata: Record<string, JsonValue>
|
||||
content: LLMToolContent[]
|
||||
}
|
||||
|
||||
|
|
@ -306,7 +306,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
assistantMessageID: event.data.assistantMessageID,
|
||||
tool: event.data.name,
|
||||
input: {},
|
||||
structured: {},
|
||||
metadata: {},
|
||||
content: [],
|
||||
})
|
||||
continue
|
||||
|
|
@ -334,7 +334,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
raw: current?.raw,
|
||||
provider: { executed: event.data.executed, state: event.data.state },
|
||||
providerState: event.data.state,
|
||||
structured: {},
|
||||
metadata: {},
|
||||
content: [],
|
||||
})
|
||||
continue
|
||||
|
|
@ -342,8 +342,7 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
if (event.type === "session.tool.progress") {
|
||||
const current = tools.get(toolKey(event.data.assistantMessageID, event.data.callID))
|
||||
if (current) {
|
||||
current.structured = event.data.structured
|
||||
current.content = event.data.content
|
||||
current.metadata = event.data.metadata
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
|
@ -360,9 +359,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
state: {
|
||||
status: "completed",
|
||||
input: current.input,
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: { created: current.timestamp, ran: current.timestamp, completed: time },
|
||||
}
|
||||
|
|
@ -379,9 +377,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
output: toolOutputText(current.tool, event.data.content),
|
||||
title: current.tool,
|
||||
metadata: {
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
providerCall: current.provider,
|
||||
providerResult: { executed: event.data.executed, state: event.data.resultState },
|
||||
rawInput: current.raw,
|
||||
|
|
@ -398,8 +395,8 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
const key = toolKey(event.data.assistantMessageID, event.data.callID)
|
||||
const current = tools.get(key) ?? fallbackTool(event)
|
||||
const error = event.data.error.message
|
||||
const structured = event.data.metadata ?? current.structured
|
||||
const content = event.data.content ?? current.content
|
||||
const metadata = event.data.metadata ?? current.metadata
|
||||
const content = event.data.content ?? nonEmptyToolContent(current.content)
|
||||
const tool: SessionMessageAssistantTool = {
|
||||
type: "tool",
|
||||
id: event.data.callID,
|
||||
|
|
@ -410,10 +407,9 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
state: {
|
||||
status: "error",
|
||||
input: current.input,
|
||||
structured,
|
||||
metadata,
|
||||
content,
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: { created: current.timestamp, ran: current.timestamp, completed: time },
|
||||
}
|
||||
|
|
@ -429,7 +425,6 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
input: current.input,
|
||||
error,
|
||||
metadata: {
|
||||
result: event.data.result,
|
||||
providerCall: current.provider,
|
||||
providerResult: { executed: event.data.executed, state: event.data.resultState },
|
||||
rawInput: current.raw,
|
||||
|
|
@ -441,15 +436,14 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
renderedTools.add(key)
|
||||
if (input.compatibility === "v1" && (permissionRejected || formCancelled)) continue
|
||||
if (!emit("tool_use", time, { part })) {
|
||||
if (toolOutputText(current.tool, content).trim())
|
||||
if (content && toolOutputText(current.tool, content).trim())
|
||||
await input.renderTool({
|
||||
...tool,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: current.input,
|
||||
structured,
|
||||
metadata,
|
||||
content,
|
||||
result: event.data.result,
|
||||
},
|
||||
})
|
||||
await input.renderToolError(tool)
|
||||
|
|
@ -597,14 +591,14 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
input: item.state.input,
|
||||
output: toolOutputText(item.name, item.state.content),
|
||||
title: item.name,
|
||||
metadata: { structured: item.state.structured, content: item.state.content, result: item.state.result },
|
||||
metadata: { metadata: item.state.metadata, content: item.state.content },
|
||||
time: { start: item.time.ran ?? item.time.created, end: item.time.completed ?? timestamp },
|
||||
}
|
||||
: {
|
||||
status: "error",
|
||||
input: item.state.input,
|
||||
error: item.state.error.message,
|
||||
metadata: { structured: item.state.structured, content: item.state.content, result: item.state.result },
|
||||
metadata: { metadata: item.state.metadata, content: item.state.content },
|
||||
time: { start: item.time.ran ?? item.time.created, end: item.time.completed ?? timestamp },
|
||||
},
|
||||
}
|
||||
|
|
@ -614,8 +608,16 @@ export async function runNonInteractivePrompt(input: Input) {
|
|||
await input.renderTool(item)
|
||||
continue
|
||||
}
|
||||
if (toolOutputText(item.name, item.state.content).trim()) {
|
||||
await input.renderTool({ ...item, state: { ...item.state, status: "completed" } })
|
||||
if (item.state.content && toolOutputText(item.name, item.state.content).trim()) {
|
||||
await input.renderTool({
|
||||
...item,
|
||||
state: {
|
||||
status: "completed",
|
||||
input: item.state.input,
|
||||
metadata: item.state.metadata,
|
||||
content: item.state.content,
|
||||
},
|
||||
})
|
||||
}
|
||||
await input.renderToolError(item)
|
||||
UI.error(item.state.error.message)
|
||||
|
|
@ -792,7 +794,7 @@ function fallbackTool(event: {
|
|||
assistantMessageID: event.data.assistantMessageID,
|
||||
tool: "tool",
|
||||
input: {},
|
||||
structured: {},
|
||||
metadata: {},
|
||||
content: [],
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue