* feat(app): compact tool call sequences * feat(app): summarize compact tool activity * feat(app): add tool call detail levels * fix(app): stabilize compact tool groups * fix(app): address compact tool review feedback
265 lines
9.1 KiB
TypeScript
265 lines
9.1 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
|
import type { StreamItem, ToolCallItem } from "@/types/stream";
|
|
import { compactToolCallRuns } from "./grouping";
|
|
|
|
function toolCall(
|
|
id: string,
|
|
detail: ToolCallDetail,
|
|
options: {
|
|
name?: string;
|
|
status?: "running" | "completed" | "failed" | "canceled";
|
|
} = {},
|
|
): ToolCallItem {
|
|
return {
|
|
kind: "tool_call",
|
|
id,
|
|
timestamp: new Date(`2026-01-01T00:00:${id.padStart(2, "0")}.000Z`),
|
|
payload: {
|
|
source: "agent",
|
|
data: {
|
|
provider: "claude",
|
|
callId: id,
|
|
name: options.name ?? detail.type,
|
|
status: options.status ?? "completed",
|
|
error: options.status === "failed" ? "boom" : null,
|
|
detail,
|
|
},
|
|
},
|
|
};
|
|
}
|
|
|
|
function assistant(id: string): StreamItem {
|
|
return {
|
|
kind: "assistant_message",
|
|
id,
|
|
text: id,
|
|
timestamp: new Date("2026-01-01T00:01:00.000Z"),
|
|
};
|
|
}
|
|
|
|
describe("compactToolCallRuns", () => {
|
|
it("preserves the original arrays when compaction is disabled", () => {
|
|
const tail = [toolCall("1", { type: "shell", command: "one" })];
|
|
const head = [toolCall("2", { type: "shell", command: "two" })];
|
|
|
|
const result = compactToolCallRuns({ tail, head, enabled: false });
|
|
|
|
expect(result.tail).toBe(tail);
|
|
expect(result.head).toBe(head);
|
|
expect(result.groupsByHostId.size).toBe(0);
|
|
});
|
|
|
|
it("replaces four contiguous calls with a stable first-call host and latest timestamp", () => {
|
|
const calls = [
|
|
toolCall("1", { type: "shell", command: "one" }),
|
|
toolCall("2", { type: "shell", command: "two" }),
|
|
toolCall("3", { type: "read", filePath: "/repo/src/a.ts" }),
|
|
toolCall("4", { type: "edit", filePath: "/repo/src/a.ts" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
|
|
expect(result.tail).toHaveLength(1);
|
|
expect(result.tail[0]).toMatchObject({ id: "1", timestamp: calls[3]?.timestamp });
|
|
expect(result.head).toEqual([]);
|
|
expect(result.groupsByHostId.get("1")?.id).toBe("1");
|
|
expect(result.groupsByHostId.get("1")?.calls).toEqual(calls);
|
|
expect(result.groupsByHostId.get("1")).toMatchObject({
|
|
editedFileCount: 1,
|
|
commandCount: 2,
|
|
readFileCount: 1,
|
|
otherToolCount: 0,
|
|
categories: [
|
|
{ key: "shell", label: "Shell", callCount: 2, resources: [] },
|
|
{ key: "read", label: "Read", callCount: 1, resources: ["a.ts"] },
|
|
{ key: "edit", label: "Edit", callCount: 1, resources: ["a.ts"] },
|
|
],
|
|
});
|
|
|
|
const nextCall = toolCall("5", { type: "shell", command: "five" }, { status: "running" });
|
|
const updated = compactToolCallRuns({
|
|
tail: [...calls, nextCall],
|
|
head: [],
|
|
enabled: true,
|
|
});
|
|
expect(updated.tail[0]).toMatchObject({ id: "1", timestamp: nextCall.timestamp });
|
|
expect(updated.groupsByHostId.get("1")?.id).toBe("1");
|
|
});
|
|
|
|
it("does not compact short runs and stops at visible content boundaries", () => {
|
|
const firstRun = [
|
|
toolCall("1", { type: "shell", command: "one" }),
|
|
toolCall("2", { type: "shell", command: "two" }),
|
|
toolCall("3", { type: "shell", command: "three" }),
|
|
];
|
|
const boundary = assistant("assistant");
|
|
const secondRun = [
|
|
toolCall("4", { type: "read", filePath: "/repo/a.ts" }),
|
|
toolCall("5", { type: "read", filePath: "/repo/b.ts" }),
|
|
toolCall("6", { type: "read", filePath: "/repo/c.ts" }),
|
|
toolCall("7", { type: "read", filePath: "/repo/d.ts" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({
|
|
tail: [...firstRun, boundary, ...secondRun],
|
|
head: [],
|
|
enabled: true,
|
|
});
|
|
|
|
expect(result.tail.slice(0, -1)).toEqual([...firstRun, boundary]);
|
|
expect(result.tail.at(-1)).toMatchObject({ id: "4", timestamp: secondRun[3]?.timestamp });
|
|
expect([...result.groupsByHostId.keys()]).toEqual(["4"]);
|
|
});
|
|
|
|
it("forms one group across the history and live-head boundary", () => {
|
|
const tail = [
|
|
assistant("assistant"),
|
|
toolCall("1", { type: "shell", command: "one" }),
|
|
toolCall("2", { type: "shell", command: "two" }),
|
|
];
|
|
const head = [
|
|
toolCall("3", { type: "read", filePath: "/repo/a.ts" }),
|
|
toolCall("4", { type: "edit", filePath: "/repo/a.ts" }, { status: "running" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail, head, enabled: true });
|
|
|
|
expect(result.tail).toEqual([tail[0]]);
|
|
expect(result.head).toHaveLength(1);
|
|
expect(result.head[0]).toMatchObject({ id: "1", timestamp: head[1]?.timestamp });
|
|
expect(result.groupsByHostId.get("1")?.calls).toEqual([...tail.slice(1), ...head]);
|
|
expect(result.groupsByHostId.get("1")?.isRunning).toBe(true);
|
|
});
|
|
|
|
it("separates reads and searches from other tools", () => {
|
|
const calls = [
|
|
toolCall("1", { type: "read", filePath: "/repo/src/a.ts" }),
|
|
toolCall("2", { type: "read", filePath: "C:\\repo\\src\\beta.ts" }),
|
|
toolCall("3", { type: "fetch", url: "https://github.com/org/repo" }),
|
|
toolCall(
|
|
"4",
|
|
{ type: "search", query: "paseo", toolName: "web_search" },
|
|
{ status: "failed" },
|
|
),
|
|
toolCall("5", { type: "fetch", url: "not a url" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
const group = result.groupsByHostId.get("1");
|
|
|
|
expect(group).toMatchObject({
|
|
failedCount: 1,
|
|
isRunning: false,
|
|
editedFileCount: 0,
|
|
commandCount: 0,
|
|
readFileCount: 2,
|
|
searchCount: 1,
|
|
otherToolCount: 2,
|
|
});
|
|
});
|
|
|
|
it("counts unique edited files while counting each shell command", () => {
|
|
const calls = [
|
|
toolCall("1", { type: "edit", filePath: "/repo/a.ts" }),
|
|
toolCall("2", { type: "edit", filePath: "/repo/a.ts" }),
|
|
toolCall("3", { type: "write", filePath: "/repo/b.ts" }),
|
|
toolCall("4", { type: "shell", command: "npm test" }),
|
|
toolCall("5", { type: "shell", command: "npm run lint" }),
|
|
toolCall("6", { type: "read", filePath: "/repo/c.ts" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
|
|
expect(result.groupsByHostId.get("1")).toMatchObject({
|
|
editedFileCount: 2,
|
|
commandCount: 2,
|
|
readFileCount: 1,
|
|
otherToolCount: 0,
|
|
});
|
|
});
|
|
|
|
it("counts Paseo calls separately from other tools", () => {
|
|
const calls = [
|
|
toolCall("1", { type: "unknown", input: null, output: null }, { name: "paseo.list_agents" }),
|
|
toolCall(
|
|
"2",
|
|
{ type: "unknown", input: null, output: null },
|
|
{ name: "mcp__paseo__list_worktrees" },
|
|
),
|
|
toolCall("3", { type: "fetch", url: "https://paseo.sh" }),
|
|
toolCall("4", { type: "fetch", url: "https://github.com/getpaseo" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
|
|
expect(result.groupsByHostId.get("1")).toMatchObject({
|
|
otherToolCount: 2,
|
|
paseoCallCount: 2,
|
|
});
|
|
});
|
|
|
|
it("classifies direct Brave and Paseo runtime tool names", () => {
|
|
const unknownDetail = { type: "unknown" as const, input: null, output: null };
|
|
const calls = [
|
|
toolCall("1", unknownDetail, { name: "brave-search_brave_web_search" }),
|
|
toolCall("2", unknownDetail, { name: "brave-search_brave_llm_context" }),
|
|
toolCall("3", unknownDetail, { name: "paseo_list_providers" }),
|
|
toolCall("4", unknownDetail, { name: "paseo_list_worktrees" }),
|
|
toolCall("5", unknownDetail, { name: "paseo_list_worktrees" }),
|
|
toolCall("6", unknownDetail, { name: "mcp__exa__web_search" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
|
|
expect(result.groupsByHostId.get("1")).toMatchObject({
|
|
searchCount: 3,
|
|
otherToolCount: 0,
|
|
paseoCallCount: 3,
|
|
});
|
|
});
|
|
|
|
it("keeps plan and spoken-message calls outside compact groups", () => {
|
|
const shellCalls = ["1", "2", "3", "4"].map((id) =>
|
|
toolCall(id, { type: "shell", command: id }),
|
|
);
|
|
const plan = toolCall("5", { type: "plan", text: "Plan" });
|
|
const speak = toolCall(
|
|
"6",
|
|
{ type: "unknown", input: "Hello", output: null },
|
|
{ name: "speak" },
|
|
);
|
|
|
|
const result = compactToolCallRuns({
|
|
tail: [...shellCalls, plan, speak],
|
|
head: [],
|
|
enabled: true,
|
|
});
|
|
|
|
expect(result.tail[0]).toMatchObject({ id: "1", timestamp: shellCalls[3]?.timestamp });
|
|
expect(result.tail.slice(1)).toEqual([plan, speak]);
|
|
expect(result.groupsByHostId.get("1")?.calls).toEqual(shellCalls);
|
|
});
|
|
|
|
it("deduplicates file resources by full path while displaying basenames", () => {
|
|
const calls = [
|
|
toolCall("1", { type: "read", filePath: "/repo/src/index.ts" }),
|
|
toolCall("2", { type: "read", filePath: "/repo/tests/index.ts" }),
|
|
toolCall("3", { type: "read", filePath: "/repo/src/other.ts" }),
|
|
toolCall("4", { type: "read", filePath: "/repo/src/index.ts" }),
|
|
];
|
|
|
|
const result = compactToolCallRuns({ tail: calls, head: [], enabled: true });
|
|
|
|
expect(result.groupsByHostId.get("1")).toMatchObject({
|
|
readFileCount: 3,
|
|
categories: [
|
|
{
|
|
key: "read",
|
|
resources: ["index.ts", "index.ts", "other.ts"],
|
|
},
|
|
],
|
|
});
|
|
});
|
|
});
|