feat(app): add tool call detail levels (#2031)
* 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
This commit is contained in:
@@ -0,0 +1,264 @@
|
||||
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"],
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,325 @@
|
||||
import type { ToolCallDetail } from "@getpaseo/protocol/agent-types";
|
||||
import { isPaseoToolName } from "@getpaseo/protocol/tool-name-normalization";
|
||||
import { getFileNameFromPath } from "@/attachments/utils";
|
||||
import type { StreamItem, ToolCallItem } from "@/types/stream";
|
||||
import { buildToolCallDisplayModel } from "@/utils/tool-call-display";
|
||||
import { resolveToolCallIconName, type ToolCallIcon } from "@/utils/tool-call-icon-name";
|
||||
|
||||
export const MIN_COMPACT_TOOL_CALLS = 4;
|
||||
|
||||
const DIRECT_PASEO_TOOL_PREFIX = "paseo_";
|
||||
const DIRECT_SEARCH_TOOL_SUFFIX_PATTERN = /(?:^|[_.:/])(?:web_search|llm_context)$/;
|
||||
|
||||
export interface ToolCallCategorySummary {
|
||||
key: string;
|
||||
label: string;
|
||||
iconName: ToolCallIcon;
|
||||
callCount: number;
|
||||
failedCount: number;
|
||||
runningCount: number;
|
||||
resources: string[];
|
||||
}
|
||||
|
||||
export interface CompactToolCallGroup {
|
||||
id: string;
|
||||
calls: ToolCallItem[];
|
||||
callCount: number;
|
||||
failedCount: number;
|
||||
isRunning: boolean;
|
||||
editedFileCount: number;
|
||||
commandCount: number;
|
||||
readFileCount: number;
|
||||
searchCount: number;
|
||||
otherToolCount: number;
|
||||
paseoCallCount: number;
|
||||
categories: ToolCallCategorySummary[];
|
||||
}
|
||||
|
||||
export interface CompactToolCallRunsResult {
|
||||
tail: StreamItem[];
|
||||
head: StreamItem[];
|
||||
groupsByHostId: Map<string, CompactToolCallGroup>;
|
||||
}
|
||||
|
||||
interface CompactToolCallRunsInput {
|
||||
tail: StreamItem[];
|
||||
head: StreamItem[];
|
||||
enabled: boolean;
|
||||
}
|
||||
|
||||
interface TaggedStreamItem {
|
||||
segment: "tail" | "head";
|
||||
item: StreamItem;
|
||||
}
|
||||
|
||||
interface ToolCallDescriptor {
|
||||
detail: ToolCallDetail;
|
||||
name: string;
|
||||
status: "running" | "completed" | "failed" | "canceled";
|
||||
error: unknown;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ResourceSummary {
|
||||
key: string;
|
||||
label: string;
|
||||
}
|
||||
|
||||
function describeToolCall(item: ToolCallItem): ToolCallDescriptor {
|
||||
if (item.payload.source === "agent") {
|
||||
const { data } = item.payload;
|
||||
return {
|
||||
detail: data.detail,
|
||||
name: data.name,
|
||||
status: data.status,
|
||||
error: data.error,
|
||||
metadata: data.metadata,
|
||||
};
|
||||
}
|
||||
|
||||
const { data } = item.payload;
|
||||
return {
|
||||
detail: {
|
||||
type: "unknown",
|
||||
input: data.arguments ?? null,
|
||||
output: data.result ?? null,
|
||||
},
|
||||
name: data.toolName,
|
||||
status: data.status === "executing" ? "running" : data.status,
|
||||
error: data.error,
|
||||
};
|
||||
}
|
||||
|
||||
function isCompactableToolCall(item: StreamItem): item is ToolCallItem {
|
||||
if (item.kind !== "tool_call") {
|
||||
return false;
|
||||
}
|
||||
const descriptor = describeToolCall(item);
|
||||
return descriptor.detail.type !== "plan" && descriptor.name.trim().toLowerCase() !== "speak";
|
||||
}
|
||||
|
||||
function isDirectPaseoToolName(name: string): boolean {
|
||||
return name.startsWith(DIRECT_PASEO_TOOL_PREFIX);
|
||||
}
|
||||
|
||||
function isDirectSearchToolName(name: string): boolean {
|
||||
return DIRECT_SEARCH_TOOL_SUFFIX_PATTERN.test(name);
|
||||
}
|
||||
|
||||
function resourceForDetail(detail: ToolCallDetail): ResourceSummary | null {
|
||||
if (detail.type === "read" || detail.type === "edit" || detail.type === "write") {
|
||||
return {
|
||||
key: detail.filePath,
|
||||
label: getFileNameFromPath(detail.filePath) ?? detail.filePath,
|
||||
};
|
||||
}
|
||||
if (detail.type !== "fetch") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const hostname = new URL(detail.url).hostname || detail.url;
|
||||
return { key: hostname, label: hostname };
|
||||
} catch {
|
||||
return { key: detail.url, label: detail.url };
|
||||
}
|
||||
}
|
||||
|
||||
function categoryIdentity(input: {
|
||||
descriptor: ToolCallDescriptor;
|
||||
normalizedName: string;
|
||||
displayName: string;
|
||||
}): { key: string; label: string; iconName: ToolCallIcon } {
|
||||
if (isPaseoToolName(input.descriptor.name) || isDirectPaseoToolName(input.normalizedName)) {
|
||||
return { key: "paseo", label: "Paseo", iconName: "paseo" };
|
||||
}
|
||||
if (
|
||||
(input.descriptor.detail.type === "search" &&
|
||||
input.descriptor.detail.toolName === "web_search") ||
|
||||
isDirectSearchToolName(input.normalizedName)
|
||||
) {
|
||||
return { key: "web_search", label: "Web search", iconName: "search" };
|
||||
}
|
||||
if (input.descriptor.detail.type === "fetch") {
|
||||
return { key: "fetch", label: "Web fetch", iconName: "search" };
|
||||
}
|
||||
if (input.descriptor.detail.type !== "unknown" && input.descriptor.detail.type !== "plain_text") {
|
||||
return {
|
||||
key: input.descriptor.detail.type,
|
||||
label: input.displayName,
|
||||
iconName: resolveToolCallIconName(input.descriptor.name, input.descriptor.detail),
|
||||
};
|
||||
}
|
||||
return {
|
||||
key: `tool:${input.displayName.toLowerCase()}`,
|
||||
label: input.displayName,
|
||||
iconName: resolveToolCallIconName(input.descriptor.name, input.descriptor.detail),
|
||||
};
|
||||
}
|
||||
|
||||
function buildCompactToolCallGroup(calls: ToolCallItem[]) {
|
||||
const editedFiles = new Set<string>();
|
||||
const readFiles = new Set<string>();
|
||||
const categories = new Map<string, ToolCallCategorySummary>();
|
||||
const categoryResourceKeys = new Map<string, Set<string>>();
|
||||
let failedCount = 0;
|
||||
let isRunning = false;
|
||||
let commandCount = 0;
|
||||
let searchCount = 0;
|
||||
let otherToolCount = 0;
|
||||
let paseoCallCount = 0;
|
||||
|
||||
for (const call of calls) {
|
||||
const descriptor = describeToolCall(call);
|
||||
const isFailed = descriptor.status === "failed";
|
||||
const isCallRunning = descriptor.status === "running";
|
||||
failedCount += isFailed ? 1 : 0;
|
||||
isRunning ||= isCallRunning;
|
||||
const normalizedName = descriptor.name.trim().toLowerCase();
|
||||
const display = buildToolCallDisplayModel({
|
||||
name: descriptor.name,
|
||||
status: descriptor.status,
|
||||
error: descriptor.error,
|
||||
detail: descriptor.detail,
|
||||
metadata: descriptor.metadata,
|
||||
});
|
||||
const identity = categoryIdentity({
|
||||
descriptor,
|
||||
normalizedName,
|
||||
displayName: display.displayName,
|
||||
});
|
||||
let category = categories.get(identity.key);
|
||||
if (!category) {
|
||||
category = {
|
||||
...identity,
|
||||
callCount: 0,
|
||||
failedCount: 0,
|
||||
runningCount: 0,
|
||||
resources: [],
|
||||
};
|
||||
categories.set(identity.key, category);
|
||||
}
|
||||
category.callCount += 1;
|
||||
category.failedCount += isFailed ? 1 : 0;
|
||||
category.runningCount += isCallRunning ? 1 : 0;
|
||||
const resource = resourceForDetail(descriptor.detail);
|
||||
if (resource) {
|
||||
let resourceKeys = categoryResourceKeys.get(identity.key);
|
||||
if (!resourceKeys) {
|
||||
resourceKeys = new Set();
|
||||
categoryResourceKeys.set(identity.key, resourceKeys);
|
||||
}
|
||||
if (!resourceKeys.has(resource.key)) {
|
||||
resourceKeys.add(resource.key);
|
||||
category.resources.push(resource.label);
|
||||
}
|
||||
}
|
||||
|
||||
if (isPaseoToolName(descriptor.name) || isDirectPaseoToolName(normalizedName)) {
|
||||
paseoCallCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (descriptor.detail.type === "edit" || descriptor.detail.type === "write") {
|
||||
editedFiles.add(descriptor.detail.filePath);
|
||||
continue;
|
||||
}
|
||||
if (descriptor.detail.type === "shell") {
|
||||
commandCount += 1;
|
||||
continue;
|
||||
}
|
||||
if (descriptor.detail.type === "read") {
|
||||
readFiles.add(descriptor.detail.filePath);
|
||||
continue;
|
||||
}
|
||||
if (descriptor.detail.type === "search" || isDirectSearchToolName(normalizedName)) {
|
||||
searchCount += 1;
|
||||
continue;
|
||||
}
|
||||
otherToolCount += 1;
|
||||
}
|
||||
|
||||
const firstCall = calls[0];
|
||||
if (!firstCall) {
|
||||
throw new Error("Cannot build an empty tool call group");
|
||||
}
|
||||
return {
|
||||
id: firstCall.id,
|
||||
calls,
|
||||
callCount: calls.length,
|
||||
failedCount,
|
||||
isRunning,
|
||||
editedFileCount: editedFiles.size,
|
||||
commandCount,
|
||||
readFileCount: readFiles.size,
|
||||
searchCount,
|
||||
otherToolCount,
|
||||
paseoCallCount,
|
||||
categories: [...categories.values()],
|
||||
} satisfies CompactToolCallGroup;
|
||||
}
|
||||
|
||||
export function compactToolCallRuns(input: CompactToolCallRunsInput): CompactToolCallRunsResult {
|
||||
if (!input.enabled) {
|
||||
return {
|
||||
tail: input.tail,
|
||||
head: input.head,
|
||||
groupsByHostId: new Map(),
|
||||
};
|
||||
}
|
||||
|
||||
const taggedItems: TaggedStreamItem[] = [
|
||||
...input.tail.map((item) => ({ segment: "tail" as const, item })),
|
||||
...input.head.map((item) => ({ segment: "head" as const, item })),
|
||||
];
|
||||
const compactedTail: StreamItem[] = [];
|
||||
const compactedHead: StreamItem[] = [];
|
||||
const groupsByHostId = new Map<string, CompactToolCallGroup>();
|
||||
let pendingRun: TaggedStreamItem[] = [];
|
||||
|
||||
const append = ({ segment, item }: TaggedStreamItem) => {
|
||||
(segment === "tail" ? compactedTail : compactedHead).push(item);
|
||||
};
|
||||
const flushRun = () => {
|
||||
if (pendingRun.length >= MIN_COMPACT_TOOL_CALLS) {
|
||||
const first = pendingRun[0];
|
||||
const latest = pendingRun.at(-1);
|
||||
if (!first || !latest) {
|
||||
throw new Error("Cannot compact an empty tool call run");
|
||||
}
|
||||
const calls = pendingRun.map(({ item }) => item as ToolCallItem);
|
||||
const stableHost: TaggedStreamItem = {
|
||||
segment: latest.segment,
|
||||
item: { ...latest.item, id: first.item.id },
|
||||
};
|
||||
append(stableHost);
|
||||
groupsByHostId.set(stableHost.item.id, buildCompactToolCallGroup(calls));
|
||||
} else {
|
||||
for (const entry of pendingRun) {
|
||||
append(entry);
|
||||
}
|
||||
}
|
||||
pendingRun = [];
|
||||
};
|
||||
|
||||
for (const entry of taggedItems) {
|
||||
if (isCompactableToolCall(entry.item)) {
|
||||
pendingRun.push(entry);
|
||||
continue;
|
||||
}
|
||||
flushRun();
|
||||
append(entry);
|
||||
}
|
||||
flushRun();
|
||||
|
||||
if (groupsByHostId.size === 0) {
|
||||
return {
|
||||
tail: input.tail,
|
||||
head: input.head,
|
||||
groupsByHostId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
tail: compactedTail,
|
||||
head: compactedHead,
|
||||
groupsByHostId,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user