| @@ -2367,6 +2367,139 @@ describe("ClaudeAdapterLive", () => { · 2366 unmodified lines |
| 2367 | 2367 | ); |
| 2368 | 2368 | }); |
| 2369 | 2369 | |
| 2370 | + const usageLimitMessage = |
| 2371 | + "Claude usage limit reached. Send the message again once the limit resets."; |
| 2372 | + const genericApiErrorMessage = "Claude gave up after repeated API errors."; |
| 2373 | + const rateLimitAssistant = { |
| 2374 | + type: "assistant", |
| 2375 | + session_id: "sdk-session-limit", |
| 2376 | + uuid: "assistant-limit", |
| 2377 | + parent_tool_use_id: null, |
| 2378 | + error: "rate_limit", |
| 2379 | + message: { |
| 2380 | + id: "assistant-message-limit", |
| 2381 | + model: "<synthetic>", |
| 2382 | + content: [{ type: "text", text: "You've hit your session limit" }], |
| 2383 | + }, |
| 2384 | + }; |
| 2385 | + const rateLimitResult = { |
| 2386 | + type: "result", |
| 2387 | + subtype: "success", |
| 2388 | + is_error: true, |
| 2389 | + terminal_reason: "api_error", |
| 2390 | + session_id: "sdk-session-limit", |
| 2391 | + uuid: "result-limit", |
| 2392 | + }; |
| 2393 | + |
| 2394 | + it.effect.each([ |
| 2395 | + { |
| 2396 | + name: "an assistant-only rate limit", |
| 2397 | + messages: [rateLimitAssistant], |
| 2398 | + expected: usageLimitMessage, |
| 2399 | + }, |
| 2400 | + { |
| 2401 | + name: "a normal parent response after a rate limit", |
| 2402 | + messages: [rateLimitAssistant, { ...rateLimitAssistant, error: undefined }], |
| 2403 | + expected: genericApiErrorMessage, |
| 2404 | + }, |
| 2405 | + { |
| 2406 | + name: "a server error after a rate limit", |
| 2407 | + messages: [rateLimitAssistant, { ...rateLimitAssistant, error: "server_error" }], |
| 2408 | + expected: genericApiErrorMessage, |
| 2409 | + }, |
| 2410 | + { |
| 2411 | + name: "a subagent rate limit", |
| 2412 | + messages: [{ ...rateLimitAssistant, parent_tool_use_id: "nested-tool" }], |
| 2413 | + expected: genericApiErrorMessage, |
| 2414 | + }, |
| 2415 | + { |
| 2416 | + name: "a subagent response after a parent rate limit", |
| 2417 | + messages: [ |
| 2418 | + rateLimitAssistant, |
| 2419 | + { ...rateLimitAssistant, error: undefined, parent_tool_use_id: "nested-tool" }, |
| 2420 | + ], |
| 2421 | + expected: usageLimitMessage, |
| 2422 | + }, |
| 2423 | + ])("classifies the terminal API failure after $name", ({ messages, expected }) => { |
| 2424 | + const harness = makeHarness(); |
| 2425 | + return Effect.gen(function* () { |
| 2426 | + const adapter = yield* ClaudeAdapter; |
| 2427 | + const eventsFiber = yield* adapter.streamEvents.pipe( |
| 2428 | + Stream.takeUntil((event) => event.type === "turn.completed"), |
| 2429 | + Stream.runCollect, |
| 2430 | + Effect.forkChild, |
| 2431 | + ); |
| 2432 | + const session = yield* adapter.startSession({ |
| 2433 | + threadId: THREAD_ID, |
| 2434 | + provider: ProviderDriverKind.make("claudeAgent"), |
| 2435 | + runtimeMode: "full-access", |
| 2436 | + }); |
| 2437 | + yield* adapter.sendTurn({ threadId: session.threadId, input: "hello", attachments: [] }); |
| 2438 | + for (const [index, message] of messages.entries()) { |
| 2439 | + harness.query.emit({ ...message, uuid: `assistant-${index}` } as unknown as SDKMessage); |
| 2440 | + } |
| 2441 | + harness.query.emit(rateLimitResult as unknown as SDKMessage); |
| 2442 | + |
| 2443 | + const events = Array.from(yield* Fiber.join(eventsFiber)); |
| 2444 | + const errors = events.filter((event) => event.type === "runtime.error"); |
| 2445 | + assert.equal(errors.length, 1); |
| 2446 | + assert.equal(errors[0]?.payload.message, expected); |
| 2447 | + assert.equal(completedTurn(events).state, "failed"); |
| 2448 | + assert.equal(completedTurn(events).errorMessage, expected); |
| 2449 | + }).pipe( |
| 2450 | + Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 2451 | + Effect.provide(harness.layer), |
| 2452 | + ); |
| 2453 | + }); |
| 2454 | + |
| 2455 | + it.effect("names repeated usage limits without carrying them into a later turn", () => { |
| 2456 | + const harness = makeHarness(); |
| 2457 | + return Effect.gen(function* () { |
| 2458 | + const adapter = yield* ClaudeAdapter; |
| 2459 | + const session = yield* adapter.startSession({ |
| 2460 | + threadId: THREAD_ID, |
| 2461 | + provider: ProviderDriverKind.make("claudeAgent"), |
| 2462 | + runtimeMode: "full-access", |
| 2463 | + }); |
| 2464 | + for (const [index, expected] of [ |
| 2465 | + usageLimitMessage, |
| 2466 | + usageLimitMessage, |
| 2467 | + genericApiErrorMessage, |
| 2468 | + ].entries()) { |
| 2469 | + const eventsFiber = yield* adapter.streamEvents.pipe( |
| 2470 | + Stream.takeUntil((event) => event.type === "turn.completed"), |
| 2471 | + Stream.runCollect, |
| 2472 | + Effect.forkChild, |
| 2473 | + ); |
| 2474 | + yield* adapter.sendTurn({ threadId: session.threadId, input: "again", attachments: [] }); |
| 2475 | + if (index === 0) { |
| 2476 | + harness.query.emit({ |
| 2477 | + type: "rate_limit_event", |
| 2478 | + rate_limit_info: { status: "rejected", rateLimitType: "five_hour" }, |
| 2479 | + session_id: "sdk-session-limit", |
| 2480 | + uuid: "limit-rejected", |
| 2481 | + } as unknown as SDKMessage); |
| 2482 | + } |
| 2483 | + if (index < 2) { |
| 2484 | + harness.query.emit({ |
| 2485 | + ...rateLimitAssistant, |
| 2486 | + uuid: `assistant-limit-${index}`, |
| 2487 | + } as unknown as SDKMessage); |
| 2488 | + } |
| 2489 | + harness.query.emit({ |
| 2490 | + ...rateLimitResult, |
| 2491 | + uuid: `result-limit-${index}`, |
| 2492 | + } as unknown as SDKMessage); |
| 2493 | + const payload = completedTurn(Array.from(yield* Fiber.join(eventsFiber))); |
| 2494 | + assert.equal(payload.state, "failed"); |
| 2495 | + assert.equal(payload.errorMessage, expected); |
| 2496 | + } |
| 2497 | + }).pipe( |
| 2498 | + Effect.provideService(Random.Random, makeDeterministicRandomService()), |
| 2499 | + Effect.provide(harness.layer), |
| 2500 | + ); |
| 2501 | + }); |
| 2502 | + |
| 2370 | 2503 | it.effect.each([ |
| 2371 | 2504 | { |
| 2372 | 2505 | name: "listed error with api_error", |
| @@ -2418,6 +2551,43 @@ describe("ClaudeAdapterLive", () => { · 45 unmodified lines |
| 2418 | 2551 | expected: /repeated API errors/, |
| 2419 | 2552 | expectedState: "failed", |
| 2420 | 2553 | }, |
| 2554 | + { |
| 2555 | + name: "assistant rate limit followed by overload", |
| 2556 | + evidence: "assistant-rate-limit", |
| 2557 | + result: { |
| 2558 | + subtype: "success", |
| 2559 | + is_error: true, |
| 2560 | + terminal_reason: "api_error", |
| 2561 | + api_error_status: 529, |
| 2562 | + errors: [], |
| 2563 | + }, |
| 2564 | + expected: /overloaded \(529\)/, |
| 2565 | + expectedState: "failed", |
| 2566 | + }, |
| 2567 | + { |
| 2568 | + name: "assistant rate limit followed by a listed error", |
| 2569 | + evidence: "assistant-rate-limit", |
| 2570 | + result: { |
| 2571 | + subtype: "success", |
| 2572 | + is_error: true, |
| 2573 | + terminal_reason: "api_error", |
| 2574 | + errors: ["Tool execution failed: EACCES"], |
| 2575 | + }, |
| 2576 | + expected: /EACCES/, |
| 2577 | + expectedState: "failed", |
| 2578 | + }, |
| 2579 | + { |
| 2580 | + name: "assistant rate limit followed by an interrupt", |
| 2581 | + evidence: "assistant-rate-limit", |
| 2582 | + result: { |
| 2583 | + subtype: "error_during_execution", |
| 2584 | + is_error: true, |
| 2585 | + terminal_reason: "aborted_tools", |
| 2586 | + errors: [], |
| 2587 | + }, |
| 2588 | + expected: undefined, |
| 2589 | + expectedState: "interrupted", |
| 2590 | + }, |
| 2421 | 2591 | ...[ |
| 2422 | 2592 | "recovered-missing-reset", |
| 2423 | 2593 | "recovered-next-reset", |
| @@ -2465,7 +2635,9 @@ describe("ClaudeAdapterLive", () => { · 41 unmodified lines |
| 2465 | 2635 | input: "synthetic hello", |
| 2466 | 2636 | attachments: [], |
| 2467 | 2637 | }); |
| 2468 | | − if (evidence === "auth" || evidence === "nested-auth") { |
| 2638 | + if (evidence === "assistant-rate-limit") { |
| 2639 | + harness.query.emit(rateLimitAssistant as unknown as SDKMessage); |
| 2640 | + } else if (evidence === "auth" || evidence === "nested-auth") { |
| 2469 | 2641 | harness.query.emit({ |
| 2470 | 2642 | type: "assistant", |
| 2471 | 2643 | session_id: "sdk-audit", |