Avoid duplicate Claude result text (#1095)
This commit is contained in:
@@ -1461,4 +1461,64 @@ describe("ClaudeAgentSession context window usage", () => {
|
|||||||
expect(timelineEvents).toEqual([]);
|
expect(timelineEvents).toEqual([]);
|
||||||
expect(events.some((event) => event.type === "turn_completed")).toBe(true);
|
expect(events.some((event) => event.type === "turn_completed")).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("result.result is not duplicated when assistant text already streamed with zero token usage", async () => {
|
||||||
|
const queryFactory = createQueryFactoryForTurns([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
type: "system",
|
||||||
|
subtype: "init",
|
||||||
|
session_id: "session-third-party",
|
||||||
|
permissionMode: "default",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "assistant",
|
||||||
|
message: {
|
||||||
|
id: "assistant-third-party-1",
|
||||||
|
role: "assistant",
|
||||||
|
content: [{ type: "text", text: "Here is the answer." }],
|
||||||
|
usage: {
|
||||||
|
input_tokens: 0,
|
||||||
|
output_tokens: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session_id: "session-third-party",
|
||||||
|
uuid: "assistant-third-party-event-1",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: "result",
|
||||||
|
subtype: "success",
|
||||||
|
result: "Here is the answer.",
|
||||||
|
is_error: false,
|
||||||
|
duration_ms: 100,
|
||||||
|
duration_api_ms: 80,
|
||||||
|
num_turns: 1,
|
||||||
|
stop_reason: null,
|
||||||
|
total_cost_usd: 0.01,
|
||||||
|
usage: {
|
||||||
|
input_tokens: 10,
|
||||||
|
cache_read_input_tokens: 0,
|
||||||
|
output_tokens: 0,
|
||||||
|
},
|
||||||
|
permission_denials: [],
|
||||||
|
uuid: "result-third-party-1",
|
||||||
|
session_id: "session-third-party",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
const client = new ClaudeAgentClient({
|
||||||
|
logger,
|
||||||
|
queryFactory,
|
||||||
|
resolveBinary: async () => "/test/claude/bin",
|
||||||
|
});
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "claude",
|
||||||
|
cwd: process.cwd(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await session.run("turn");
|
||||||
|
await session.close();
|
||||||
|
|
||||||
|
expect(result.timeline).toEqual([{ type: "assistant_message", text: "Here is the answer." }]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1568,6 +1568,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private pendingInterruptAbort = false;
|
private pendingInterruptAbort = false;
|
||||||
private lastForegroundPromptText: string | null = null;
|
private lastForegroundPromptText: string | null = null;
|
||||||
private foregroundHasVisibleActivity = false;
|
private foregroundHasVisibleActivity = false;
|
||||||
|
private activeTurnHasAssistantText = false;
|
||||||
private lastContextWindowUsedTokens: number | undefined;
|
private lastContextWindowUsedTokens: number | undefined;
|
||||||
private lastContextWindowMaxTokens: number | undefined;
|
private lastContextWindowMaxTokens: number | undefined;
|
||||||
private lastStreamRequestInputTokens: number | undefined;
|
private lastStreamRequestInputTokens: number | undefined;
|
||||||
@@ -1692,6 +1693,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
const turnId = this.createTurnId("foreground");
|
const turnId = this.createTurnId("foreground");
|
||||||
this.activeForegroundTurnId = turnId;
|
this.activeForegroundTurnId = turnId;
|
||||||
this.foregroundHasVisibleActivity = false;
|
this.foregroundHasVisibleActivity = false;
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.transitionTurnState("foreground", "foreground turn started");
|
this.transitionTurnState("foreground", "foreground turn started");
|
||||||
this.clearRecentStderr();
|
this.clearRecentStderr();
|
||||||
|
|
||||||
@@ -2630,6 +2632,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.activeForegroundTurnId = null;
|
this.activeForegroundTurnId = null;
|
||||||
this.lastForegroundPromptText = null;
|
this.lastForegroundPromptText = null;
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.syncTurnState("foreground turn terminal");
|
this.syncTurnState("foreground turn terminal");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2645,9 +2648,11 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.activeForegroundTurnId = null;
|
this.activeForegroundTurnId = null;
|
||||||
this.lastForegroundPromptText = null;
|
this.lastForegroundPromptText = null;
|
||||||
this.cancelCurrentTurn = null;
|
this.cancelCurrentTurn = null;
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.syncTurnState("foreground turn terminal");
|
this.syncTurnState("foreground turn terminal");
|
||||||
} else if (this.autonomousTurn) {
|
} else if (this.autonomousTurn) {
|
||||||
this.autonomousTurn = null;
|
this.autonomousTurn = null;
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.syncTurnState("autonomous turn terminal");
|
this.syncTurnState("autonomous turn terminal");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2660,6 +2665,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.autonomousTurn = {
|
this.autonomousTurn = {
|
||||||
id: this.createTurnId("autonomous"),
|
id: this.createTurnId("autonomous"),
|
||||||
};
|
};
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.notifySubscribers({ type: "turn_started", provider: "claude" });
|
this.notifySubscribers({ type: "turn_started", provider: "claude" });
|
||||||
this.syncTurnState("autonomous turn started");
|
this.syncTurnState("autonomous turn started");
|
||||||
}
|
}
|
||||||
@@ -2670,6 +2676,7 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
}
|
}
|
||||||
this.notifySubscribers({ type: "turn_completed", provider: "claude" });
|
this.notifySubscribers({ type: "turn_completed", provider: "claude" });
|
||||||
this.autonomousTurn = null;
|
this.autonomousTurn = null;
|
||||||
|
this.activeTurnHasAssistantText = false;
|
||||||
this.syncTurnState("autonomous turn completed");
|
this.syncTurnState("autonomous turn completed");
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2898,7 +2905,6 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (
|
||||||
this.pendingInterruptAbort &&
|
this.pendingInterruptAbort &&
|
||||||
message.type === "result" &&
|
message.type === "result" &&
|
||||||
@@ -2909,6 +2915,11 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
this.logger.debug("Suppressing stale Claude interrupt terminal result");
|
this.logger.debug("Suppressing stale Claude interrupt terminal result");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (
|
||||||
|
events.some((event) => event.type === "timeline" && event.item.type === "assistant_message")
|
||||||
|
) {
|
||||||
|
this.activeTurnHasAssistantText = true;
|
||||||
|
}
|
||||||
if (
|
if (
|
||||||
this.activeForegroundTurnId &&
|
this.activeForegroundTurnId &&
|
||||||
events.some(
|
events.some(
|
||||||
@@ -3231,12 +3242,12 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
if (message.subtype === "success") {
|
if (message.subtype === "success") {
|
||||||
// Built-in slash commands (e.g. /voice, /usage, "Unknown command: …")
|
// Built-in slash commands (e.g. /voice, /usage, "Unknown command: …")
|
||||||
// run client-side in the Claude CLI with no model turn — output_tokens
|
// run client-side in the Claude CLI with no model turn — output_tokens
|
||||||
// is 0 and the user-visible text is carried in `result`. Surface it as
|
// is 0 and the user-visible text is carried in `result`. Surface it only
|
||||||
// an assistant message so the turn doesn't end silently. Normal turns
|
// when the turn has not already emitted assistant text so zero-token
|
||||||
// have output_tokens > 0 and their text is already in the stream.
|
// accounting from provider gateways does not duplicate streamed output.
|
||||||
const resultText = typeof message.result === "string" ? message.result.trim() : "";
|
const resultText = typeof message.result === "string" ? message.result.trim() : "";
|
||||||
const outputTokens = message.usage?.output_tokens;
|
const outputTokens = message.usage?.output_tokens;
|
||||||
if (resultText.length > 0 && outputTokens === 0) {
|
if (resultText.length > 0 && outputTokens === 0 && !this.activeTurnHasAssistantText) {
|
||||||
events.push({
|
events.push({
|
||||||
type: "timeline",
|
type: "timeline",
|
||||||
provider: "claude",
|
provider: "claude",
|
||||||
|
|||||||
Reference in New Issue
Block a user