fix(ui): project item-only native tool lifecycles
This commit is contained in:
parent
8f957033b8
commit
ed4a59eeeb
|
|
@ -257,6 +257,65 @@ describe("nativeRunEventsToTranscript", () => {
|
|||
]);
|
||||
});
|
||||
|
||||
it("projects ACPX item-only tool lifecycles at the named write boundary", () => {
|
||||
const transcript = nativeRunEventsToTranscript([
|
||||
itemEvent(1, "item.started", "2", {
|
||||
kind: "dynamicToolCall",
|
||||
item: {
|
||||
type: "tool_use",
|
||||
id: "2",
|
||||
name: "write_document",
|
||||
input: {
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
body: "Ship the durable runner.",
|
||||
},
|
||||
},
|
||||
}),
|
||||
itemEvent(2, "item.completed", "2", {
|
||||
kind: "dynamicToolCall",
|
||||
item: {
|
||||
type: "tool_result",
|
||||
id: "2",
|
||||
tool_use_id: "2",
|
||||
result: {
|
||||
disposition: "applied",
|
||||
document: {
|
||||
key: "plan",
|
||||
latestRevisionId: "revision-1",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(transcript).toEqual([
|
||||
expect.objectContaining({
|
||||
kind: "tool_call",
|
||||
name: "write_document",
|
||||
toolUseId: "2",
|
||||
input: {
|
||||
key: "plan",
|
||||
title: "Plan",
|
||||
body: "Ship the durable runner.",
|
||||
},
|
||||
}),
|
||||
expect.objectContaining({
|
||||
kind: "tool_result",
|
||||
toolUseId: "2",
|
||||
toolName: "write_document",
|
||||
content: JSON.stringify({
|
||||
disposition: "applied",
|
||||
document: {
|
||||
key: "plan",
|
||||
latestRevisionId: "revision-1",
|
||||
},
|
||||
}),
|
||||
isError: false,
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("streams deltas until a loss-resistant completed item is available", () => {
|
||||
expect(nativeRunEventsToTranscript([
|
||||
event(1, "item.delta", { itemId: "message-1", kind: "agentMessage", text: "Still " }),
|
||||
|
|
|
|||
|
|
@ -512,7 +512,11 @@ interface NativeToolItemDetails {
|
|||
|
||||
function nativeToolItemDetails(
|
||||
payload: Record<string, unknown>,
|
||||
): { id: string | null; details: NativeToolItemDetails } | null {
|
||||
): {
|
||||
id: string | null;
|
||||
kind: "tooluse" | "toolresult";
|
||||
details: NativeToolItemDetails;
|
||||
} | null {
|
||||
const item = normalizedItem(payload);
|
||||
const kind = (text(item.type) ?? "").replaceAll("_", "").toLowerCase();
|
||||
if (kind !== "tooluse" && kind !== "toolresult") return null;
|
||||
|
|
@ -521,6 +525,7 @@ function nativeToolItemDetails(
|
|||
: text(item.id);
|
||||
return {
|
||||
id,
|
||||
kind,
|
||||
details: {
|
||||
name: text(item.name),
|
||||
...(Object.prototype.hasOwnProperty.call(item, "input")
|
||||
|
|
@ -534,6 +539,15 @@ function nativeToolItemDetails(
|
|||
};
|
||||
}
|
||||
|
||||
function serializedNativeToolResult(item: NativeToolItemDetails): string {
|
||||
if (item.result === undefined) return "";
|
||||
try {
|
||||
return JSON.stringify(item.result) ?? "";
|
||||
} catch {
|
||||
return "Tool result could not be serialized";
|
||||
}
|
||||
}
|
||||
|
||||
function toolPresentation(
|
||||
payload: Record<string, unknown>,
|
||||
item?: NativeToolItemDetails,
|
||||
|
|
@ -726,6 +740,50 @@ export function nativeRunEventsToTranscript(events: readonly HeartbeatRunEvent[]
|
|||
continue;
|
||||
}
|
||||
|
||||
// Some provider transports expose their complete dynamic-tool lifecycle
|
||||
// directly as item.started/item.completed events and do not emit the
|
||||
// parallel tool.execution.* activity stream. Project those canonical
|
||||
// tool items at their own stable item boundary so saved documents can be
|
||||
// embedded beside the write that created them instead of falling back to
|
||||
// the end of the run timeline.
|
||||
const nativeToolEvent = isItemIdentityEvent(event.eventType)
|
||||
? nativeToolItemDetails(payload)
|
||||
: null;
|
||||
if (nativeToolEvent) {
|
||||
const toolId = nativeToolEvent.id ?? itemId;
|
||||
if (!toolId) continue;
|
||||
const nativeToolItem = nativeToolItemsById.get(toolId)
|
||||
?? nativeToolEvent.details;
|
||||
const presentation = toolPresentation({}, nativeToolItem);
|
||||
if (!startedToolIds.has(toolId)) {
|
||||
startedToolIds.add(toolId);
|
||||
entries.push({
|
||||
kind: "tool_call",
|
||||
ts,
|
||||
name: presentation.name,
|
||||
input: presentation.input,
|
||||
toolUseId: toolId,
|
||||
});
|
||||
}
|
||||
if (
|
||||
event.eventType === "item.completed"
|
||||
&& (nativeToolEvent.kind === "toolresult"
|
||||
|| nativeToolEvent.details.result !== undefined)
|
||||
&& !completedToolIds.has(toolId)
|
||||
) {
|
||||
completedToolIds.add(toolId);
|
||||
entries.push({
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
toolUseId: toolId,
|
||||
toolName: presentation.name,
|
||||
content: serializedNativeToolResult(nativeToolItem),
|
||||
isError: nativeToolItem.isError,
|
||||
});
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
const providerActivity = providerActivityPresentation(event, payload);
|
||||
if (providerActivity) {
|
||||
if (!startedToolIds.has(providerActivity.id)) {
|
||||
|
|
@ -770,14 +828,9 @@ export function nativeRunEventsToTranscript(events: readonly HeartbeatRunEvent[]
|
|||
if (event.eventType === "tool.execution.completed" && !completedToolIds.has(executionId)) {
|
||||
completedToolIds.add(executionId);
|
||||
const output = text(payload.output);
|
||||
let content = output ?? "";
|
||||
if (!output && nativeToolItem?.result !== undefined) {
|
||||
try {
|
||||
content = JSON.stringify(nativeToolItem.result) ?? "";
|
||||
} catch {
|
||||
content = "Tool result could not be serialized";
|
||||
}
|
||||
}
|
||||
const content = output ?? (nativeToolItem
|
||||
? serializedNativeToolResult(nativeToolItem)
|
||||
: "");
|
||||
entries.push({
|
||||
kind: "tool_result",
|
||||
ts,
|
||||
|
|
|
|||
Loading…
Reference in New Issue