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
|
|
@ -342,13 +342,13 @@ function make(state: State, tool: string, input: Record<string, JsonValue>): Ref
|
|||
}
|
||||
}
|
||||
|
||||
function startTool(state: State, ref: Ref, structured: Record<string, JsonValue> = {}): SessionMessageAssistantTool {
|
||||
function startTool(state: State, ref: Ref, metadata: Record<string, JsonValue> = {}): SessionMessageAssistantTool {
|
||||
state.started.add(ref.call)
|
||||
const part = {
|
||||
type: "tool" as const,
|
||||
id: ref.call,
|
||||
name: ref.tool,
|
||||
state: { status: "running" as const, input: ref.input, structured, content: [] },
|
||||
state: { status: "running" as const, input: ref.input, metadata },
|
||||
time: { created: ref.start, ran: ref.start },
|
||||
}
|
||||
present(state, [toolCommit(part, ref.msg, "start")])
|
||||
|
|
@ -395,8 +395,8 @@ function doneTool(
|
|||
state: {
|
||||
status: "completed",
|
||||
input: ref.input,
|
||||
content: output.output ? [{ type: "text", text: output.output }] : [],
|
||||
structured: output.metadata ?? {},
|
||||
content: [{ type: "text", text: output.output }],
|
||||
metadata: output.metadata,
|
||||
},
|
||||
time: { created: ref.start, ran: ref.start, completed: Date.now() },
|
||||
}
|
||||
|
|
@ -415,8 +415,6 @@ function failTool(state: State, ref: Ref, error: string): void {
|
|||
status: "error",
|
||||
input: ref.input,
|
||||
error: { type: "unknown", message: error },
|
||||
structured: {},
|
||||
content: [],
|
||||
},
|
||||
time: { created: ref.start, ran: ref.start, completed: Date.now() },
|
||||
},
|
||||
|
|
@ -527,8 +525,7 @@ function emitTask(state: State): void {
|
|||
offset: 1,
|
||||
limit: 200,
|
||||
},
|
||||
structured: {},
|
||||
content: [],
|
||||
metadata: {},
|
||||
},
|
||||
time: { created: Date.now(), ran: Date.now() },
|
||||
} satisfies SessionMessageAssistantTool
|
||||
|
|
|
|||
|
|
@ -53,7 +53,7 @@ export function permissionInfo(request: MiniPermissionRequest, directory?: strin
|
|||
resources: request.resources,
|
||||
metadata: request.metadata,
|
||||
input: state?.status === "streaming" ? undefined : state?.input,
|
||||
structured: state?.status === "streaming" ? undefined : state?.structured,
|
||||
toolMetadata: state?.status === "streaming" ? undefined : state?.metadata,
|
||||
},
|
||||
(value) => toolPath(value, { home: true, directory }),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
// Current-native subagent (child Session) tracking for the mini transport.
|
||||
//
|
||||
// Discovers child Sessions of the active parent from four current sources:
|
||||
// 1. projected subagent tool output (`structured.sessionID`) during hydration
|
||||
// 1. projected subagent tool output (`metadata.sessionID`) during hydration
|
||||
// 2. the current session list filtered by `parentID` during hydration
|
||||
// 3. the process-local active-session map during hydration
|
||||
// 4. live events from unknown sessions whose `parentID` matches the parent
|
||||
|
|
@ -33,6 +33,7 @@ import type {
|
|||
StreamCommit,
|
||||
} from "./types"
|
||||
import { canonicalToolName, normalizeTool, toolOutputText, toolView } from "./tool"
|
||||
import { toolDisplayContent } from "../util/tool-display"
|
||||
|
||||
const CHILD_MESSAGE_LIMIT = 80
|
||||
const CHILD_FRAME_LIMIT = 80
|
||||
|
|
@ -55,7 +56,7 @@ export function toolCommit(
|
|||
): StreamCommit {
|
||||
const part = normalizeTool(input)
|
||||
const status = part.state.status
|
||||
const output = status === "streaming" ? "" : toolOutputText(part.name, part.state.content)
|
||||
const output = status === "streaming" ? "" : toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
const partial = status === "error" && phase === "progress" && value !== undefined
|
||||
const text =
|
||||
status === "running" || partial
|
||||
|
|
@ -178,10 +179,10 @@ function blockerCategory(event: V2Event): "permission" | "form" | undefined {
|
|||
if (event.type === "form.created" || event.type === "form.replied" || event.type === "form.cancelled") return "form"
|
||||
}
|
||||
|
||||
function childSessionID(structured: Record<string, unknown> | undefined) {
|
||||
const sessionID = text(structured?.sessionID)
|
||||
function childSessionID(metadata: Record<string, unknown> | undefined) {
|
||||
const sessionID = text(metadata?.sessionID)
|
||||
if (!sessionID || !sessionID.startsWith("ses")) return undefined
|
||||
const status = structured?.status
|
||||
const status = metadata?.status
|
||||
if (status !== "running" && status !== "completed") return undefined
|
||||
return { sessionID, running: status === "running" }
|
||||
}
|
||||
|
|
@ -199,7 +200,7 @@ function tab(child: ChildState): FooterSubagentTab {
|
|||
|
||||
export function createSubagentTracker(input: SubagentTrackerInput): SubagentTracker {
|
||||
const children = new Map<string, ChildState>()
|
||||
// Live subagent tool calls in the parent, so tool.success structured output
|
||||
// Live subagent tool calls in the parent, so tool.success metadata
|
||||
// can be joined with the call's input metadata.
|
||||
const pendingCalls = new Map<string, Record<string, unknown>>()
|
||||
// Recently resolved non-family sessions. Retention is bounded so unrelated
|
||||
|
|
@ -309,7 +310,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
return
|
||||
}
|
||||
const current = child.tools.get(key)
|
||||
const output = toolOutputText(part.name, part.state.content)
|
||||
const output = toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
if (part.state.status === "running") {
|
||||
if (!current || current.part.state.status === "streaming")
|
||||
setFrame(child, frame, toolCommit(part, messageID, "start", undefined, input.directory))
|
||||
|
|
@ -779,7 +780,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
state: { status: "running", input: event.data.input, metadata: {} },
|
||||
time: { created: current?.part.time.created ?? event.created, ran: event.created },
|
||||
},
|
||||
event.data.assistantMessageID,
|
||||
|
|
@ -804,8 +805,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
state: {
|
||||
status: "running",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
content: event.data.content,
|
||||
metadata: event.data.metadata,
|
||||
},
|
||||
time: {
|
||||
created: part?.time.created ?? event.created,
|
||||
|
|
@ -837,18 +837,15 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
? {
|
||||
status: "error",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured:
|
||||
event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}),
|
||||
content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []),
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
}
|
||||
: {
|
||||
status: "completed",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: {
|
||||
created: part?.time.created ?? event.created,
|
||||
|
|
@ -921,7 +918,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
const mainTool = (item: SessionMessageAssistantTool, active?: Record<string, unknown>) => {
|
||||
const tool = normalizeTool(item)
|
||||
if (tool.name !== "subagent" || tool.state.status === "streaming") return
|
||||
const found = childSessionID(record(tool.state.structured))
|
||||
const found = childSessionID(record(tool.state.metadata))
|
||||
if (!found) return
|
||||
const child = admitChild(found.sessionID)
|
||||
if (!child) return
|
||||
|
|
@ -967,7 +964,7 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
const key = sourceKey(event.data.assistantMessageID, event.data.callID)
|
||||
const pending = pendingCalls.get(key)
|
||||
if (event.type !== "session.tool.progress") pendingCalls.delete(key)
|
||||
const found = childSessionID(record(event.type === "session.tool.failed" ? event.data.metadata : event.data.structured))
|
||||
const found = childSessionID(record(event.data.metadata))
|
||||
if (!found) return
|
||||
const child = admitChild(found.sessionID)
|
||||
if (!child) return
|
||||
|
|
@ -1085,11 +1082,13 @@ export function createSubagentTracker(input: SubagentTrackerInput): SubagentTrac
|
|||
input.emit()
|
||||
},
|
||||
snapshot() {
|
||||
const tabs = [...children.values()].toSorted((a, b) => {
|
||||
const active = Number(b.status === "running") - Number(a.status === "running")
|
||||
if (active !== 0) return active
|
||||
return b.lastUpdatedAt - a.lastUpdatedAt
|
||||
}).map(tab)
|
||||
const tabs = [...children.values()]
|
||||
.toSorted((a, b) => {
|
||||
const active = Number(b.status === "running") - Number(a.status === "running")
|
||||
if (active !== 0) return active
|
||||
return b.lastUpdatedAt - a.lastUpdatedAt
|
||||
})
|
||||
.map(tab)
|
||||
const child = selected ? children.get(selected) : undefined
|
||||
const details: Record<string, FooterSubagentDetail> =
|
||||
child && !child.detailStale ? { [child.sessionID]: { commits: child.frames.map((item) => item.commit) } } : {}
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { writeSessionOutput } from "./stream"
|
|||
import { createFragmentReconciler, fragmentRef, type FragmentReconciler } from "./stream-v2.fragment"
|
||||
import { createSubagentTracker, toolCommit, toolFinalPhase } from "./stream-v2.subagent"
|
||||
import { normalizeTool, toolOutputText } from "./tool"
|
||||
import { toolDisplayContent } from "../util/tool-display"
|
||||
import type {
|
||||
FooterApi,
|
||||
FooterView,
|
||||
|
|
@ -558,7 +559,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
return
|
||||
}
|
||||
const current = state.tools.get(key)
|
||||
const output = toolOutputText(part.name, part.state.content)
|
||||
const output = toolOutputText(part.name, toolDisplayContent(part.state))
|
||||
const prefix = current ? output.startsWith(current.output) : false
|
||||
const version = current && !prefix ? current.version + 1 : (current?.version ?? 0)
|
||||
const delta = current && prefix ? output.slice(current.output.length) : output
|
||||
|
|
@ -670,8 +671,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
{
|
||||
kind: "reasoning",
|
||||
source: "reasoning",
|
||||
text:
|
||||
update.previous.length === 0 ? `Thinking: ${item.text}` : item.text.slice(update.previous.length),
|
||||
text: update.previous.length === 0 ? `Thinking: ${item.text}` : item.text.slice(update.previous.length),
|
||||
phase: "progress",
|
||||
messageID: message.id,
|
||||
partID: fragment.partID,
|
||||
|
|
@ -1042,7 +1042,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
name: current?.part.name ?? "tool",
|
||||
executed: event.data.executed,
|
||||
providerState: event.data.state,
|
||||
state: { status: "running", input: event.data.input, structured: {}, content: [] },
|
||||
state: { status: "running", input: event.data.input, metadata: {} },
|
||||
time: { created: current?.part.time.created ?? event.created, ran: event.created },
|
||||
}
|
||||
renderTool(event.data.assistantMessageID, item)
|
||||
|
|
@ -1062,8 +1062,7 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
state: {
|
||||
status: "running",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
content: event.data.content,
|
||||
metadata: event.data.metadata,
|
||||
},
|
||||
time: { created: part?.time.created ?? event.created, ran: part?.time.ran ?? event.created },
|
||||
})
|
||||
|
|
@ -1084,18 +1083,15 @@ export async function createSessionTransport(input: StreamInput): Promise<Sessio
|
|||
? {
|
||||
status: "error",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured:
|
||||
event.data.metadata ?? (part && part.state.status !== "streaming" ? part.state.structured : {}),
|
||||
content: event.data.content ?? (part && part.state.status !== "streaming" ? part.state.content : []),
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
error: event.data.error,
|
||||
result: event.data.result,
|
||||
}
|
||||
: {
|
||||
status: "completed",
|
||||
input: part && part.state.status !== "streaming" ? part.state.input : {},
|
||||
structured: event.data.structured,
|
||||
metadata: event.data.metadata,
|
||||
content: event.data.content,
|
||||
result: event.data.result,
|
||||
},
|
||||
time: { created: part?.time.created ?? event.created, ran: part?.time.ran, completed: event.created },
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ function traceCommit(commit: StreamCommit) {
|
|||
state: {
|
||||
status: commit.part.state.status,
|
||||
input: summarize(commit.part.state.input),
|
||||
structured: "structured" in commit.part.state ? summarize(commit.part.state.structured) : undefined,
|
||||
metadata: "metadata" in commit.part.state ? summarize(commit.part.state.metadata) : undefined,
|
||||
content: "content" in commit.part.state ? summarize(commit.part.state.content) : undefined,
|
||||
error: "error" in commit.part.state ? summarize(commit.part.state.error) : undefined,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,2 +1,3 @@
|
|||
export { toolInlineInfo, toolOutputText } from "./tool"
|
||||
export { readDisplayText, toolInlineInfo, toolOutputText } from "./tool"
|
||||
export { nonEmptyToolContent } from "../util/tool-display"
|
||||
export type { MiniToolPart } from "./types"
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
canonicalToolName,
|
||||
finiteNumber,
|
||||
primitiveInputSummary,
|
||||
toolDisplayContent,
|
||||
toolDisplayMetadata,
|
||||
webSearchProviderLabel,
|
||||
} from "../util/tool-display"
|
||||
|
|
@ -157,10 +158,36 @@ function text(v: unknown): string {
|
|||
return typeof v === "string" ? v : ""
|
||||
}
|
||||
|
||||
export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }>) {
|
||||
export function toolOutputText(name: string, content: ReadonlyArray<{ type: string; text?: string }> | undefined) {
|
||||
if (!content) return ""
|
||||
// V2 shell content appends model-only status after the user-visible command output.
|
||||
if (canonicalToolName(name) === "shell") return content.find((item) => item.type === "text")?.text ?? ""
|
||||
return content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
||||
const joined = content.flatMap((item) => (item.type === "text" && item.text ? [item.text] : [])).join("\n")
|
||||
if (canonicalToolName(name) === "read") return readDisplayText(joined) ?? joined
|
||||
return joined
|
||||
}
|
||||
|
||||
/** Read's model content is a JSON page envelope; unwrap the human-facing text. */
|
||||
export function readDisplayText(text: string): string | undefined {
|
||||
if (!text.startsWith("{")) return undefined
|
||||
const parsed = (() => {
|
||||
try {
|
||||
return JSON.parse(text) as unknown
|
||||
} catch {
|
||||
return undefined
|
||||
}
|
||||
})()
|
||||
const envelope = dict(parsed)
|
||||
if (typeof envelope.content === "string" && (envelope.type === "text-page" || envelope.encoding === "utf8"))
|
||||
return envelope.content
|
||||
if (!Array.isArray(envelope.entries)) return undefined
|
||||
return envelope.entries
|
||||
.flatMap((entry): string[] => {
|
||||
if (typeof entry === "string") return [entry]
|
||||
const path = dict(entry).path
|
||||
return typeof path === "string" ? [path] : []
|
||||
})
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
function normalizeInput(name: string, value: unknown) {
|
||||
|
|
@ -202,16 +229,16 @@ function normalizeFile(value: unknown): PatchFile | undefined {
|
|||
}
|
||||
}
|
||||
|
||||
function normalizeStructured(name: string, value: unknown) {
|
||||
const structured = dict(value)
|
||||
const files = list(structured.files).flatMap((item) => {
|
||||
function normalizeMetadata(name: string, value: unknown) {
|
||||
const metadata = dict(value)
|
||||
const files = list(metadata.files).flatMap((item) => {
|
||||
const file = normalizeFile(item)
|
||||
return file ? [file] : []
|
||||
})
|
||||
const sessionID = text(structured.sessionID) || text(structured.sessionId)
|
||||
const sessionID = text(metadata.sessionID) || text(metadata.sessionId)
|
||||
return {
|
||||
...structured,
|
||||
...(["edit", "patch"].includes(name) && Array.isArray(structured.files) ? { files } : {}),
|
||||
...metadata,
|
||||
...(["edit", "patch"].includes(name) && Array.isArray(metadata.files) ? { files } : {}),
|
||||
...(name === "subagent" && sessionID ? { sessionID } : {}),
|
||||
}
|
||||
}
|
||||
|
|
@ -225,7 +252,7 @@ export function normalizeTool(tool: SessionMessageAssistantTool): SessionMessage
|
|||
state: {
|
||||
...tool.state,
|
||||
input: normalizeInput(name, tool.state.input),
|
||||
structured: normalizeStructured(name, toolDisplayMetadata(tool.state)),
|
||||
metadata: normalizeMetadata(name, toolDisplayMetadata(tool.state)),
|
||||
},
|
||||
} as SessionMessageAssistantTool
|
||||
}
|
||||
|
|
@ -1089,13 +1116,13 @@ function frame(part: SessionMessageAssistantTool, directory?: string): ToolFrame
|
|||
output: "",
|
||||
time: { start: tool.time.created },
|
||||
}
|
||||
const output = toolOutputText(tool.name, tool.state.content)
|
||||
const output = toolOutputText(tool.name, toolDisplayContent(tool.state))
|
||||
return {
|
||||
directory,
|
||||
raw: output,
|
||||
name: tool.name,
|
||||
input: normalizeInput(tool.name, tool.state.input),
|
||||
meta: normalizeStructured(tool.name, tool.state.structured),
|
||||
meta: normalizeMetadata(tool.name, tool.state.metadata),
|
||||
state: dict(tool.state),
|
||||
status: tool.state.status,
|
||||
error: tool.state.status === "error" ? tool.state.error.message : "",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue