Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 65 additions & 4 deletions apps/server/src/provider/acp/AcpRuntimeModel.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,7 @@ describe("AcpRuntimeModel", () => {
toolCallId: "tool-1",
status: "completed",
rawOutput: { exitCode: 0 },
content: [{ type: "content", content: { type: "text", text: "Checks passed" } }],
},
} satisfies EffectAcpSchema.SessionNotification);

Expand All @@ -314,9 +315,10 @@ describe("AcpRuntimeModel", () => {
if (createdEvent?._tag === "ToolCallUpdated" && updatedEvent?._tag === "ToolCallUpdated") {
expect(mergeToolCallState(createdEvent.toolCall, updatedEvent.toolCall)).toMatchObject({
toolCallId: "tool-1",
kind: "execute",
status: "completed",
title: "Ran command",
detail: "bun run typecheck",
detail: "Checks passed",
command: "bun run typecheck",
});
}
Expand Down Expand Up @@ -907,14 +909,54 @@ describe("AcpRuntimeModel", () => {
).toEqual({ emit: true, skippedSinceEmit: 0 });
});

it("emits immediately when the title changes, even with no growth", () => {
it("emits title and kind changes immediately without emitting duplicate metadata", () => {
const decision = decideToolCallUpdateEmission({
previous: { toolCallId: "tool-1", title: "Reading file", detail: "x", data: {} },
next: { toolCallId: "tool-1", title: "Ran command", detail: "x", data: {} },
lastEmittedDetailLength: 1,
skippedSinceEmit: 0,
});
expect(decision).toEqual({ emit: true, skippedSinceEmit: 0 });

const created = parseSessionUpdateEvent({
sessionId: "session-1",
update: {
sessionUpdate: "tool_call",
toolCallId: "inspect-1",
title: "Inspect",
kind: "other",
status: "in_progress",
},
} satisfies EffectAcpSchema.SessionNotification).events[0];
const kindUpdate = parseSessionUpdateEvent({
sessionId: "session-1",
update: {
sessionUpdate: "tool_call_update",
toolCallId: "inspect-1",
kind: "read",
},
} satisfies EffectAcpSchema.SessionNotification).events[0];
if (created?._tag !== "ToolCallUpdated" || kindUpdate?._tag !== "ToolCallUpdated") {
throw new Error("expected tool call updates");
}
const changed = mergeToolCallState(created.toolCall, kindUpdate.toolCall);
expect(changed).toMatchObject({ title: "Inspect", kind: "read", status: "inProgress" });
expect(
decideToolCallUpdateEmission({
previous: created.toolCall,
next: changed,
lastEmittedDetailLength: 0,
skippedSinceEmit: 0,
}),
).toEqual({ emit: true, skippedSinceEmit: 0 });
expect(
decideToolCallUpdateEmission({
previous: changed,
next: mergeToolCallState(changed, kindUpdate.toolCall),
lastEmittedDetailLength: 0,
skippedSinceEmit: 0,
}),
).toEqual({ emit: false, skippedSinceEmit: 0 });
});

it("coalesces small deltas but forces an emission after the coalesce limit", () => {
Expand Down Expand Up @@ -1416,8 +1458,7 @@ describe("extractMcpToolCallIdentity", () => {
it("recovers T3 identity from server-namespaced titles across titleless updates", () => {
// Captured verbatim from Kilo 7.4.22 2026-08-15: the initial tool_call
// titles the MCP function "<server>_<tool>" with kind "other", and the
// completed update carries no title at all, so the merged presentation
// title regresses to "Tool" while data.title keeps the wire value.
// completed update carries result content without repeating the tool metadata.
const created = toolCallFromUpdate({
sessionUpdate: "tool_call",
toolCallId: "chatcmpl-tool-b2a6142ee1a510a5",
Expand All @@ -1435,10 +1476,30 @@ describe("extractMcpToolCallIdentity", () => {
});
const merged = mergeToolCallState(created, completed);

expect(completed).not.toHaveProperty("title");
expect(completed).not.toHaveProperty("kind");
expect(merged).toMatchObject({
title: "t3-code_orchestrator_capabilities",
kind: "other",
status: "completed",
detail: '{"ok":true}',
});
expect(extractMcpToolCallIdentity(merged)).toEqual({
server: "t3-code",
tool: "orchestrator_capabilities",
});

const renamed = toolCallFromUpdate({
sessionUpdate: "tool_call_update",
toolCallId: created.toolCallId,
title: "Finished capability lookup",
kind: "think",
});
expect(mergeToolCallState(merged, renamed)).toMatchObject({
title: "Finished capability lookup",
kind: "think",
status: "completed",
});
});

it("recovers T3 identity from Gemini and qwen MCP-server title templates", () => {
Expand Down
18 changes: 15 additions & 3 deletions apps/server/src/provider/acp/AcpRuntimeModel.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import * as NodeUtil from "node:util";

import * as Clock from "effect/Clock";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
Expand Down Expand Up @@ -849,6 +851,7 @@ function makeToolCallState(
},
options?: {
readonly fallbackStatus?: "pending" | "inProgress" | "completed" | "failed";
readonly partial?: boolean;
},
): AcpToolCallState | undefined {
const toolCallId = input.toolCallId.trim();
Expand Down Expand Up @@ -918,7 +921,10 @@ function makeToolCallState(
return {
toolCallId,
...(kind ? { kind } : {}),
...(presentation?.summary ? { title: presentation.summary } : {}),
// Partial updates keep the existing title when only result content changes.
...(presentation?.summary && (!options?.partial || title !== undefined)
? { title: presentation.summary }
: {}),
...(status ? { status } : {}),
...(command ? { command } : {}),
...(presentation?.detail ? { detail: presentation.detail } : {}),
Expand All @@ -944,7 +950,7 @@ function parseTypedToolCallState(
locations: event.locations,
_meta: event._meta,
},
options,
{ ...options, partial: event.sessionUpdate === "tool_call_update" },
);
}

Expand Down Expand Up @@ -1034,7 +1040,13 @@ export function decideToolCallUpdateEmission(
if (next.status === "completed" || next.status === "failed") {
return { emit: true, skippedSinceEmit: 0 };
}
if (previous === undefined || previous.title !== next.title || previous.status !== next.status) {
if (
previous === undefined ||
previous.title !== next.title ||
previous.kind !== next.kind ||
previous.status !== next.status ||
!NodeUtil.isDeepStrictEqual(previous.data.locations, next.data.locations)
) {
return { emit: true, skippedSinceEmit: 0 };
}
if (previous.detail === next.detail && toolCallOutputUnchanged(previous, next)) {
Expand Down
Loading