Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
5 changes: 5 additions & 0 deletions .changeset/surface-ai-sdk-data-agents.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@funkai/agents": minor
---

Surface all AI SDK data through agent results including token usage, finish reasons, warnings, request metadata, response metadata, and provider-specific fields. Unified hook types for flow agent onFinish and cleaned up type casts in generate/stream paths.
5 changes: 5 additions & 0 deletions .changeset/surface-ai-sdk-data-models.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@funkai/models": patch
---

Export additional provider types and update cost calculation to support surfaced AI SDK data.
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);
}
98 changes: 34 additions & 64 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 @@
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 @@
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 @@
}
}

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 @@
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 @@
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 @@
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 @@ -526,9 +532,9 @@
if (!firstCall) {
throw new Error("Expected onStepFinish first call");
}
const event = firstCall[0];
const [event] = firstCall;

// funkai additions
// Funkai additions
expect(event.stepId).toBe("test-agent:0");
expect(event.stepOperation).toBe("agent");
expect(event.agentChain).toEqual([{ id: "test-agent" }]);
Expand Down Expand Up @@ -602,7 +608,7 @@
if (!firstCall) {
throw new Error("Expected onStepFinish first call");
}
const event = firstCall[0];
const [event] = firstCall;

// Full tool call objects preserved (not stripped to toolName + argsTextLength)
expect(event.toolCalls).toEqual(mockStepData.toolCalls);
Expand All @@ -617,7 +623,7 @@
// Usage passed through as-is
expect(event.usage).toEqual(mockStepData.usage);

// finishReason passed through (not as stepId)
// FinishReason passed through (not as stepId)
expect(event.finishReason).toBe("tool-calls");
});

Expand Down Expand Up @@ -857,10 +863,9 @@
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 @@
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 @@
}

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 @@ -1148,9 +1118,9 @@
if (!firstCall) {
throw new Error("Expected onStepFinish first call");
}
const event = firstCall[0];
const [event] = firstCall;

// funkai additions
// Funkai additions
expect(event.stepId).toBe("test-agent:0");
expect(event.stepOperation).toBe("agent");
expect(event.agentChain).toEqual([{ id: "test-agent" }]);
Expand Down Expand Up @@ -1417,6 +1387,7 @@
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 @@
}

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

Check warning on line 1411 in packages/agents/src/core/agents/base/agent.test.ts

View workflow job for this annotation

GitHub Actions / ci

eslint-plugin-promise(prefer-catch)

Prefer `catch` to `then(a, b)` or `then(null, b)`
result.usage.then(undefined, () => {});

Check warning on line 1412 in packages/agents/src/core/agents/base/agent.test.ts

View workflow job for this annotation

GitHub Actions / ci

eslint-plugin-promise(prefer-catch)

Prefer `catch` to `then(a, b)` or `then(null, b)`
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 @@
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 @@
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