@@ -5,6 +5,7 @@
|
||||
### Breaking Changes
|
||||
|
||||
- Moved the `uuidv7` export to `@earendil-works/pi-ai`.
|
||||
- Replaced the optional `Agent` `streamFn` fallback with a required `streamFunction` and made low-level loop stream functions required, preventing `@earendil-works/pi-ai/compat` and all built-in providers from entering selective-provider bundles ([#6851](https://github.com/earendil-works/pi/issues/6851)).
|
||||
|
||||
### Added
|
||||
|
||||
|
||||
+28
-13
@@ -12,13 +12,20 @@ npm install @earendil-works/pi-agent-core
|
||||
|
||||
```typescript
|
||||
import { Agent } from "@earendil-works/pi-agent-core";
|
||||
import { getModel } from "@earendil-works/pi-ai";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
|
||||
|
||||
const models = createModels();
|
||||
models.setProvider(anthropicProvider());
|
||||
const model = models.getModel("anthropic", "claude-sonnet-4-6");
|
||||
if (!model) throw new Error("Model not found");
|
||||
|
||||
const agent = new Agent({
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: getModel("anthropic", "claude-sonnet-4-20250514"),
|
||||
model,
|
||||
},
|
||||
streamFunction: models.streamSimple.bind(models),
|
||||
});
|
||||
|
||||
agent.subscribe((event) => {
|
||||
@@ -115,13 +122,19 @@ Tools can also return `terminate: true` to hint that the automatic follow-up LLM
|
||||
Low-level loop callers can set `shouldStopAfterTurn` to stop gracefully after the current turn completes:
|
||||
|
||||
```typescript
|
||||
const stream = agentLoop(prompts, context, {
|
||||
model,
|
||||
convertToLlm,
|
||||
shouldStopAfterTurn: async ({ message, toolResults, context, newMessages }) => {
|
||||
return shouldCompactBeforeNextTurn(context.messages);
|
||||
const stream = agentLoop(
|
||||
prompts,
|
||||
context,
|
||||
{
|
||||
model,
|
||||
convertToLlm,
|
||||
shouldStopAfterTurn: async ({ message, toolResults, context, newMessages }) => {
|
||||
return shouldCompactBeforeNextTurn(context.messages);
|
||||
},
|
||||
},
|
||||
});
|
||||
undefined,
|
||||
models.streamSimple.bind(models),
|
||||
);
|
||||
```
|
||||
|
||||
`shouldStopAfterTurn` runs after `turn_end` is emitted and after the assistant response and any tool executions have completed normally. If it returns `true`, the loop emits `agent_end` and exits before polling steering or follow-up queues, and before starting another LLM call. It does not abort the provider stream, does not cancel running tools, and does not alter the assistant message stop reason.
|
||||
@@ -181,8 +194,8 @@ const agent = new Agent({
|
||||
// Follow-up mode: "one-at-a-time" (default) or "all"
|
||||
followUpMode: "one-at-a-time",
|
||||
|
||||
// Custom stream function (for proxy backends)
|
||||
streamFn: streamProxy,
|
||||
// Required stream function
|
||||
streamFunction: models.streamSimple.bind(models),
|
||||
|
||||
// Session ID for provider caching
|
||||
sessionId: "session-123",
|
||||
@@ -369,6 +382,7 @@ Handle custom types in `convertToLlm`:
|
||||
|
||||
```typescript
|
||||
const agent = new Agent({
|
||||
streamFunction: models.streamSimple.bind(models),
|
||||
convertToLlm: (messages) => messages.flatMap(m => {
|
||||
if (m.role === "notification") return []; // Filter out
|
||||
return [m];
|
||||
@@ -439,7 +453,7 @@ For browser apps that proxy through a backend:
|
||||
import { Agent, streamProxy } from "@earendil-works/pi-agent-core";
|
||||
|
||||
const agent = new Agent({
|
||||
streamFn: (model, context, options) =>
|
||||
streamFunction: (model, context, options) =>
|
||||
streamProxy(model, context, {
|
||||
...options,
|
||||
authToken: "...",
|
||||
@@ -471,12 +485,13 @@ const config: AgentLoopConfig = {
|
||||
|
||||
const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };
|
||||
|
||||
for await (const event of agentLoop([userMessage], context, config)) {
|
||||
const streamFunction = models.streamSimple.bind(models);
|
||||
for await (const event of agentLoop([userMessage], context, config, undefined, streamFunction)) {
|
||||
console.log(event.type);
|
||||
}
|
||||
|
||||
// Continue from existing context
|
||||
for await (const event of agentLoopContinue(context, config)) {
|
||||
for await (const event of agentLoopContinue(context, config, undefined, streamFunction)) {
|
||||
console.log(event.type);
|
||||
}
|
||||
```
|
||||
|
||||
@@ -7,10 +7,9 @@ import {
|
||||
type AssistantMessage,
|
||||
type Context,
|
||||
EventStream,
|
||||
streamSimple,
|
||||
type ToolResultMessage,
|
||||
validateToolArguments,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
} from "@earendil-works/pi-ai";
|
||||
import type {
|
||||
AgentContext,
|
||||
AgentEvent,
|
||||
@@ -32,8 +31,8 @@ export function agentLoop(
|
||||
prompts: AgentMessage[],
|
||||
context: AgentContext,
|
||||
config: AgentLoopConfig,
|
||||
signal?: AbortSignal,
|
||||
streamFn?: StreamFn,
|
||||
signal: AbortSignal | undefined,
|
||||
streamFunction: StreamFn,
|
||||
): EventStream<AgentEvent, AgentMessage[]> {
|
||||
const stream = createAgentStream();
|
||||
|
||||
@@ -45,7 +44,7 @@ export function agentLoop(
|
||||
stream.push(event);
|
||||
},
|
||||
signal,
|
||||
streamFn,
|
||||
streamFunction,
|
||||
).then((messages) => {
|
||||
stream.end(messages);
|
||||
});
|
||||
@@ -64,8 +63,8 @@ export function agentLoop(
|
||||
export function agentLoopContinue(
|
||||
context: AgentContext,
|
||||
config: AgentLoopConfig,
|
||||
signal?: AbortSignal,
|
||||
streamFn?: StreamFn,
|
||||
signal: AbortSignal | undefined,
|
||||
streamFunction: StreamFn,
|
||||
): EventStream<AgentEvent, AgentMessage[]> {
|
||||
if (context.messages.length === 0) {
|
||||
throw new Error("Cannot continue: no messages in context");
|
||||
@@ -84,7 +83,7 @@ export function agentLoopContinue(
|
||||
stream.push(event);
|
||||
},
|
||||
signal,
|
||||
streamFn,
|
||||
streamFunction,
|
||||
).then((messages) => {
|
||||
stream.end(messages);
|
||||
});
|
||||
@@ -97,8 +96,8 @@ export async function runAgentLoop(
|
||||
context: AgentContext,
|
||||
config: AgentLoopConfig,
|
||||
emit: AgentEventSink,
|
||||
signal?: AbortSignal,
|
||||
streamFn?: StreamFn,
|
||||
signal: AbortSignal | undefined,
|
||||
streamFunction: StreamFn,
|
||||
): Promise<AgentMessage[]> {
|
||||
const newMessages: AgentMessage[] = [...prompts];
|
||||
const currentContext: AgentContext = {
|
||||
@@ -113,7 +112,7 @@ export async function runAgentLoop(
|
||||
await emit({ type: "message_end", message: prompt });
|
||||
}
|
||||
|
||||
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
||||
await runLoop(currentContext, newMessages, config, signal, emit, streamFunction);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
@@ -121,8 +120,8 @@ export async function runAgentLoopContinue(
|
||||
context: AgentContext,
|
||||
config: AgentLoopConfig,
|
||||
emit: AgentEventSink,
|
||||
signal?: AbortSignal,
|
||||
streamFn?: StreamFn,
|
||||
signal: AbortSignal | undefined,
|
||||
streamFunction: StreamFn,
|
||||
): Promise<AgentMessage[]> {
|
||||
if (context.messages.length === 0) {
|
||||
throw new Error("Cannot continue: no messages in context");
|
||||
@@ -138,7 +137,7 @@ export async function runAgentLoopContinue(
|
||||
await emit({ type: "agent_start" });
|
||||
await emit({ type: "turn_start" });
|
||||
|
||||
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
||||
await runLoop(currentContext, newMessages, config, signal, emit, streamFunction);
|
||||
return newMessages;
|
||||
}
|
||||
|
||||
@@ -158,7 +157,7 @@ async function runLoop(
|
||||
initialConfig: AgentLoopConfig,
|
||||
signal: AbortSignal | undefined,
|
||||
emit: AgentEventSink,
|
||||
streamFn?: StreamFn,
|
||||
streamFunction: StreamFn,
|
||||
): Promise<void> {
|
||||
let currentContext = initialContext;
|
||||
let config = initialConfig;
|
||||
@@ -190,7 +189,7 @@ async function runLoop(
|
||||
}
|
||||
|
||||
// Stream assistant response
|
||||
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
|
||||
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
|
||||
newMessages.push(message);
|
||||
|
||||
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
||||
@@ -283,7 +282,7 @@ async function streamAssistantResponse(
|
||||
config: AgentLoopConfig,
|
||||
signal: AbortSignal | undefined,
|
||||
emit: AgentEventSink,
|
||||
streamFn?: StreamFn,
|
||||
streamFunction: StreamFn,
|
||||
): Promise<AssistantMessage> {
|
||||
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
||||
let messages = context.messages;
|
||||
@@ -301,8 +300,6 @@ async function streamAssistantResponse(
|
||||
tools: context.tools,
|
||||
};
|
||||
|
||||
const streamFunction = streamFn || streamSimple;
|
||||
|
||||
// Resolve API key (important for expiring tokens)
|
||||
const resolvedApiKey =
|
||||
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
||||
|
||||
+15
-16
@@ -1,13 +1,12 @@
|
||||
import {
|
||||
type ImageContent,
|
||||
type Message,
|
||||
type Model,
|
||||
type SimpleStreamOptions,
|
||||
streamSimple,
|
||||
type TextContent,
|
||||
type ThinkingBudgets,
|
||||
type Transport,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
import type {
|
||||
ImageContent,
|
||||
Message,
|
||||
Model,
|
||||
SimpleStreamOptions,
|
||||
TextContent,
|
||||
ThinkingBudgets,
|
||||
Transport,
|
||||
} from "@earendil-works/pi-ai";
|
||||
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts";
|
||||
import type {
|
||||
AfterToolCallContext,
|
||||
@@ -98,7 +97,7 @@ export interface AgentOptions {
|
||||
initialState?: Partial<Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage">>;
|
||||
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
||||
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
||||
streamFn?: StreamFn;
|
||||
streamFunction: StreamFn;
|
||||
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
||||
onPayload?: SimpleStreamOptions["onPayload"];
|
||||
onResponse?: SimpleStreamOptions["onResponse"];
|
||||
@@ -176,7 +175,7 @@ export class Agent {
|
||||
|
||||
public convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
||||
public transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
||||
public streamFn: StreamFn;
|
||||
public streamFunction: StreamFn;
|
||||
public getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
||||
public onPayload?: SimpleStreamOptions["onPayload"];
|
||||
public onResponse?: SimpleStreamOptions["onResponse"];
|
||||
@@ -207,11 +206,11 @@ export class Agent {
|
||||
/** Tool execution strategy for assistant messages that contain multiple tool calls. */
|
||||
public toolExecution: ToolExecutionMode;
|
||||
|
||||
constructor(options: AgentOptions = {}) {
|
||||
constructor(options: AgentOptions) {
|
||||
this._state = createMutableAgentState(options.initialState);
|
||||
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
|
||||
this.transformContext = options.transformContext;
|
||||
this.streamFn = options.streamFn ?? streamSimple;
|
||||
this.streamFunction = options.streamFunction;
|
||||
this.getApiKey = options.getApiKey;
|
||||
this.onPayload = options.onPayload;
|
||||
this.onResponse = options.onResponse;
|
||||
@@ -404,7 +403,7 @@ export class Agent {
|
||||
this.createLoopConfig(options),
|
||||
(event) => this.processEvents(event),
|
||||
signal,
|
||||
this.streamFn,
|
||||
this.streamFunction,
|
||||
);
|
||||
});
|
||||
}
|
||||
@@ -416,7 +415,7 @@ export class Agent {
|
||||
this.createLoopConfig(),
|
||||
(event) => this.processEvents(event),
|
||||
signal,
|
||||
this.streamFn,
|
||||
this.streamFunction,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -84,12 +84,12 @@ export interface ProxyStreamOptions extends ProxySerializableStreamOptions {
|
||||
* The server strips the partial field from delta events to reduce bandwidth.
|
||||
* We reconstruct the partial message client-side.
|
||||
*
|
||||
* Use this as the `streamFn` option when creating an Agent that needs to go through a proxy.
|
||||
* Use this as the `streamFunction` option when creating an Agent that needs to go through a proxy.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const agent = new Agent({
|
||||
* streamFn: (model, context, options) =>
|
||||
* streamFunction: (model, context, options) =>
|
||||
* streamProxy(model, context, {
|
||||
* ...options,
|
||||
* authToken: await getAuthToken(),
|
||||
|
||||
@@ -1342,7 +1342,11 @@ describe("agentLoopContinue with AgentMessage", () => {
|
||||
convertToLlm: identityConverter,
|
||||
};
|
||||
|
||||
expect(() => agentLoopContinue(context, config)).toThrow("Cannot continue: no messages in context");
|
||||
expect(() =>
|
||||
agentLoopContinue(context, config, undefined, () => {
|
||||
throw new Error("Unexpected stream call");
|
||||
}),
|
||||
).toThrow("Cannot continue: no messages in context");
|
||||
});
|
||||
|
||||
it("should continue from existing context without emitting user message events", async () => {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { type AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai/compat";
|
||||
import { Type } from "typebox";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { Agent, type AgentEvent, type AgentTool, type AgentToolUpdateCallback } from "../src/index.ts";
|
||||
import { Agent, type AgentEvent, type AgentTool, type AgentToolUpdateCallback, type StreamFn } from "../src/index.ts";
|
||||
|
||||
// Mock stream that mimics AssistantMessageEventStream
|
||||
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
||||
@@ -59,6 +59,10 @@ function createAssistantToolUseMessage(content: ToolCallContent[]): AssistantMes
|
||||
};
|
||||
}
|
||||
|
||||
const unusedStreamFunction: StreamFn = () => {
|
||||
throw new Error("Unexpected stream call");
|
||||
};
|
||||
|
||||
function createDeferred(): {
|
||||
promise: Promise<void>;
|
||||
resolve: () => void;
|
||||
@@ -72,7 +76,7 @@ function createDeferred(): {
|
||||
|
||||
describe("Agent", () => {
|
||||
it("should create an agent instance with default state", () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
expect(agent.state).toBeDefined();
|
||||
expect(agent.state.systemPrompt).toBe("");
|
||||
@@ -89,6 +93,7 @@ describe("Agent", () => {
|
||||
it("should create an agent instance with custom initial state", () => {
|
||||
const customModel = getModel("openai", "gpt-4o-mini");
|
||||
const agent = new Agent({
|
||||
streamFunction: unusedStreamFunction,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: customModel,
|
||||
@@ -102,7 +107,7 @@ describe("Agent", () => {
|
||||
});
|
||||
|
||||
it("should subscribe to events", () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
let eventCount = 0;
|
||||
const unsubscribe = agent.subscribe((_event) => {
|
||||
@@ -125,7 +130,7 @@ describe("Agent", () => {
|
||||
|
||||
it("emits full lifecycle events for thrown run failures", async () => {
|
||||
const agent = new Agent({
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
throw new Error("provider exploded");
|
||||
},
|
||||
});
|
||||
@@ -157,7 +162,7 @@ describe("Agent", () => {
|
||||
it("should await async subscribers before prompt resolves", async () => {
|
||||
const barrier = createDeferred();
|
||||
const agent = new Agent({
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("ok") });
|
||||
@@ -195,7 +200,7 @@ describe("Agent", () => {
|
||||
it("waitForIdle should wait for async subscribers", async () => {
|
||||
const barrier = createDeferred();
|
||||
const agent = new Agent({
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("ok") });
|
||||
@@ -230,7 +235,7 @@ describe("Agent", () => {
|
||||
it("should pass the active abort signal to subscribers", async () => {
|
||||
let receivedSignal: AbortSignal | undefined;
|
||||
const agent = new Agent({
|
||||
streamFn: (_model, _context, options) => {
|
||||
streamFunction: (_model, _context, options) => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({ type: "start", partial: createAssistantMessage("") });
|
||||
@@ -293,7 +298,7 @@ describe("Agent", () => {
|
||||
};
|
||||
const agent = new Agent({
|
||||
initialState: { tools: [tool] },
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({
|
||||
@@ -368,7 +373,7 @@ describe("Agent", () => {
|
||||
};
|
||||
const agent = new Agent({
|
||||
initialState: { tools: [settledTool, slowTool] },
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({
|
||||
@@ -407,7 +412,7 @@ describe("Agent", () => {
|
||||
});
|
||||
|
||||
it("should update state with mutators", () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
// Test setSystemPrompt
|
||||
agent.state.systemPrompt = "Custom prompt";
|
||||
@@ -446,7 +451,7 @@ describe("Agent", () => {
|
||||
});
|
||||
|
||||
it("should support steering message queue", async () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
const message = { role: "user" as const, content: "Steering message", timestamp: Date.now() };
|
||||
agent.steer(message);
|
||||
@@ -456,7 +461,7 @@ describe("Agent", () => {
|
||||
});
|
||||
|
||||
it("should support follow-up message queue", async () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
const message = { role: "user" as const, content: "Follow-up message", timestamp: Date.now() };
|
||||
agent.followUp(message);
|
||||
@@ -466,7 +471,7 @@ describe("Agent", () => {
|
||||
});
|
||||
|
||||
it("should handle abort controller", () => {
|
||||
const agent = new Agent();
|
||||
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||
|
||||
// Should not throw even if nothing is running
|
||||
expect(() => agent.abort()).not.toThrow();
|
||||
@@ -476,7 +481,7 @@ describe("Agent", () => {
|
||||
let abortSignal: AbortSignal | undefined;
|
||||
const agent = new Agent({
|
||||
// Use a stream function that responds to abort
|
||||
streamFn: (_model, _context, options) => {
|
||||
streamFunction: (_model, _context, options) => {
|
||||
abortSignal = options?.signal;
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
@@ -515,7 +520,7 @@ describe("Agent", () => {
|
||||
it("should throw when continue() called while streaming", async () => {
|
||||
let abortSignal: AbortSignal | undefined;
|
||||
const agent = new Agent({
|
||||
streamFn: (_model, _context, options) => {
|
||||
streamFunction: (_model, _context, options) => {
|
||||
abortSignal = options?.signal;
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
@@ -550,7 +555,7 @@ describe("Agent", () => {
|
||||
|
||||
it("continue() should process queued follow-up messages after an assistant turn", async () => {
|
||||
const agent = new Agent({
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("Processed") });
|
||||
@@ -589,7 +594,7 @@ describe("Agent", () => {
|
||||
it("continue() should keep one-at-a-time steering semantics from assistant tail", async () => {
|
||||
let responseCount = 0;
|
||||
const agent = new Agent({
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
const stream = new MockAssistantStream();
|
||||
responseCount++;
|
||||
queueMicrotask(() => {
|
||||
@@ -647,7 +652,7 @@ describe("Agent", () => {
|
||||
sawAbortSignal = signal instanceof AbortSignal;
|
||||
return undefined;
|
||||
},
|
||||
streamFn: () => {
|
||||
streamFunction: () => {
|
||||
requestCount++;
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
@@ -671,11 +676,11 @@ describe("Agent", () => {
|
||||
expect(sawAbortSignal).toBe(true);
|
||||
});
|
||||
|
||||
it("forwards sessionId to streamFn options", async () => {
|
||||
it("forwards sessionId to streamFunction options", async () => {
|
||||
let receivedSessionId: string | undefined;
|
||||
const agent = new Agent({
|
||||
sessionId: "session-abc",
|
||||
streamFn: (_model, _context, options) => {
|
||||
streamFunction: (_model, _context, options) => {
|
||||
receivedSessionId = options?.sessionId;
|
||||
const stream = new MockAssistantStream();
|
||||
queueMicrotask(() => {
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
fauxToolCall,
|
||||
type Model,
|
||||
registerFauxProvider,
|
||||
streamSimple,
|
||||
type ToolResultMessage,
|
||||
type UserMessage,
|
||||
} from "@earendil-works/pi-ai/compat";
|
||||
@@ -37,6 +38,7 @@ afterEach(() => {
|
||||
|
||||
async function basicPrompt(model: Model<string>) {
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant. Keep your responses concise.",
|
||||
model,
|
||||
@@ -59,6 +61,7 @@ async function basicPrompt(model: Model<string>) {
|
||||
|
||||
async function toolExecution(model: Model<string>) {
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant. Always use the calculator tool for math.",
|
||||
model,
|
||||
@@ -98,6 +101,7 @@ async function toolExecution(model: Model<string>) {
|
||||
|
||||
async function abortExecution(model: Model<string>) {
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model,
|
||||
@@ -125,6 +129,7 @@ async function abortExecution(model: Model<string>) {
|
||||
|
||||
async function stateUpdates(model: Model<string>) {
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model,
|
||||
@@ -157,6 +162,7 @@ async function stateUpdates(model: Model<string>) {
|
||||
|
||||
async function multiTurnConversation(model: Model<string>) {
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model,
|
||||
@@ -238,6 +244,7 @@ describe("Agent integration with faux provider", () => {
|
||||
faux.setResponses([fauxAssistantMessage([fauxThinking("step by step"), fauxText("4")])]);
|
||||
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant.",
|
||||
model: faux.getModel(),
|
||||
@@ -262,6 +269,7 @@ describe("Agent.continue() with faux provider", () => {
|
||||
it("throws when no messages in context", async () => {
|
||||
const faux = createFauxRegistration();
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "Test",
|
||||
model: faux.getModel(),
|
||||
@@ -275,6 +283,7 @@ describe("Agent.continue() with faux provider", () => {
|
||||
const faux = createFauxRegistration();
|
||||
const model = faux.getModel();
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "Test",
|
||||
model,
|
||||
@@ -309,6 +318,7 @@ describe("Agent.continue() with faux provider", () => {
|
||||
const faux = createFauxRegistration();
|
||||
faux.setResponses([fauxAssistantMessage("HELLO WORLD")]);
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt: "You are a helpful assistant. Follow instructions exactly.",
|
||||
model: faux.getModel(),
|
||||
@@ -343,6 +353,7 @@ describe("Agent.continue() with faux provider", () => {
|
||||
const model = faux.getModel();
|
||||
faux.setResponses([fauxAssistantMessage("The answer is 8.")]);
|
||||
const agent = new Agent({
|
||||
streamFunction: streamSimple,
|
||||
initialState: {
|
||||
systemPrompt:
|
||||
"You are a helpful assistant. After getting a calculation result, state the answer clearly.",
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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,
|
||||
|
||||
+1
-1
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Agent } from "@earendil-works/pi-agent-core";
|
||||
import { createModels } from "@earendil-works/pi-ai";
|
||||
import { anthropicProvider } from "@earendil-works/pi-ai/providers/anthropic";
|
||||
|
||||
const models = createModels();
|
||||
models.setProvider(anthropicProvider());
|
||||
const model = models.getModel("anthropic", "claude-sonnet-4-5");
|
||||
if (!model) throw new Error("Anthropic smoke-test model not found");
|
||||
|
||||
export const agent = new Agent({
|
||||
initialState: { model },
|
||||
streamFunction: models.streamSimple.bind(models),
|
||||
});
|
||||
@@ -1,5 +1,5 @@
|
||||
import { createAssistantMessageEventStream, Type } from "@earendil-works/pi-ai";
|
||||
import { complete, getModel, getProviders } from "@earendil-works/pi-ai/compat";
|
||||
import { complete, getModel, getProviders, streamSimple } from "@earendil-works/pi-ai/compat";
|
||||
import {
|
||||
Agent,
|
||||
bashExecutionToText,
|
||||
@@ -24,7 +24,7 @@ const model = getModel("google", "gemini-2.5-flash");
|
||||
const schema = Type.Object({ prompt: Type.String() });
|
||||
const stream = createAssistantMessageEventStream();
|
||||
|
||||
const agent = new Agent({ initialState: { model } });
|
||||
const agent = new Agent({ initialState: { model }, streamFunction: streamSimple });
|
||||
agent.steer({ role: "user", content: [{ type: "text", text: "queued" }], timestamp: 0 });
|
||||
const repo = new InMemorySessionRepo();
|
||||
const result = getOrThrow(ok({ value: 1 }));
|
||||
|
||||
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
|
||||
import { build } from "esbuild";
|
||||
|
||||
const outputPath = join(tmpdir(), "pi-browser-smoke.js");
|
||||
const agentTreeshakeOutputPath = join(tmpdir(), "pi-agent-treeshake-smoke.js");
|
||||
const errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log");
|
||||
const generatedCatalogDataDir = join(process.cwd(), "packages/ai/src/providers/data");
|
||||
|
||||
@@ -23,6 +24,22 @@ const generatedCatalogDataPlugin = {
|
||||
},
|
||||
};
|
||||
|
||||
function normalizePath(path) {
|
||||
return path.replaceAll("\\", "/");
|
||||
}
|
||||
|
||||
function findInput(inputs, suffix) {
|
||||
return Object.keys(inputs).find((input) => {
|
||||
const normalized = normalizePath(input);
|
||||
return normalized === suffix || normalized.endsWith(`/${suffix}`);
|
||||
});
|
||||
}
|
||||
|
||||
function includesNodePackage(inputs, packageName) {
|
||||
const marker = `node_modules/${packageName}/`;
|
||||
return Object.keys(inputs).some((input) => normalizePath(input).includes(marker));
|
||||
}
|
||||
|
||||
try {
|
||||
await build({
|
||||
entryPoints: ["scripts/browser-smoke-entry.ts"],
|
||||
@@ -33,6 +50,47 @@ try {
|
||||
outfile: outputPath,
|
||||
plugins: [generatedCatalogDataPlugin],
|
||||
});
|
||||
|
||||
const agentTreeshakeBuild = await build({
|
||||
entryPoints: ["scripts/agent-treeshake-smoke-entry.ts"],
|
||||
bundle: true,
|
||||
platform: "browser",
|
||||
format: "esm",
|
||||
logLevel: "silent",
|
||||
metafile: true,
|
||||
outfile: agentTreeshakeOutputPath,
|
||||
plugins: [generatedCatalogDataPlugin],
|
||||
write: false,
|
||||
});
|
||||
const inputs = agentTreeshakeBuild.metafile.inputs;
|
||||
for (const forbiddenInput of [
|
||||
"packages/ai/src/compat.ts",
|
||||
"packages/ai/src/models.generated.ts",
|
||||
"packages/ai/src/providers/all.ts",
|
||||
]) {
|
||||
const includedInput = findInput(inputs, forbiddenInput);
|
||||
if (includedInput) {
|
||||
throw new Error(`Agent selective-provider bundle unexpectedly includes ${includedInput}`);
|
||||
}
|
||||
}
|
||||
|
||||
const aiSdkPackages = [
|
||||
"@anthropic-ai/sdk",
|
||||
"@aws-sdk/client-bedrock-runtime",
|
||||
"@google/genai",
|
||||
"@mistralai/mistralai",
|
||||
"openai",
|
||||
];
|
||||
const includedAiSdkPackages = aiSdkPackages.filter((packageName) => includesNodePackage(inputs, packageName));
|
||||
if (
|
||||
includedAiSdkPackages.length !== 1 ||
|
||||
includedAiSdkPackages[0] !== "@anthropic-ai/sdk"
|
||||
) {
|
||||
throw new Error(
|
||||
`Agent selective-provider bundle SDKs: expected only @anthropic-ai/sdk, found ${includedAiSdkPackages.join(", ") || "none"}`,
|
||||
);
|
||||
}
|
||||
|
||||
process.exit(0);
|
||||
} catch (error) {
|
||||
let detailedErrors = "";
|
||||
|
||||
Reference in New Issue
Block a user