mirror of
https://github.com/getpaseo/paseo.git
synced 2026-11-03 15:12:48 +00:00
fix(claude): retain final usage when a turn fails
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.
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