Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 1 addition & 7 deletions .oxlintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,12 +83,7 @@
"vitest/prefer-called-times": "off",
"vitest/prefer-called-once": "off",
"vitest/prefer-expect-type-of": "off",
"oxc/no-optional-chaining": [
"error",
{
"message": "Use explicit null checks (if/else or ts-pattern match) instead of optional chaining."
}
],
"oxc/no-optional-chaining": "off",
"jsdoc-js/multiline-blocks": [
"error",
{
Expand All @@ -103,7 +98,6 @@
"no-unused-vars": "off",
"functional/no-classes": "off",
"typescript/no-non-null-assertion": "off",
"oxc/no-optional-chaining": "off",
"vitest/prefer-to-be-truthy": "off",
"vitest/prefer-to-be-falsy": "off"
}
Expand Down
1 change: 0 additions & 1 deletion examples/basic-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,6 @@ const result = await weatherAgent.generate({ prompt: "What is the weather in San

if (result.ok) {
console.log("Output:", result.output);
console.log("Messages:", result.messages.length);
console.log("Usage:", result.usage);
} else {
console.error("Error:", result.error);
Expand Down
6 changes: 0 additions & 6 deletions examples/streaming/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { openai } from "@ai-sdk/openai";
import { agent, flowAgent, tool } from "@funkai/agents";
import type { Message } from "@funkai/agents";
import { z } from "zod";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -193,11 +192,6 @@ if (flowResult.ok) {

const output = await flowResult.output;
console.log("\nFindings:", JSON.stringify(output, null, 2));

const messages: Message[] = await flowResult.messages;
console.log(
`\nFlow produced ${messages.length} messages (including synthetic tool-call/result pairs for each step)`,
);
} else {
console.error("Error:", flowResult.error);
}
86 changes: 28 additions & 58 deletions packages/agents/src/core/agents/base/agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,15 @@ function createMockGenerateResult(overrides?: {
output?: unknown;
response?: { messages: unknown[] };
totalUsage?: typeof MOCK_TOTAL_USAGE;
usage?: typeof MOCK_TOTAL_USAGE;
finishReason?: string;
}) {
const defaults = {
text: "mock response text",
output: undefined,
response: { messages: [{ role: "assistant", content: "mock" }] },
totalUsage: MOCK_TOTAL_USAGE,
usage: MOCK_TOTAL_USAGE,
finishReason: "stop",
};
return { ...defaults, ...overrides };
Expand All @@ -75,13 +77,15 @@ function createMockStreamResult(overrides?: {
response?: { messages: unknown[] };
chunks?: string[];
totalUsage?: typeof MOCK_TOTAL_USAGE;
usage?: typeof MOCK_TOTAL_USAGE;
finishReason?: string;
}) {
const defaults = {
chunks: ["hello", " world"] as string[],
output: undefined as unknown,
response: undefined as { messages: unknown[] } | undefined,
totalUsage: MOCK_TOTAL_USAGE,
usage: MOCK_TOTAL_USAGE,
finishReason: "stop",
};
const merged = { ...defaults, ...overrides };
Expand All @@ -94,6 +98,14 @@ function createMockStreamResult(overrides?: {
}
}

const mockStep = {
text: textValue,
output: merged.output,
usage: merged.usage,
finishReason: merged.finishReason,
response: merged.response ?? { messages: [{ role: "assistant", content: textValue }] },
};

return {
fullStream: makeFullStream(),
text: Promise.resolve(textValue),
Expand All @@ -102,6 +114,8 @@ function createMockStreamResult(overrides?: {
merged.response ?? { messages: [{ role: "assistant", content: textValue }] },
),
totalUsage: Promise.resolve(merged.totalUsage),
usage: Promise.resolve(merged.usage),
steps: Promise.resolve([mockStep]),
finishReason: Promise.resolve(merged.finishReason),
toTextStreamResponse: vi.fn(() => new Response("mock text stream")),
toUIMessageStreamResponse: vi.fn(() => new Response("mock ui stream")),
Expand Down Expand Up @@ -176,15 +190,7 @@ describe("generate() success", () => {
return;
}
expect(result.output).toBe("mock response text");
expect(result.messages).toBeInstanceOf(Array);
expect(result.usage).toEqual({
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
cacheReadTokens: 10,
cacheWriteTokens: 5,
reasoningTokens: 3,
});
expect(result.usage).toEqual(MOCK_TOTAL_USAGE);
expect(result.finishReason).toBe("stop");
});

Expand Down Expand Up @@ -419,7 +425,7 @@ describe("generate() hooks", () => {
const [event] = firstCall;
expect(event.input).toBe("hello");
expect(event.result).toHaveProperty("output");
expect(event.result).toHaveProperty("messages");
expect(event.result).toHaveProperty("response");
expect(event.result).toHaveProperty("usage");
expect(event.result).toHaveProperty("finishReason");
expect(event.duration).toBeGreaterThanOrEqual(0);
Expand Down Expand Up @@ -857,10 +863,9 @@ describe("stream() success", () => {
return;
}
expect(result.fullStream).toBeInstanceOf(ReadableStream);
expect(result.output).toBeInstanceOf(Promise);
expect(result.messages).toBeInstanceOf(Promise);
expect(result.usage).toBeInstanceOf(Promise);
expect(result.finishReason).toBeInstanceOf(Promise);
expect(result.output).toBeDefined();
expect(result.usage).toBeDefined();
expect(result.finishReason).toBeDefined();
});

it("fullStream emits typed StreamPart events", async () => {
Expand Down Expand Up @@ -916,34 +921,6 @@ describe("stream() success", () => {
expect(output).toBe("full text");
});

it("messages promise resolves after stream completes", async () => {
const expectedMessages = [{ role: "assistant", content: "msg" }];
mockStreamText.mockReturnValue(
createMockStreamResult({ response: { messages: expectedMessages } }),
);

const a = createSimpleAgent();
const result = await a.stream({ prompt: "hello" });

expect(result.ok).toBeTruthy();
if (!result.ok) {
return;
}

// Drain the stream to complete
const reader = result.fullStream.getReader();
for (;;) {
// eslint-disable-next-line no-await-in-loop -- Sequential stream consumption requires awaiting each read
const { done } = await reader.read();
if (done) {
break;
}
}

const messages = await result.messages;
expect(messages).toEqual(expectedMessages);
});

it("usage and finishReason promises resolve after stream completes", async () => {
const a = createSimpleAgent();
const result = await a.stream({ prompt: "hello" });
Expand All @@ -964,14 +941,7 @@ describe("stream() success", () => {
}

const usage = await result.usage;
expect(usage).toEqual({
inputTokens: 100,
outputTokens: 50,
totalTokens: 150,
cacheReadTokens: 10,
cacheWriteTokens: 5,
reasoningTokens: 3,
});
expect(usage).toEqual(MOCK_TOTAL_USAGE);

const finishReason = await result.finishReason;
expect(finishReason).toBe("stop");
Expand Down Expand Up @@ -1417,6 +1387,7 @@ function createErrorStreamResult(error: Error) {
fullStream: makeFullStream(),
text: makeSuppressedRejection<string>(error),
output: makeSuppressedRejection<unknown>(error),
usage: makeSuppressedRejection<typeof MOCK_TOTAL_USAGE>(error),
response: makeSuppressedRejection<{ messages: unknown[] }>(error),
totalUsage: makeSuppressedRejection<typeof MOCK_TOTAL_USAGE>(error),
finishReason: makeSuppressedRejection<string>(error),
Expand All @@ -1437,10 +1408,9 @@ describe("stream() async error during consumption", () => {
}

// Suppress derived promise rejections
result.output.catch(() => {});
result.messages.catch(() => {});
result.usage.catch(() => {});
result.finishReason.catch(() => {});
result.output.then(undefined, () => {});
result.usage.then(undefined, () => {});
result.finishReason.then(undefined, () => {});

// Drain the stream — writer.abort() errors the readable side, so
// Reader.read() will reject once the error propagates.
Expand Down Expand Up @@ -1481,10 +1451,9 @@ describe("stream() async error during consumption", () => {
return;
}

result.output.catch(() => {});
result.messages.catch(() => {});
result.usage.catch(() => {});
result.finishReason.catch(() => {});
result.output.then(undefined, () => {});
result.usage.then(undefined, () => {});
result.finishReason.then(undefined, () => {});

// Drain the stream to trigger the error — reader.read() rejects
// Once the writer aborts the transform stream.
Expand Down Expand Up @@ -1533,6 +1502,7 @@ describe("stream() unhandled rejection safety", () => {
fullStream: makeFullStream(),
text: makeSuppressedRejection<string>(streamError),
output: makeSuppressedRejection<unknown>(streamError),
usage: makeSuppressedRejection<typeof MOCK_TOTAL_USAGE>(streamError),
response: makeSuppressedRejection<{ messages: unknown[] }>(streamError),
totalUsage: makeSuppressedRejection<typeof MOCK_TOTAL_USAGE>(streamError),
finishReason: makeSuppressedRejection<string>(streamError),
Expand Down
Loading
Loading