mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-02 22:52:58 +00:00
fix(claude): retain final usage when a turn fails (#8519)
Non-success SDK results computed final token counts and cost but emitted only failure. Publish the existing usage update before the terminal event so clients retain completed work accounting. Co-authored-by: paseo-bot[bot] <266920839+paseo-bot[bot]@users.noreply.github.com>
This commit is contained in:
@@ -13,6 +13,11 @@ interface ScriptedTurn {
|
||||
before?: string;
|
||||
after?: string;
|
||||
isError: boolean;
|
||||
subtype?:
|
||||
| "error_max_turns"
|
||||
| "error_during_execution"
|
||||
| "error_max_budget_usd"
|
||||
| "error_max_structured_output_retries";
|
||||
usage?: {
|
||||
inputTokens: number;
|
||||
cachedInputTokens: number;
|
||||
@@ -57,11 +62,12 @@ function scriptedQuery(turns: ScriptedTurn[]): ClaudeQueryFactory {
|
||||
if (turn.after) yield assistant(`after-${index}`, turn.after);
|
||||
yield {
|
||||
type: "result",
|
||||
subtype: "success",
|
||||
subtype: turn.subtype ?? "success",
|
||||
session_id: "error-session",
|
||||
duration_ms: 1,
|
||||
duration_api_ms: 0,
|
||||
is_error: turn.isError,
|
||||
...(turn.subtype ? { errors: ["Reached maximum number of turns (1)"] } : {}),
|
||||
num_turns: 1,
|
||||
result: turn.isError ? (turn.errorReply ?? "") : (turn.after ?? turn.errorReply ?? ""),
|
||||
stop_reason: "stop_sequence",
|
||||
@@ -188,3 +194,33 @@ test("an error reply the turn moves past is still shown", async () => {
|
||||
expect(terminal(completed)).toMatchObject({ type: "turn_completed" });
|
||||
expect(assistantTexts(completed)).toEqual([LIMIT]);
|
||||
});
|
||||
|
||||
test.each([
|
||||
"error_max_turns",
|
||||
"error_during_execution",
|
||||
"error_max_budget_usd",
|
||||
"error_max_structured_output_retries",
|
||||
] as const)("a %s result publishes final usage before failure", async (subtype) => {
|
||||
const usage = {
|
||||
inputTokens: 2,
|
||||
cachedInputTokens: 12577,
|
||||
outputTokens: 110,
|
||||
totalCostUsd: 0.00365537,
|
||||
};
|
||||
const [failed, next] = await runTurns([
|
||||
{ before: "Running the command.", isError: true, subtype, usage },
|
||||
{ after: "Done.", isError: false },
|
||||
]);
|
||||
|
||||
// Collection ends at the terminal event, so this also checks ordering.
|
||||
expect(failed).toContainEqual(
|
||||
expect.objectContaining({ type: "usage_updated", usage: expect.objectContaining(usage) }),
|
||||
);
|
||||
expect(terminal(failed)).toMatchObject({
|
||||
type: "turn_failed",
|
||||
error: "Reached maximum number of turns (1)",
|
||||
});
|
||||
expect(assistantTexts(failed)).toEqual(["Running the command."]);
|
||||
expect(terminal(next)).toMatchObject({ type: "turn_completed" });
|
||||
expect(assistantTexts(next)).toEqual(["Done."]);
|
||||
});
|
||||
|
||||
@@ -5037,6 +5037,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
"errors" in message && Array.isArray(message.errors) && message.errors.length > 0
|
||||
? message.errors.join("\n")
|
||||
: "Claude run failed";
|
||||
if (usage) events.push({ type: "usage_updated", provider: "claude", usage });
|
||||
events.push(...this.sidechainTracker.finishAll("failed"));
|
||||
events.push(this.buildTurnFailedEvent(errorMessage));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user