fix(agent): decouple agent streams from compat

closes #6851
This commit is contained in:
Mario Zechner
2026-07-20 17:54:17 +02:00
parent 3a40794ea1
commit 1235c0ec64
29 changed files with 219 additions and 105 deletions
@@ -426,7 +426,7 @@ export class AgentSession {
headers?: Record<string, string>;
env?: Record<string, string>;
}> {
if (this.agent.streamFn === streamSimple) {
if (this.agent.streamFunction === streamSimple) {
return this._getRequiredRequestAuth(model);
}
@@ -1836,7 +1836,7 @@ export class AgentSession {
customInstructions,
this._compactionAbortController.signal,
this.thinkingLevel,
this.agent.streamFn,
this.agent.streamFunction,
env,
);
summary = result.summary;
@@ -2037,7 +2037,7 @@ export class AgentSession {
let apiKey: string | undefined;
let headers: Record<string, string> | undefined;
let env: Record<string, string> | undefined;
if (this.agent.streamFn === streamSimple) {
if (this.agent.streamFunction === streamSimple) {
const authResult = await this._modelRuntime.getAuth(this.model);
if (!authResult?.auth.apiKey) return false;
apiKey = authResult.auth.apiKey;
@@ -2112,7 +2112,7 @@ export class AgentSession {
undefined,
this._autoCompactionAbortController.signal,
this.thinkingLevel,
this.agent.streamFn,
this.agent.streamFunction,
env,
);
summary = compactResult.summary;
@@ -2933,7 +2933,7 @@ export class AgentSession {
customInstructions,
replaceInstructions,
reserveTokens: branchSummarySettings.reserveTokens,
streamFn: this.agent.streamFn,
streamFn: this.agent.streamFunction,
});
if (result.aborted) {
return { cancelled: true, aborted: true };
+1 -1
View File
@@ -294,7 +294,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
tools: [],
},
convertToLlm: convertToLlmWithBlockImages,
streamFn: async (model, context, options) => {
streamFunction: async (model, context, options) => {
const providerRetrySettings = settingsManager.getProviderRetrySettings();
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
// SDKs treat timeout=0 as 0ms (immediate timeout), not "no timeout".
@@ -3,7 +3,7 @@ import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { type AssistantMessage, createAssistantMessageEventStream, fauxAssistantMessage } from "@earendil-works/pi-ai";
import { getModel } from "@earendil-works/pi-ai/compat";
import { getModel, streamSimple } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
@@ -21,10 +21,10 @@ describe("AgentSession auto-compaction queue resume", () => {
beforeEach(async () => {
tempDir = join(tmpdir(), `pi-auto-compaction-queue-${Date.now()}`);
mkdirSync(tempDir, { recursive: true });
vi.useFakeTimers();
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const agent = new Agent({
streamFunction: streamSimple,
initialState: {
model,
systemPrompt: "Test",
@@ -50,7 +50,6 @@ describe("AgentSession auto-compaction queue resume", () => {
afterEach(() => {
session.dispose();
vi.useRealTimers();
vi.restoreAllMocks();
if (tempDir && existsSync(tempDir)) {
rmSync(tempDir, { recursive: true });
@@ -84,9 +83,9 @@ describe("AgentSession auto-compaction queue resume", () => {
timestamp: now - 500,
});
session.agent.state.messages = sessionManager.buildSessionContext().messages;
session.agent.streamFn = (summaryModel) => {
session.agent.streamFunction = (summaryModel) => {
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
void Promise.resolve().then(() => {
stream.push({
type: "done",
reason: "stop",
@@ -12,7 +12,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { getModel } from "@earendil-works/pi-ai/compat";
import { getModel, streamSimple } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
@@ -49,6 +49,7 @@ describe.skipIf(!API_KEY)("AgentSession compaction e2e", () => {
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const agent = new Agent({
getApiKey: () => API_KEY,
streamFunction: streamSimple,
initialState: {
model,
systemPrompt: "You are a helpful assistant. Be concise.",
@@ -90,7 +90,7 @@ describe("AgentSession concurrent prompt guard", () => {
systemPrompt: "Test",
tools: [],
},
streamFn: (_model, _context, options) => {
streamFunction: (_model, _context, options) => {
abortSignal = options?.signal;
const stream = new MockAssistantStream();
queueMicrotask(() => {
@@ -195,7 +195,7 @@ describe("AgentSession concurrent prompt guard", () => {
systemPrompt: "Test",
tools: [],
},
streamFn: (_model, context, options) => {
streamFunction: (_model, context, options) => {
abortSignal = options?.signal;
const stream = new MockAssistantStream();
queueMicrotask(() => {
@@ -301,7 +301,7 @@ describe("AgentSession concurrent prompt guard", () => {
systemPrompt: "Test",
tools: [],
},
streamFn: () => {
streamFunction: () => {
const stream = new MockAssistantStream();
queueMicrotask(() => {
stream.push({ type: "start", partial: createAssistantMessage("") });
@@ -362,7 +362,7 @@ describe("AgentSession concurrent prompt guard", () => {
systemPrompt: "Test",
tools: [tool],
},
streamFn: async (_model, context) => {
streamFunction: async (_model, context) => {
const stream = new MockAssistantStream();
queueMicrotask(() => {
const toolResultCount = context.messages.filter((message) => message.role === "toolResult").length;
@@ -508,7 +508,7 @@ describe("AgentSession concurrent prompt guard", () => {
systemPrompt: "Test",
tools: [tool],
},
streamFn: async (_model, context) => {
streamFunction: async (_model, context) => {
const stream = new MockAssistantStream();
queueMicrotask(() => {
const hasToolResult = context.messages.some((message) => message.role === "toolResult");
@@ -84,7 +84,7 @@ describe("AgentSession dynamic provider registration", () => {
session: Awaited<ReturnType<typeof createSession>>,
): Promise<string | undefined> {
let baseUrl: string | undefined;
session.agent.streamFn = async (model) => {
session.agent.streamFunction = async (model) => {
baseUrl = model.baseUrl;
throw new Error("stop");
};
@@ -82,7 +82,7 @@ describe("AgentSession retry", () => {
const agent = new Agent({
getApiKey: () => "test-key",
initialState: { model, systemPrompt: "Test", tools: [] },
streamFn: () => {
streamFunction: () => {
callCount++;
const stream = new MockAssistantStream();
queueMicrotask(() => {
@@ -203,7 +203,7 @@ describe("AgentSession retry", () => {
const agent = new Agent({
getApiKey: () => "test-key",
initialState: { model, systemPrompt: "Test", tools: [] },
streamFn,
streamFunction: streamFn,
});
const sessionManager = SessionManager.inMemory();
const settingsManager = SettingsManager.create(tempDir, tempDir);
@@ -255,7 +255,7 @@ describe("AgentSession retry", () => {
const agent = new Agent({
getApiKey: () => "test-key",
initialState: { model, systemPrompt: "Test", tools: [] },
streamFn: () => {
streamFunction: () => {
callCount++;
const stream = new MockAssistantStream();
queueMicrotask(() => {
@@ -1,5 +1,11 @@
import { Agent } from "@earendil-works/pi-agent-core";
import { type AssistantMessage, getModel, type ToolResultMessage, type Usage } from "@earendil-works/pi-ai/compat";
import {
type AssistantMessage,
getModel,
streamSimple,
type ToolResultMessage,
type Usage,
} from "@earendil-works/pi-ai/compat";
import { describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
@@ -69,6 +75,7 @@ async function createSession() {
const session = new AgentSession({
agent: new Agent({
getApiKey: () => "test-key",
streamFunction: streamSimple,
initialState: {
model,
systemPrompt: "You are a helpful assistant.",
@@ -7,7 +7,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { getModel } from "@earendil-works/pi-ai/compat";
import { getModel, streamSimple } from "@earendil-works/pi-ai/compat";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
@@ -89,6 +89,7 @@ describe.skipIf(!API_KEY)("Compaction extensions", () => {
const model = getModel("anthropic", "claude-sonnet-4-5")!;
const agent = new Agent({
getApiKey: () => API_KEY,
streamFunction: streamSimple,
initialState: {
model,
systemPrompt: "You are a helpful assistant. Be concise.",
@@ -114,7 +114,7 @@ async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs:
systemPrompt: "Test",
tools: [],
},
streamFn: (_model, _context, _options) => {
streamFunction: (_model, _context, _options) => {
const stream = new MockAssistantStream();
queueMicrotask(() => {
stream.push({ type: "start", partial: createAssistantMessage("") });
@@ -126,7 +126,7 @@ describe("createAgentSession provider attribution headers", () => {
});
try {
const stream = await session.agent.streamFn(
const stream = await session.agent.streamFunction(
model,
{ messages: [] },
{
@@ -114,7 +114,7 @@ describe("createAgentSession stream options", () => {
});
try {
const stream = await session.agent.streamFn(model, { messages: [] }, requestOptions);
const stream = await session.agent.streamFunction(model, { messages: [] }, requestOptions);
await stream.result();
return capturedOptions;
} finally {
@@ -49,7 +49,7 @@ function createAssistant(
function useSummaryStreamFn(harness: Harness, summary: string): () => number {
let callCount = 0;
harness.session.agent.streamFn = (model) => {
harness.session.agent.streamFunction = (model) => {
callCount++;
const stream = createAssistantMessageEventStream();
queueMicrotask(() => {
+2 -1
View File
@@ -14,7 +14,7 @@ import type {
FauxResponseStep,
Model,
} from "@earendil-works/pi-ai/compat";
import { registerFauxProvider } from "@earendil-works/pi-ai/compat";
import { registerFauxProvider, streamSimple } from "@earendil-works/pi-ai/compat";
import { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.ts";
import { AuthStorage } from "../../src/core/auth-storage.ts";
import type { ExtensionRunner } from "../../src/core/extensions/index.ts";
@@ -137,6 +137,7 @@ export async function createHarness(options: HarnessOptions = {}): Promise<Harne
const agent = new Agent({
getApiKey: () => (withConfiguredAuth ? "faux-key" : undefined),
streamFunction: streamSimple,
initialState: {
model,
systemPrompt: options.systemPrompt ?? "You are a test assistant.",
@@ -2,7 +2,7 @@ import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import { fauxAssistantMessage, registerFauxProvider } from "@earendil-works/pi-ai/compat";
import { fauxAssistantMessage, registerFauxProvider, streamSimple } from "@earendil-works/pi-ai/compat";
import { afterEach, describe, expect, it } from "vitest";
import { AgentSession } from "../../../src/core/agent-session.ts";
import { AuthStorage } from "../../../src/core/auth-storage.ts";
@@ -61,6 +61,7 @@ describe("regression #5596: missing configured theme export", () => {
tools: [],
},
convertToLlm,
streamFunction: streamSimple,
});
const session = new AgentSession({
agent,
@@ -17,7 +17,7 @@ describe("issue #6324 branch summary ambient auth", () => {
harnesses.push(harness);
let streamCallCount = 0;
harness.session.agent.streamFn = (model, _context, options) => {
harness.session.agent.streamFunction = (model, _context, options) => {
streamCallCount++;
expect(options?.apiKey).toBeUndefined();
+1 -1
View File
@@ -378,7 +378,7 @@ async function createHarnessWithResourceLoader(
systemPrompt: options.systemPrompt ?? "You are a test assistant.",
tools: options.tools ?? [],
},
streamFn,
streamFunction: streamFn,
});
const sessionManager = SessionManager.inMemory();
+2 -1
View File
@@ -8,7 +8,7 @@ import { homedir, tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { Agent } from "@earendil-works/pi-agent-core";
import type { OAuthCredentials } from "@earendil-works/pi-ai";
import { getModel } from "@earendil-works/pi-ai/compat";
import { getModel, streamSimple } from "@earendil-works/pi-ai/compat";
import { builtinProviders } from "@earendil-works/pi-ai/providers/all";
import { AgentSession } from "../src/core/agent-session.ts";
import { AuthStorage } from "../src/core/auth-storage.ts";
@@ -246,6 +246,7 @@ export async function createTestSession(options: TestSessionOptions = {}): Promi
systemPrompt: options.systemPrompt ?? "You are a helpful assistant. Be extremely concise.",
tools: createCodingTools(process.cwd()),
},
streamFunction: streamSimple,
});
const sessionManager = options.inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir);