fix(agent): recover completed tools after stream read errors

This commit is contained in:
ben
2026-06-28 16:18:46 +08:00
parent 4373dde48a
commit cb4e68888f
7 changed files with 159 additions and 2 deletions
@@ -141,6 +141,66 @@ describe("AgentSession retry delay cap", () => {
expect(session.isRetrying).toBe(false);
});
it("auto-retries OpenAI Responses stream_read_error instead of stopping the conversation", async () => {
const model = getBundledModel("openai", "gpt-5");
if (!model) {
throw new Error("Expected bundled OpenAI test model to exist");
}
authStorage.setRuntimeApiKey("openai", "openai-test-key");
const mock = createMockModel({
responses: [
{ throw: "Error Code stream_read_error: stream_read_error" },
{ content: ["recovered after stream read retry"], stopReason: "stop" },
],
});
const agent = new Agent({
getApiKey: requestedModel => `${requestedModel.provider}-test-key`,
initialState: {
model,
systemPrompt: ["Test"],
tools: [],
messages: [],
},
streamFn: (requestedModel, context, options) => mock.stream(requestedModel, context, options),
});
const settings = Settings.isolated({
"compaction.enabled": false,
"retry.baseDelayMs": 5,
"retry.maxDelayMs": 5_000,
"retry.maxRetries": 1,
"retry.modelFallback": false,
});
settings.setModelRole("default", `${model.provider}/${model.id}`);
session = new AgentSession({
agent,
sessionManager: SessionManager.inMemory(),
settings,
modelRegistry,
});
vi.spyOn(scheduler, "wait").mockResolvedValue(undefined);
const retryStartEvents: AutoRetryStartEvent[] = [];
const retryEndEvents: AutoRetryEndEvent[] = [];
session.subscribe(event => {
if (event.type === "auto_retry_start") retryStartEvents.push(event);
if (event.type === "auto_retry_end") retryEndEvents.push(event);
});
await session.prompt("Trigger stream read retry");
await session.waitForIdle();
expect(mock.calls).toHaveLength(2);
expect(retryStartEvents).toHaveLength(1);
expect(retryEndEvents).toHaveLength(1);
expect(retryEndEvents[0]).toMatchObject({ success: true });
const last = lastAssistant(session);
expect(last.stopReason).toBe("stop");
expect(last.content).toContainEqual({ type: "text", text: "recovered after stream read retry" });
});
it("switches credentials instead of failing the delay cap for account rate limits", async () => {
const model = getBundledModel("anthropic", "claude-sonnet-4-5");
if (!model) {