@@ -5,6 +5,7 @@
|
|||||||
### Breaking Changes
|
### Breaking Changes
|
||||||
|
|
||||||
- Moved the `uuidv7` export to `@earendil-works/pi-ai`.
|
- 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
|
### Added
|
||||||
|
|
||||||
|
|||||||
+28
-13
@@ -12,13 +12,20 @@ npm install @earendil-works/pi-agent-core
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
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({
|
const agent = new Agent({
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model: getModel("anthropic", "claude-sonnet-4-20250514"),
|
model,
|
||||||
},
|
},
|
||||||
|
streamFunction: models.streamSimple.bind(models),
|
||||||
});
|
});
|
||||||
|
|
||||||
agent.subscribe((event) => {
|
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:
|
Low-level loop callers can set `shouldStopAfterTurn` to stop gracefully after the current turn completes:
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const stream = agentLoop(prompts, context, {
|
const stream = agentLoop(
|
||||||
model,
|
prompts,
|
||||||
convertToLlm,
|
context,
|
||||||
shouldStopAfterTurn: async ({ message, toolResults, context, newMessages }) => {
|
{
|
||||||
return shouldCompactBeforeNextTurn(context.messages);
|
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.
|
`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"
|
// Follow-up mode: "one-at-a-time" (default) or "all"
|
||||||
followUpMode: "one-at-a-time",
|
followUpMode: "one-at-a-time",
|
||||||
|
|
||||||
// Custom stream function (for proxy backends)
|
// Required stream function
|
||||||
streamFn: streamProxy,
|
streamFunction: models.streamSimple.bind(models),
|
||||||
|
|
||||||
// Session ID for provider caching
|
// Session ID for provider caching
|
||||||
sessionId: "session-123",
|
sessionId: "session-123",
|
||||||
@@ -369,6 +382,7 @@ Handle custom types in `convertToLlm`:
|
|||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: models.streamSimple.bind(models),
|
||||||
convertToLlm: (messages) => messages.flatMap(m => {
|
convertToLlm: (messages) => messages.flatMap(m => {
|
||||||
if (m.role === "notification") return []; // Filter out
|
if (m.role === "notification") return []; // Filter out
|
||||||
return [m];
|
return [m];
|
||||||
@@ -439,7 +453,7 @@ For browser apps that proxy through a backend:
|
|||||||
import { Agent, streamProxy } from "@earendil-works/pi-agent-core";
|
import { Agent, streamProxy } from "@earendil-works/pi-agent-core";
|
||||||
|
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: (model, context, options) =>
|
streamFunction: (model, context, options) =>
|
||||||
streamProxy(model, context, {
|
streamProxy(model, context, {
|
||||||
...options,
|
...options,
|
||||||
authToken: "...",
|
authToken: "...",
|
||||||
@@ -471,12 +485,13 @@ const config: AgentLoopConfig = {
|
|||||||
|
|
||||||
const userMessage = { role: "user", content: "Hello", timestamp: Date.now() };
|
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);
|
console.log(event.type);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Continue from existing context
|
// 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);
|
console.log(event.type);
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -7,10 +7,9 @@ import {
|
|||||||
type AssistantMessage,
|
type AssistantMessage,
|
||||||
type Context,
|
type Context,
|
||||||
EventStream,
|
EventStream,
|
||||||
streamSimple,
|
|
||||||
type ToolResultMessage,
|
type ToolResultMessage,
|
||||||
validateToolArguments,
|
validateToolArguments,
|
||||||
} from "@earendil-works/pi-ai/compat";
|
} from "@earendil-works/pi-ai";
|
||||||
import type {
|
import type {
|
||||||
AgentContext,
|
AgentContext,
|
||||||
AgentEvent,
|
AgentEvent,
|
||||||
@@ -32,8 +31,8 @@ export function agentLoop(
|
|||||||
prompts: AgentMessage[],
|
prompts: AgentMessage[],
|
||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: AgentLoopConfig,
|
config: AgentLoopConfig,
|
||||||
signal?: AbortSignal,
|
signal: AbortSignal | undefined,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): EventStream<AgentEvent, AgentMessage[]> {
|
): EventStream<AgentEvent, AgentMessage[]> {
|
||||||
const stream = createAgentStream();
|
const stream = createAgentStream();
|
||||||
|
|
||||||
@@ -45,7 +44,7 @@ export function agentLoop(
|
|||||||
stream.push(event);
|
stream.push(event);
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
streamFn,
|
streamFunction,
|
||||||
).then((messages) => {
|
).then((messages) => {
|
||||||
stream.end(messages);
|
stream.end(messages);
|
||||||
});
|
});
|
||||||
@@ -64,8 +63,8 @@ export function agentLoop(
|
|||||||
export function agentLoopContinue(
|
export function agentLoopContinue(
|
||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: AgentLoopConfig,
|
config: AgentLoopConfig,
|
||||||
signal?: AbortSignal,
|
signal: AbortSignal | undefined,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): EventStream<AgentEvent, AgentMessage[]> {
|
): EventStream<AgentEvent, AgentMessage[]> {
|
||||||
if (context.messages.length === 0) {
|
if (context.messages.length === 0) {
|
||||||
throw new Error("Cannot continue: no messages in context");
|
throw new Error("Cannot continue: no messages in context");
|
||||||
@@ -84,7 +83,7 @@ export function agentLoopContinue(
|
|||||||
stream.push(event);
|
stream.push(event);
|
||||||
},
|
},
|
||||||
signal,
|
signal,
|
||||||
streamFn,
|
streamFunction,
|
||||||
).then((messages) => {
|
).then((messages) => {
|
||||||
stream.end(messages);
|
stream.end(messages);
|
||||||
});
|
});
|
||||||
@@ -97,8 +96,8 @@ export async function runAgentLoop(
|
|||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: AgentLoopConfig,
|
config: AgentLoopConfig,
|
||||||
emit: AgentEventSink,
|
emit: AgentEventSink,
|
||||||
signal?: AbortSignal,
|
signal: AbortSignal | undefined,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): Promise<AgentMessage[]> {
|
): Promise<AgentMessage[]> {
|
||||||
const newMessages: AgentMessage[] = [...prompts];
|
const newMessages: AgentMessage[] = [...prompts];
|
||||||
const currentContext: AgentContext = {
|
const currentContext: AgentContext = {
|
||||||
@@ -113,7 +112,7 @@ export async function runAgentLoop(
|
|||||||
await emit({ type: "message_end", message: prompt });
|
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;
|
return newMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,8 +120,8 @@ export async function runAgentLoopContinue(
|
|||||||
context: AgentContext,
|
context: AgentContext,
|
||||||
config: AgentLoopConfig,
|
config: AgentLoopConfig,
|
||||||
emit: AgentEventSink,
|
emit: AgentEventSink,
|
||||||
signal?: AbortSignal,
|
signal: AbortSignal | undefined,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): Promise<AgentMessage[]> {
|
): Promise<AgentMessage[]> {
|
||||||
if (context.messages.length === 0) {
|
if (context.messages.length === 0) {
|
||||||
throw new Error("Cannot continue: no messages in context");
|
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: "agent_start" });
|
||||||
await emit({ type: "turn_start" });
|
await emit({ type: "turn_start" });
|
||||||
|
|
||||||
await runLoop(currentContext, newMessages, config, signal, emit, streamFn);
|
await runLoop(currentContext, newMessages, config, signal, emit, streamFunction);
|
||||||
return newMessages;
|
return newMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,7 +157,7 @@ async function runLoop(
|
|||||||
initialConfig: AgentLoopConfig,
|
initialConfig: AgentLoopConfig,
|
||||||
signal: AbortSignal | undefined,
|
signal: AbortSignal | undefined,
|
||||||
emit: AgentEventSink,
|
emit: AgentEventSink,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
let currentContext = initialContext;
|
let currentContext = initialContext;
|
||||||
let config = initialConfig;
|
let config = initialConfig;
|
||||||
@@ -190,7 +189,7 @@ async function runLoop(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Stream assistant response
|
// Stream assistant response
|
||||||
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFn);
|
const message = await streamAssistantResponse(currentContext, config, signal, emit, streamFunction);
|
||||||
newMessages.push(message);
|
newMessages.push(message);
|
||||||
|
|
||||||
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
if (message.stopReason === "error" || message.stopReason === "aborted") {
|
||||||
@@ -283,7 +282,7 @@ async function streamAssistantResponse(
|
|||||||
config: AgentLoopConfig,
|
config: AgentLoopConfig,
|
||||||
signal: AbortSignal | undefined,
|
signal: AbortSignal | undefined,
|
||||||
emit: AgentEventSink,
|
emit: AgentEventSink,
|
||||||
streamFn?: StreamFn,
|
streamFunction: StreamFn,
|
||||||
): Promise<AssistantMessage> {
|
): Promise<AssistantMessage> {
|
||||||
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
// Apply context transform if configured (AgentMessage[] → AgentMessage[])
|
||||||
let messages = context.messages;
|
let messages = context.messages;
|
||||||
@@ -301,8 +300,6 @@ async function streamAssistantResponse(
|
|||||||
tools: context.tools,
|
tools: context.tools,
|
||||||
};
|
};
|
||||||
|
|
||||||
const streamFunction = streamFn || streamSimple;
|
|
||||||
|
|
||||||
// Resolve API key (important for expiring tokens)
|
// Resolve API key (important for expiring tokens)
|
||||||
const resolvedApiKey =
|
const resolvedApiKey =
|
||||||
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
(config.getApiKey ? await config.getApiKey(config.model.provider) : undefined) || config.apiKey;
|
||||||
|
|||||||
+15
-16
@@ -1,13 +1,12 @@
|
|||||||
import {
|
import type {
|
||||||
type ImageContent,
|
ImageContent,
|
||||||
type Message,
|
Message,
|
||||||
type Model,
|
Model,
|
||||||
type SimpleStreamOptions,
|
SimpleStreamOptions,
|
||||||
streamSimple,
|
TextContent,
|
||||||
type TextContent,
|
ThinkingBudgets,
|
||||||
type ThinkingBudgets,
|
Transport,
|
||||||
type Transport,
|
} from "@earendil-works/pi-ai";
|
||||||
} from "@earendil-works/pi-ai/compat";
|
|
||||||
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts";
|
import { runAgentLoop, runAgentLoopContinue } from "./agent-loop.ts";
|
||||||
import type {
|
import type {
|
||||||
AfterToolCallContext,
|
AfterToolCallContext,
|
||||||
@@ -98,7 +97,7 @@ export interface AgentOptions {
|
|||||||
initialState?: Partial<Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage">>;
|
initialState?: Partial<Omit<AgentState, "pendingToolCalls" | "isStreaming" | "streamingMessage" | "errorMessage">>;
|
||||||
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
convertToLlm?: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
||||||
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
||||||
streamFn?: StreamFn;
|
streamFunction: StreamFn;
|
||||||
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
||||||
onPayload?: SimpleStreamOptions["onPayload"];
|
onPayload?: SimpleStreamOptions["onPayload"];
|
||||||
onResponse?: SimpleStreamOptions["onResponse"];
|
onResponse?: SimpleStreamOptions["onResponse"];
|
||||||
@@ -176,7 +175,7 @@ export class Agent {
|
|||||||
|
|
||||||
public convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
public convertToLlm: (messages: AgentMessage[]) => Message[] | Promise<Message[]>;
|
||||||
public transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
public transformContext?: (messages: AgentMessage[], signal?: AbortSignal) => Promise<AgentMessage[]>;
|
||||||
public streamFn: StreamFn;
|
public streamFunction: StreamFn;
|
||||||
public getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
public getApiKey?: (provider: string) => Promise<string | undefined> | string | undefined;
|
||||||
public onPayload?: SimpleStreamOptions["onPayload"];
|
public onPayload?: SimpleStreamOptions["onPayload"];
|
||||||
public onResponse?: SimpleStreamOptions["onResponse"];
|
public onResponse?: SimpleStreamOptions["onResponse"];
|
||||||
@@ -207,11 +206,11 @@ export class Agent {
|
|||||||
/** Tool execution strategy for assistant messages that contain multiple tool calls. */
|
/** Tool execution strategy for assistant messages that contain multiple tool calls. */
|
||||||
public toolExecution: ToolExecutionMode;
|
public toolExecution: ToolExecutionMode;
|
||||||
|
|
||||||
constructor(options: AgentOptions = {}) {
|
constructor(options: AgentOptions) {
|
||||||
this._state = createMutableAgentState(options.initialState);
|
this._state = createMutableAgentState(options.initialState);
|
||||||
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
|
this.convertToLlm = options.convertToLlm ?? defaultConvertToLlm;
|
||||||
this.transformContext = options.transformContext;
|
this.transformContext = options.transformContext;
|
||||||
this.streamFn = options.streamFn ?? streamSimple;
|
this.streamFunction = options.streamFunction;
|
||||||
this.getApiKey = options.getApiKey;
|
this.getApiKey = options.getApiKey;
|
||||||
this.onPayload = options.onPayload;
|
this.onPayload = options.onPayload;
|
||||||
this.onResponse = options.onResponse;
|
this.onResponse = options.onResponse;
|
||||||
@@ -404,7 +403,7 @@ export class Agent {
|
|||||||
this.createLoopConfig(options),
|
this.createLoopConfig(options),
|
||||||
(event) => this.processEvents(event),
|
(event) => this.processEvents(event),
|
||||||
signal,
|
signal,
|
||||||
this.streamFn,
|
this.streamFunction,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -416,7 +415,7 @@ export class Agent {
|
|||||||
this.createLoopConfig(),
|
this.createLoopConfig(),
|
||||||
(event) => this.processEvents(event),
|
(event) => this.processEvents(event),
|
||||||
signal,
|
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.
|
* The server strips the partial field from delta events to reduce bandwidth.
|
||||||
* We reconstruct the partial message client-side.
|
* 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
|
* @example
|
||||||
* ```typescript
|
* ```typescript
|
||||||
* const agent = new Agent({
|
* const agent = new Agent({
|
||||||
* streamFn: (model, context, options) =>
|
* streamFunction: (model, context, options) =>
|
||||||
* streamProxy(model, context, {
|
* streamProxy(model, context, {
|
||||||
* ...options,
|
* ...options,
|
||||||
* authToken: await getAuthToken(),
|
* authToken: await getAuthToken(),
|
||||||
|
|||||||
@@ -1342,7 +1342,11 @@ describe("agentLoopContinue with AgentMessage", () => {
|
|||||||
convertToLlm: identityConverter,
|
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 () => {
|
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 AssistantMessage, type AssistantMessageEvent, EventStream, getModel } from "@earendil-works/pi-ai/compat";
|
||||||
import { Type } from "typebox";
|
import { Type } from "typebox";
|
||||||
import { describe, expect, it } from "vitest";
|
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
|
// Mock stream that mimics AssistantMessageEventStream
|
||||||
class MockAssistantStream extends EventStream<AssistantMessageEvent, AssistantMessage> {
|
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(): {
|
function createDeferred(): {
|
||||||
promise: Promise<void>;
|
promise: Promise<void>;
|
||||||
resolve: () => void;
|
resolve: () => void;
|
||||||
@@ -72,7 +76,7 @@ function createDeferred(): {
|
|||||||
|
|
||||||
describe("Agent", () => {
|
describe("Agent", () => {
|
||||||
it("should create an agent instance with default state", () => {
|
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).toBeDefined();
|
||||||
expect(agent.state.systemPrompt).toBe("");
|
expect(agent.state.systemPrompt).toBe("");
|
||||||
@@ -89,6 +93,7 @@ describe("Agent", () => {
|
|||||||
it("should create an agent instance with custom initial state", () => {
|
it("should create an agent instance with custom initial state", () => {
|
||||||
const customModel = getModel("openai", "gpt-4o-mini");
|
const customModel = getModel("openai", "gpt-4o-mini");
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: unusedStreamFunction,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model: customModel,
|
model: customModel,
|
||||||
@@ -102,7 +107,7 @@ describe("Agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should subscribe to events", () => {
|
it("should subscribe to events", () => {
|
||||||
const agent = new Agent();
|
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||||
|
|
||||||
let eventCount = 0;
|
let eventCount = 0;
|
||||||
const unsubscribe = agent.subscribe((_event) => {
|
const unsubscribe = agent.subscribe((_event) => {
|
||||||
@@ -125,7 +130,7 @@ describe("Agent", () => {
|
|||||||
|
|
||||||
it("emits full lifecycle events for thrown run failures", async () => {
|
it("emits full lifecycle events for thrown run failures", async () => {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
throw new Error("provider exploded");
|
throw new Error("provider exploded");
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -157,7 +162,7 @@ describe("Agent", () => {
|
|||||||
it("should await async subscribers before prompt resolves", async () => {
|
it("should await async subscribers before prompt resolves", async () => {
|
||||||
const barrier = createDeferred();
|
const barrier = createDeferred();
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("ok") });
|
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("ok") });
|
||||||
@@ -195,7 +200,7 @@ describe("Agent", () => {
|
|||||||
it("waitForIdle should wait for async subscribers", async () => {
|
it("waitForIdle should wait for async subscribers", async () => {
|
||||||
const barrier = createDeferred();
|
const barrier = createDeferred();
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("ok") });
|
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 () => {
|
it("should pass the active abort signal to subscribers", async () => {
|
||||||
let receivedSignal: AbortSignal | undefined;
|
let receivedSignal: AbortSignal | undefined;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: (_model, _context, options) => {
|
streamFunction: (_model, _context, options) => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "start", partial: createAssistantMessage("") });
|
stream.push({ type: "start", partial: createAssistantMessage("") });
|
||||||
@@ -293,7 +298,7 @@ describe("Agent", () => {
|
|||||||
};
|
};
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
initialState: { tools: [tool] },
|
initialState: { tools: [tool] },
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({
|
stream.push({
|
||||||
@@ -368,7 +373,7 @@ describe("Agent", () => {
|
|||||||
};
|
};
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
initialState: { tools: [settledTool, slowTool] },
|
initialState: { tools: [settledTool, slowTool] },
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({
|
stream.push({
|
||||||
@@ -407,7 +412,7 @@ describe("Agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should update state with mutators", () => {
|
it("should update state with mutators", () => {
|
||||||
const agent = new Agent();
|
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||||
|
|
||||||
// Test setSystemPrompt
|
// Test setSystemPrompt
|
||||||
agent.state.systemPrompt = "Custom prompt";
|
agent.state.systemPrompt = "Custom prompt";
|
||||||
@@ -446,7 +451,7 @@ describe("Agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should support steering message queue", async () => {
|
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() };
|
const message = { role: "user" as const, content: "Steering message", timestamp: Date.now() };
|
||||||
agent.steer(message);
|
agent.steer(message);
|
||||||
@@ -456,7 +461,7 @@ describe("Agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should support follow-up message queue", async () => {
|
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() };
|
const message = { role: "user" as const, content: "Follow-up message", timestamp: Date.now() };
|
||||||
agent.followUp(message);
|
agent.followUp(message);
|
||||||
@@ -466,7 +471,7 @@ describe("Agent", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should handle abort controller", () => {
|
it("should handle abort controller", () => {
|
||||||
const agent = new Agent();
|
const agent = new Agent({ streamFunction: unusedStreamFunction });
|
||||||
|
|
||||||
// Should not throw even if nothing is running
|
// Should not throw even if nothing is running
|
||||||
expect(() => agent.abort()).not.toThrow();
|
expect(() => agent.abort()).not.toThrow();
|
||||||
@@ -476,7 +481,7 @@ describe("Agent", () => {
|
|||||||
let abortSignal: AbortSignal | undefined;
|
let abortSignal: AbortSignal | undefined;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
// Use a stream function that responds to abort
|
// Use a stream function that responds to abort
|
||||||
streamFn: (_model, _context, options) => {
|
streamFunction: (_model, _context, options) => {
|
||||||
abortSignal = options?.signal;
|
abortSignal = options?.signal;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -515,7 +520,7 @@ describe("Agent", () => {
|
|||||||
it("should throw when continue() called while streaming", async () => {
|
it("should throw when continue() called while streaming", async () => {
|
||||||
let abortSignal: AbortSignal | undefined;
|
let abortSignal: AbortSignal | undefined;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: (_model, _context, options) => {
|
streamFunction: (_model, _context, options) => {
|
||||||
abortSignal = options?.signal;
|
abortSignal = options?.signal;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -550,7 +555,7 @@ describe("Agent", () => {
|
|||||||
|
|
||||||
it("continue() should process queued follow-up messages after an assistant turn", async () => {
|
it("continue() should process queued follow-up messages after an assistant turn", async () => {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "done", reason: "stop", message: createAssistantMessage("Processed") });
|
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 () => {
|
it("continue() should keep one-at-a-time steering semantics from assistant tail", async () => {
|
||||||
let responseCount = 0;
|
let responseCount = 0;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
responseCount++;
|
responseCount++;
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -647,7 +652,7 @@ describe("Agent", () => {
|
|||||||
sawAbortSignal = signal instanceof AbortSignal;
|
sawAbortSignal = signal instanceof AbortSignal;
|
||||||
return undefined;
|
return undefined;
|
||||||
},
|
},
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
requestCount++;
|
requestCount++;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -671,11 +676,11 @@ describe("Agent", () => {
|
|||||||
expect(sawAbortSignal).toBe(true);
|
expect(sawAbortSignal).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("forwards sessionId to streamFn options", async () => {
|
it("forwards sessionId to streamFunction options", async () => {
|
||||||
let receivedSessionId: string | undefined;
|
let receivedSessionId: string | undefined;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
sessionId: "session-abc",
|
sessionId: "session-abc",
|
||||||
streamFn: (_model, _context, options) => {
|
streamFunction: (_model, _context, options) => {
|
||||||
receivedSessionId = options?.sessionId;
|
receivedSessionId = options?.sessionId;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
fauxToolCall,
|
fauxToolCall,
|
||||||
type Model,
|
type Model,
|
||||||
registerFauxProvider,
|
registerFauxProvider,
|
||||||
|
streamSimple,
|
||||||
type ToolResultMessage,
|
type ToolResultMessage,
|
||||||
type UserMessage,
|
type UserMessage,
|
||||||
} from "@earendil-works/pi-ai/compat";
|
} from "@earendil-works/pi-ai/compat";
|
||||||
@@ -37,6 +38,7 @@ afterEach(() => {
|
|||||||
|
|
||||||
async function basicPrompt(model: Model<string>) {
|
async function basicPrompt(model: Model<string>) {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant. Keep your responses concise.",
|
systemPrompt: "You are a helpful assistant. Keep your responses concise.",
|
||||||
model,
|
model,
|
||||||
@@ -59,6 +61,7 @@ async function basicPrompt(model: Model<string>) {
|
|||||||
|
|
||||||
async function toolExecution(model: Model<string>) {
|
async function toolExecution(model: Model<string>) {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant. Always use the calculator tool for math.",
|
systemPrompt: "You are a helpful assistant. Always use the calculator tool for math.",
|
||||||
model,
|
model,
|
||||||
@@ -98,6 +101,7 @@ async function toolExecution(model: Model<string>) {
|
|||||||
|
|
||||||
async function abortExecution(model: Model<string>) {
|
async function abortExecution(model: Model<string>) {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model,
|
model,
|
||||||
@@ -125,6 +129,7 @@ async function abortExecution(model: Model<string>) {
|
|||||||
|
|
||||||
async function stateUpdates(model: Model<string>) {
|
async function stateUpdates(model: Model<string>) {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model,
|
model,
|
||||||
@@ -157,6 +162,7 @@ async function stateUpdates(model: Model<string>) {
|
|||||||
|
|
||||||
async function multiTurnConversation(model: Model<string>) {
|
async function multiTurnConversation(model: Model<string>) {
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model,
|
model,
|
||||||
@@ -238,6 +244,7 @@ describe("Agent integration with faux provider", () => {
|
|||||||
faux.setResponses([fauxAssistantMessage([fauxThinking("step by step"), fauxText("4")])]);
|
faux.setResponses([fauxAssistantMessage([fauxThinking("step by step"), fauxText("4")])]);
|
||||||
|
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
model: faux.getModel(),
|
model: faux.getModel(),
|
||||||
@@ -262,6 +269,7 @@ describe("Agent.continue() with faux provider", () => {
|
|||||||
it("throws when no messages in context", async () => {
|
it("throws when no messages in context", async () => {
|
||||||
const faux = createFauxRegistration();
|
const faux = createFauxRegistration();
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
model: faux.getModel(),
|
model: faux.getModel(),
|
||||||
@@ -275,6 +283,7 @@ describe("Agent.continue() with faux provider", () => {
|
|||||||
const faux = createFauxRegistration();
|
const faux = createFauxRegistration();
|
||||||
const model = faux.getModel();
|
const model = faux.getModel();
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
model,
|
model,
|
||||||
@@ -309,6 +318,7 @@ describe("Agent.continue() with faux provider", () => {
|
|||||||
const faux = createFauxRegistration();
|
const faux = createFauxRegistration();
|
||||||
faux.setResponses([fauxAssistantMessage("HELLO WORLD")]);
|
faux.setResponses([fauxAssistantMessage("HELLO WORLD")]);
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt: "You are a helpful assistant. Follow instructions exactly.",
|
systemPrompt: "You are a helpful assistant. Follow instructions exactly.",
|
||||||
model: faux.getModel(),
|
model: faux.getModel(),
|
||||||
@@ -343,6 +353,7 @@ describe("Agent.continue() with faux provider", () => {
|
|||||||
const model = faux.getModel();
|
const model = faux.getModel();
|
||||||
faux.setResponses([fauxAssistantMessage("The answer is 8.")]);
|
faux.setResponses([fauxAssistantMessage("The answer is 8.")]);
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
systemPrompt:
|
systemPrompt:
|
||||||
"You are a helpful assistant. After getting a calculation result, state the answer clearly.",
|
"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>;
|
headers?: Record<string, string>;
|
||||||
env?: Record<string, string>;
|
env?: Record<string, string>;
|
||||||
}> {
|
}> {
|
||||||
if (this.agent.streamFn === streamSimple) {
|
if (this.agent.streamFunction === streamSimple) {
|
||||||
return this._getRequiredRequestAuth(model);
|
return this._getRequiredRequestAuth(model);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1836,7 +1836,7 @@ export class AgentSession {
|
|||||||
customInstructions,
|
customInstructions,
|
||||||
this._compactionAbortController.signal,
|
this._compactionAbortController.signal,
|
||||||
this.thinkingLevel,
|
this.thinkingLevel,
|
||||||
this.agent.streamFn,
|
this.agent.streamFunction,
|
||||||
env,
|
env,
|
||||||
);
|
);
|
||||||
summary = result.summary;
|
summary = result.summary;
|
||||||
@@ -2037,7 +2037,7 @@ export class AgentSession {
|
|||||||
let apiKey: string | undefined;
|
let apiKey: string | undefined;
|
||||||
let headers: Record<string, string> | undefined;
|
let headers: Record<string, string> | undefined;
|
||||||
let env: 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);
|
const authResult = await this._modelRuntime.getAuth(this.model);
|
||||||
if (!authResult?.auth.apiKey) return false;
|
if (!authResult?.auth.apiKey) return false;
|
||||||
apiKey = authResult.auth.apiKey;
|
apiKey = authResult.auth.apiKey;
|
||||||
@@ -2112,7 +2112,7 @@ export class AgentSession {
|
|||||||
undefined,
|
undefined,
|
||||||
this._autoCompactionAbortController.signal,
|
this._autoCompactionAbortController.signal,
|
||||||
this.thinkingLevel,
|
this.thinkingLevel,
|
||||||
this.agent.streamFn,
|
this.agent.streamFunction,
|
||||||
env,
|
env,
|
||||||
);
|
);
|
||||||
summary = compactResult.summary;
|
summary = compactResult.summary;
|
||||||
@@ -2933,7 +2933,7 @@ export class AgentSession {
|
|||||||
customInstructions,
|
customInstructions,
|
||||||
replaceInstructions,
|
replaceInstructions,
|
||||||
reserveTokens: branchSummarySettings.reserveTokens,
|
reserveTokens: branchSummarySettings.reserveTokens,
|
||||||
streamFn: this.agent.streamFn,
|
streamFn: this.agent.streamFunction,
|
||||||
});
|
});
|
||||||
if (result.aborted) {
|
if (result.aborted) {
|
||||||
return { cancelled: true, aborted: true };
|
return { cancelled: true, aborted: true };
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {}
|
|||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
convertToLlm: convertToLlmWithBlockImages,
|
convertToLlm: convertToLlmWithBlockImages,
|
||||||
streamFn: async (model, context, options) => {
|
streamFunction: async (model, context, options) => {
|
||||||
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
const providerRetrySettings = settingsManager.getProviderRetrySettings();
|
||||||
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
|
const httpIdleTimeoutMs = settingsManager.getHttpIdleTimeoutMs();
|
||||||
// SDKs treat timeout=0 as 0ms (immediate timeout), not "no timeout".
|
// 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 { join } from "node:path";
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
import { Agent } from "@earendil-works/pi-agent-core";
|
||||||
import { type AssistantMessage, createAssistantMessageEventStream, fauxAssistantMessage } from "@earendil-works/pi-ai";
|
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 { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
import { AgentSession } from "../src/core/agent-session.ts";
|
import { AgentSession } from "../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.ts";
|
import { AuthStorage } from "../src/core/auth-storage.ts";
|
||||||
@@ -21,10 +21,10 @@ describe("AgentSession auto-compaction queue resume", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
tempDir = join(tmpdir(), `pi-auto-compaction-queue-${Date.now()}`);
|
tempDir = join(tmpdir(), `pi-auto-compaction-queue-${Date.now()}`);
|
||||||
mkdirSync(tempDir, { recursive: true });
|
mkdirSync(tempDir, { recursive: true });
|
||||||
vi.useFakeTimers();
|
|
||||||
|
|
||||||
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
const model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
model,
|
model,
|
||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
@@ -50,7 +50,6 @@ describe("AgentSession auto-compaction queue resume", () => {
|
|||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
session.dispose();
|
session.dispose();
|
||||||
vi.useRealTimers();
|
|
||||||
vi.restoreAllMocks();
|
vi.restoreAllMocks();
|
||||||
if (tempDir && existsSync(tempDir)) {
|
if (tempDir && existsSync(tempDir)) {
|
||||||
rmSync(tempDir, { recursive: true });
|
rmSync(tempDir, { recursive: true });
|
||||||
@@ -84,9 +83,9 @@ describe("AgentSession auto-compaction queue resume", () => {
|
|||||||
timestamp: now - 500,
|
timestamp: now - 500,
|
||||||
});
|
});
|
||||||
session.agent.state.messages = sessionManager.buildSessionContext().messages;
|
session.agent.state.messages = sessionManager.buildSessionContext().messages;
|
||||||
session.agent.streamFn = (summaryModel) => {
|
session.agent.streamFunction = (summaryModel) => {
|
||||||
const stream = createAssistantMessageEventStream();
|
const stream = createAssistantMessageEventStream();
|
||||||
queueMicrotask(() => {
|
void Promise.resolve().then(() => {
|
||||||
stream.push({
|
stream.push({
|
||||||
type: "done",
|
type: "done",
|
||||||
reason: "stop",
|
reason: "stop",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
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 { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts";
|
import { AgentSession, type AgentSessionEvent } from "../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.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 model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
getApiKey: () => API_KEY,
|
getApiKey: () => API_KEY,
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
model,
|
model,
|
||||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
streamFn: (_model, _context, options) => {
|
streamFunction: (_model, _context, options) => {
|
||||||
abortSignal = options?.signal;
|
abortSignal = options?.signal;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -195,7 +195,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
streamFn: (_model, context, options) => {
|
streamFunction: (_model, context, options) => {
|
||||||
abortSignal = options?.signal;
|
abortSignal = options?.signal;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -301,7 +301,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "start", partial: createAssistantMessage("") });
|
stream.push({ type: "start", partial: createAssistantMessage("") });
|
||||||
@@ -362,7 +362,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [tool],
|
tools: [tool],
|
||||||
},
|
},
|
||||||
streamFn: async (_model, context) => {
|
streamFunction: async (_model, context) => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const toolResultCount = context.messages.filter((message) => message.role === "toolResult").length;
|
const toolResultCount = context.messages.filter((message) => message.role === "toolResult").length;
|
||||||
@@ -508,7 +508,7 @@ describe("AgentSession concurrent prompt guard", () => {
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [tool],
|
tools: [tool],
|
||||||
},
|
},
|
||||||
streamFn: async (_model, context) => {
|
streamFunction: async (_model, context) => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
const hasToolResult = context.messages.some((message) => message.role === "toolResult");
|
const hasToolResult = context.messages.some((message) => message.role === "toolResult");
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ describe("AgentSession dynamic provider registration", () => {
|
|||||||
session: Awaited<ReturnType<typeof createSession>>,
|
session: Awaited<ReturnType<typeof createSession>>,
|
||||||
): Promise<string | undefined> {
|
): Promise<string | undefined> {
|
||||||
let baseUrl: string | undefined;
|
let baseUrl: string | undefined;
|
||||||
session.agent.streamFn = async (model) => {
|
session.agent.streamFunction = async (model) => {
|
||||||
baseUrl = model.baseUrl;
|
baseUrl = model.baseUrl;
|
||||||
throw new Error("stop");
|
throw new Error("stop");
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ describe("AgentSession retry", () => {
|
|||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
getApiKey: () => "test-key",
|
getApiKey: () => "test-key",
|
||||||
initialState: { model, systemPrompt: "Test", tools: [] },
|
initialState: { model, systemPrompt: "Test", tools: [] },
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
callCount++;
|
callCount++;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
@@ -203,7 +203,7 @@ describe("AgentSession retry", () => {
|
|||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
getApiKey: () => "test-key",
|
getApiKey: () => "test-key",
|
||||||
initialState: { model, systemPrompt: "Test", tools: [] },
|
initialState: { model, systemPrompt: "Test", tools: [] },
|
||||||
streamFn,
|
streamFunction: streamFn,
|
||||||
});
|
});
|
||||||
const sessionManager = SessionManager.inMemory();
|
const sessionManager = SessionManager.inMemory();
|
||||||
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
const settingsManager = SettingsManager.create(tempDir, tempDir);
|
||||||
@@ -255,7 +255,7 @@ describe("AgentSession retry", () => {
|
|||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
getApiKey: () => "test-key",
|
getApiKey: () => "test-key",
|
||||||
initialState: { model, systemPrompt: "Test", tools: [] },
|
initialState: { model, systemPrompt: "Test", tools: [] },
|
||||||
streamFn: () => {
|
streamFunction: () => {
|
||||||
callCount++;
|
callCount++;
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
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 { describe, expect, it } from "vitest";
|
||||||
import { AgentSession } from "../src/core/agent-session.ts";
|
import { AgentSession } from "../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.ts";
|
import { AuthStorage } from "../src/core/auth-storage.ts";
|
||||||
@@ -69,6 +75,7 @@ async function createSession() {
|
|||||||
const session = new AgentSession({
|
const session = new AgentSession({
|
||||||
agent: new Agent({
|
agent: new Agent({
|
||||||
getApiKey: () => "test-key",
|
getApiKey: () => "test-key",
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
model,
|
model,
|
||||||
systemPrompt: "You are a helpful assistant.",
|
systemPrompt: "You are a helpful assistant.",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
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 { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||||
import { AgentSession } from "../src/core/agent-session.ts";
|
import { AgentSession } from "../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.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 model = getModel("anthropic", "claude-sonnet-4-5")!;
|
||||||
const agent = new Agent({
|
const agent = new Agent({
|
||||||
getApiKey: () => API_KEY,
|
getApiKey: () => API_KEY,
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
model,
|
model,
|
||||||
systemPrompt: "You are a helpful assistant. Be concise.",
|
systemPrompt: "You are a helpful assistant. Be concise.",
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ async function createRuntimeHost(options: { withAuth: boolean; responseDelayMs:
|
|||||||
systemPrompt: "Test",
|
systemPrompt: "Test",
|
||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
streamFn: (_model, _context, _options) => {
|
streamFunction: (_model, _context, _options) => {
|
||||||
const stream = new MockAssistantStream();
|
const stream = new MockAssistantStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
stream.push({ type: "start", partial: createAssistantMessage("") });
|
stream.push({ type: "start", partial: createAssistantMessage("") });
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ describe("createAgentSession provider attribution headers", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await session.agent.streamFn(
|
const stream = await session.agent.streamFunction(
|
||||||
model,
|
model,
|
||||||
{ messages: [] },
|
{ messages: [] },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ describe("createAgentSession stream options", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stream = await session.agent.streamFn(model, { messages: [] }, requestOptions);
|
const stream = await session.agent.streamFunction(model, { messages: [] }, requestOptions);
|
||||||
await stream.result();
|
await stream.result();
|
||||||
return capturedOptions;
|
return capturedOptions;
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ function createAssistant(
|
|||||||
|
|
||||||
function useSummaryStreamFn(harness: Harness, summary: string): () => number {
|
function useSummaryStreamFn(harness: Harness, summary: string): () => number {
|
||||||
let callCount = 0;
|
let callCount = 0;
|
||||||
harness.session.agent.streamFn = (model) => {
|
harness.session.agent.streamFunction = (model) => {
|
||||||
callCount++;
|
callCount++;
|
||||||
const stream = createAssistantMessageEventStream();
|
const stream = createAssistantMessageEventStream();
|
||||||
queueMicrotask(() => {
|
queueMicrotask(() => {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import type {
|
|||||||
FauxResponseStep,
|
FauxResponseStep,
|
||||||
Model,
|
Model,
|
||||||
} from "@earendil-works/pi-ai/compat";
|
} 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 { AgentSession, type AgentSessionEvent } from "../../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../../src/core/auth-storage.ts";
|
import { AuthStorage } from "../../src/core/auth-storage.ts";
|
||||||
import type { ExtensionRunner } from "../../src/core/extensions/index.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({
|
const agent = new Agent({
|
||||||
getApiKey: () => (withConfiguredAuth ? "faux-key" : undefined),
|
getApiKey: () => (withConfiguredAuth ? "faux-key" : undefined),
|
||||||
|
streamFunction: streamSimple,
|
||||||
initialState: {
|
initialState: {
|
||||||
model,
|
model,
|
||||||
systemPrompt: options.systemPrompt ?? "You are a test assistant.",
|
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 { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
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 { afterEach, describe, expect, it } from "vitest";
|
||||||
import { AgentSession } from "../../../src/core/agent-session.ts";
|
import { AgentSession } from "../../../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../../../src/core/auth-storage.ts";
|
import { AuthStorage } from "../../../src/core/auth-storage.ts";
|
||||||
@@ -61,6 +61,7 @@ describe("regression #5596: missing configured theme export", () => {
|
|||||||
tools: [],
|
tools: [],
|
||||||
},
|
},
|
||||||
convertToLlm,
|
convertToLlm,
|
||||||
|
streamFunction: streamSimple,
|
||||||
});
|
});
|
||||||
const session = new AgentSession({
|
const session = new AgentSession({
|
||||||
agent,
|
agent,
|
||||||
|
|||||||
+1
-1
@@ -17,7 +17,7 @@ describe("issue #6324 branch summary ambient auth", () => {
|
|||||||
harnesses.push(harness);
|
harnesses.push(harness);
|
||||||
|
|
||||||
let streamCallCount = 0;
|
let streamCallCount = 0;
|
||||||
harness.session.agent.streamFn = (model, _context, options) => {
|
harness.session.agent.streamFunction = (model, _context, options) => {
|
||||||
streamCallCount++;
|
streamCallCount++;
|
||||||
expect(options?.apiKey).toBeUndefined();
|
expect(options?.apiKey).toBeUndefined();
|
||||||
|
|
||||||
|
|||||||
@@ -378,7 +378,7 @@ async function createHarnessWithResourceLoader(
|
|||||||
systemPrompt: options.systemPrompt ?? "You are a test assistant.",
|
systemPrompt: options.systemPrompt ?? "You are a test assistant.",
|
||||||
tools: options.tools ?? [],
|
tools: options.tools ?? [],
|
||||||
},
|
},
|
||||||
streamFn,
|
streamFunction: streamFn,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionManager = SessionManager.inMemory();
|
const sessionManager = SessionManager.inMemory();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { homedir, tmpdir } from "node:os";
|
|||||||
import { dirname, join } from "node:path";
|
import { dirname, join } from "node:path";
|
||||||
import { Agent } from "@earendil-works/pi-agent-core";
|
import { Agent } from "@earendil-works/pi-agent-core";
|
||||||
import type { OAuthCredentials } from "@earendil-works/pi-ai";
|
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 { builtinProviders } from "@earendil-works/pi-ai/providers/all";
|
||||||
import { AgentSession } from "../src/core/agent-session.ts";
|
import { AgentSession } from "../src/core/agent-session.ts";
|
||||||
import { AuthStorage } from "../src/core/auth-storage.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.",
|
systemPrompt: options.systemPrompt ?? "You are a helpful assistant. Be extremely concise.",
|
||||||
tools: createCodingTools(process.cwd()),
|
tools: createCodingTools(process.cwd()),
|
||||||
},
|
},
|
||||||
|
streamFunction: streamSimple,
|
||||||
});
|
});
|
||||||
|
|
||||||
const sessionManager = options.inMemory ? SessionManager.inMemory() : SessionManager.create(tempDir);
|
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 { 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 {
|
import {
|
||||||
Agent,
|
Agent,
|
||||||
bashExecutionToText,
|
bashExecutionToText,
|
||||||
@@ -24,7 +24,7 @@ const model = getModel("google", "gemini-2.5-flash");
|
|||||||
const schema = Type.Object({ prompt: Type.String() });
|
const schema = Type.Object({ prompt: Type.String() });
|
||||||
const stream = createAssistantMessageEventStream();
|
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 });
|
agent.steer({ role: "user", content: [{ type: "text", text: "queued" }], timestamp: 0 });
|
||||||
const repo = new InMemorySessionRepo();
|
const repo = new InMemorySessionRepo();
|
||||||
const result = getOrThrow(ok({ value: 1 }));
|
const result = getOrThrow(ok({ value: 1 }));
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { dirname, join, resolve } from "node:path";
|
|||||||
import { build } from "esbuild";
|
import { build } from "esbuild";
|
||||||
|
|
||||||
const outputPath = join(tmpdir(), "pi-browser-smoke.js");
|
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 errorLogPath = join(tmpdir(), "pi-browser-smoke-errors.log");
|
||||||
const generatedCatalogDataDir = join(process.cwd(), "packages/ai/src/providers/data");
|
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 {
|
try {
|
||||||
await build({
|
await build({
|
||||||
entryPoints: ["scripts/browser-smoke-entry.ts"],
|
entryPoints: ["scripts/browser-smoke-entry.ts"],
|
||||||
@@ -33,6 +50,47 @@ try {
|
|||||||
outfile: outputPath,
|
outfile: outputPath,
|
||||||
plugins: [generatedCatalogDataPlugin],
|
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);
|
process.exit(0);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
let detailedErrors = "";
|
let detailedErrors = "";
|
||||||
|
|||||||
Reference in New Issue
Block a user