Skip to content
7 changes: 7 additions & 0 deletions .changeset/step-finish-ai-sdk-passthrough.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@funkai/agents": major
---

Pass through full AI SDK `StepResult` fields in `onStepFinish` events instead of stripping tool calls/results to summary fields. `StepFinishEvent` is now a superset of the Vercel AI SDK's `StepResult<ToolSet>` — all SDK fields (`text`, `toolCalls`, `toolResults`, `finishReason`, `usage`, `reasoning`, `sources`, `response`, etc.) are passed through unchanged, plus funkai-specific additions (`stepId`, `agentChain`).

**Breaking:** `toolCalls` entries now contain full AI SDK `TypedToolCall` objects (with `input`) instead of `{ toolName, argsTextLength }`. `toolResults` entries now contain full `TypedToolResult` objects (with `output`) instead of `{ toolName, resultTextLength }`. `usage` is now the AI SDK's `LanguageModelUsage` type (with `undefined`-able fields) instead of a simplified `{ inputTokens: number; outputTokens: number; totalTokens: number }`.
8 changes: 4 additions & 4 deletions docs/concepts/flow-agents.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const pipeline = flowAgent(
execute: async () => input.topic.toLowerCase().replace(/\s+/g, "-"),
});

// $.agent — tracked agent call, returns StepResult<GenerateResult>
// $.agent — tracked agent call, returns FlowAgentStepResult
const draft = await $.agent({
id: "write-draft",
agent: writer,
Expand All @@ -42,7 +42,7 @@ const pipeline = flowAgent(
return { text: "Generation failed." };
}

return { text: draft.value.output };
return { text: draft.output };
},
);

Expand All @@ -57,7 +57,7 @@ if (result.ok) {

## The $ step builder

`$` provides operations that are tracked in the execution trace. All return `Promise<StepResult<T>>` — check `.ok` before using `.value`.
`$` provides operations that are tracked in the execution trace. All return `Promise<FlowStepResult<T>>` — check `.ok` before using `.output`.

| Operation | Description |
| ---------- | ------------------------------------------------------ |
Expand Down Expand Up @@ -86,7 +86,7 @@ const result = await pipeline.stream({ input: { topic: "closures" } });
if (result.ok) {
for await (const event of result.fullStream) {
if (event.type === "step:finish") {
console.log(event.step.id, "done in", event.duration, "ms");
console.log(event.stepId, "done in", event.duration, "ms");
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions docs/introduction.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,14 @@ const pipeline = flowAgent(
execute: async ({ item, $ }) => {
const result = await $.agent({ id: "write", agent: writer, input: item });
if (result.ok) {
return result.value.output;
return result.output;
}
return "";
},
concurrency: 3,
});
if (docs.ok) {
return { docs: docs.value };
return { docs: docs.output };
}
return { docs: [] };
},
Expand Down
6 changes: 3 additions & 3 deletions docs/principles.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const counter = flowAgent(
input: input.prompt,
});
if (result.ok) {
answer = result.value.output;
answer = result.output;
}
attempts += 1;
}
Expand Down Expand Up @@ -112,7 +112,7 @@ const pipeline = flowAgent(
// Untraced -- plain function call, not in trace
let analysisText;
if (analysis.ok) {
analysisText = analysis.value.output;
analysisText = analysis.output;
} else {
analysisText = input.text;
}
Expand All @@ -122,7 +122,7 @@ const pipeline = flowAgent(
const final = await $.step({ id: "format", execute: () => formatOutput(cleaned) });

if (final.ok) {
return { result: final.value };
return { result: final.output };
}
return { result: cleaned };
},
Expand Down
6 changes: 3 additions & 3 deletions docs/quick-start.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,13 +108,13 @@ const pipeline = flowAgent(
const reviewed = await $.agent({
id: "review-draft",
agent: reviewer,
input: { draft: draft.value.output },
input: { draft: draft.output },
});

if (reviewed.ok) {
return { final: reviewed.value.output };
return { final: reviewed.output };
}
return { final: draft.value.output };
return { final: draft.output };
},
);

Expand Down
85 changes: 53 additions & 32 deletions docs/reference/flow-agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ function flowAgent<TInput>(
| `logger` | `Resolver<TInput, Logger>` | No | default | Pino-compatible logger |
| `onStart` | `(event: { input: TInput }) => void \| Promise<void>` | No | — | Fires when flow starts |
| `onError` | `(event: { input: TInput; error: Error }) => void \| Promise<void>` | No | — | Fires on error |
| `onStepStart` | `(event: { step: StepInfo }) => void \| Promise<void>` | No | — | Fires when a `$` step starts |
| `onStepStart` | `(event: StepStartEvent) => void \| Promise<void>` | No | — | Fires when a `$` step starts |
| `onStepFinish` | `(event: StepFinishEvent) => void \| Promise<void>` | No | — | Fires when a `$` step finishes |

### With output (`FlowAgentConfigWithOutput`)
Expand Down Expand Up @@ -98,16 +98,16 @@ interface FlowAgent<TInput, TOutput> {

The `$` object provides tracked operations. Every call appears in the execution trace. `$` is passed into nested callbacks so operations can be composed.

| Method | Signature | Returns | Description |
| ---------- | -------------------------------------------------------------------------- | ---------------------------- | ------------------------------------------ |
| `$.step` | `(config: StepConfig<T>) => Promise<StepResult<T>>` | `StepResult<T>` | Single unit of work |
| `$.agent` | `(config: AgentStepConfig<TInput>) => Promise<StepResult<GenerateResult>>` | `StepResult<GenerateResult>` | Agent call as tracked step |
| `$.map` | `(config: MapConfig<T, R>) => Promise<StepResult<R[]>>` | `StepResult<R[]>` | Parallel map with optional concurrency |
| `$.each` | `(config: EachConfig<T>) => Promise<StepResult<void>>` | `StepResult<void>` | Sequential side effects |
| `$.reduce` | `(config: ReduceConfig<T, R>) => Promise<StepResult<R>>` | `StepResult<R>` | Sequential accumulation |
| `$.while` | `(config: WhileConfig<T>) => Promise<StepResult<T \| undefined>>` | `StepResult<T \| undefined>` | Conditional loop |
| `$.all` | `(config: AllConfig) => Promise<StepResult<unknown[]>>` | `StepResult<unknown[]>` | Concurrent heterogeneous ops (Promise.all) |
| `$.race` | `(config: RaceConfig) => Promise<StepResult<unknown>>` | `StepResult<unknown>` | First-to-finish wins (Promise.race) |
| Method | Signature | Returns | Description |
| ---------- | --------------------------------------------------------------------- | -------------------------------- | ------------------------------------------ |
| `$.step` | `(config: StepConfig<T>) => Promise<FlowStepResult<T>>` | `FlowStepResult<T>` | Single unit of work |
| `$.agent` | `(config: AgentStepConfig<TInput>) => Promise<FlowAgentStepResult>` | `FlowAgentStepResult` | Agent call as tracked step |
| `$.map` | `(config: MapConfig<T, R>) => Promise<FlowStepResult<R[]>>` | `FlowStepResult<R[]>` | Parallel map with optional concurrency |
| `$.each` | `(config: EachConfig<T>) => Promise<FlowStepResult<void>>` | `FlowStepResult<void>` | Sequential side effects |
| `$.reduce` | `(config: ReduceConfig<T, R>) => Promise<FlowStepResult<R>>` | `FlowStepResult<R>` | Sequential accumulation |
| `$.while` | `(config: WhileConfig<T>) => Promise<FlowStepResult<T \| undefined>>` | `FlowStepResult<T \| undefined>` | Conditional loop |
| `$.all` | `(config: AllConfig) => Promise<FlowStepResult<unknown[]>>` | `FlowStepResult<unknown[]>` | Concurrent heterogeneous ops (Promise.all) |
| `$.race` | `(config: RaceConfig) => Promise<FlowStepResult<unknown>>` | `FlowStepResult<unknown>` | First-to-finish wins (Promise.race) |

### StepConfig

Expand Down Expand Up @@ -220,12 +220,26 @@ interface RaceConfig {
}
```

## StepResult
## FlowStepResult

```typescript
type StepResult<T> =
| { ok: true; value: T; step: StepInfo; duration: number }
| { ok: false; error: StepError; step: StepInfo; duration: number };
type FlowStepResult<T> =
| {
ok: true;
output: T;
stepId: string;
stepOperation: OperationType;
agentChain?: AgentChainEntry[];
duration: number;
}
| {
ok: false;
error: StepError;
stepId: string;
stepOperation: OperationType;
agentChain?: AgentChainEntry[];
duration: number;
};

interface StepError extends ResultError {
stepId: string; // the id from the failed step config
Expand Down Expand Up @@ -254,29 +268,36 @@ interface TraceEntry {
type OperationType = "step" | "agent" | "map" | "each" | "reduce" | "while" | "all" | "race";
```

## StepInfo
## StepStartEvent

```typescript
interface StepInfo {
id: string;
index: number; // auto-incrementing, starts at 0
type: OperationType;
interface StepStartEvent {
stepId: string; // from the $ config's `id` field
stepOperation: OperationType; // 'step' | 'agent' | 'map' | 'each' | 'reduce' | 'while' | 'all' | 'race'
agentChain?: AgentChainEntry[];
}
```

## StepFinishEvent

Emitted by `onStepFinish`. Agent tool-loop steps populate the left columns; flow orchestration steps populate the right.

| Field | Type | Present on |
| ------------- | -------------------------------------------------------------------- | ------------------------ |
| `stepId` | `string` | Agent tool-loop steps |
| `toolCalls` | `readonly { toolName: string; argsTextLength: number }[]` | Agent tool-loop steps |
| `toolResults` | `readonly { toolName: string; resultTextLength: number }[]` | Agent tool-loop steps |
| `usage` | `{ inputTokens: number; outputTokens: number; totalTokens: number }` | Agent tool-loop steps |
| `step` | `StepInfo` | Flow orchestration steps |
| `result` | `unknown` | Flow orchestration steps |
| `duration` | `number` | Flow orchestration steps |
Emitted by `onStepFinish`. For agent tool-loop steps, the event is a full superset of the Vercel AI SDK's `StepResult<ToolSet>` — all SDK fields are passed through unchanged, plus funkai-specific additions. For flow `$.agent()` steps, the event carries both flow fields (`output`, `duration`) and the AI SDK fields from the last tool-loop step. Non-agent flow steps (`$.step()`, `$.map()`, etc.) only have the flow-specific fields.

| Field | Type | Present on | Description |
| --------------- | ---------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
| `stepId` | `string` | All steps | funkai addition: the `$` config `id` |
| `stepOperation` | `OperationType` | All steps | funkai addition: operation type |
| `agentChain` | `AgentChainEntry[]` | All steps | funkai addition: agent ancestry chain |
| `stepNumber` | `number` | Agent tool-loop + flow `$.agent()` | AI SDK: zero-based step index |
| `text` | `string` | Agent tool-loop + flow `$.agent()` | AI SDK: generated text |
| `toolCalls` | `TypedToolCall<ToolSet>[]` | Agent tool-loop + flow `$.agent()` | AI SDK: full tool call objects with `input` |
| `toolResults` | `TypedToolResult<ToolSet>[]` | Agent tool-loop + flow `$.agent()` | AI SDK: full tool result objects with `output` |
| `finishReason` | `FinishReason` | Agent tool-loop + flow `$.agent()` | AI SDK: why the step ended |
| `usage` | `LanguageModelUsage` | Agent tool-loop + flow `$.agent()` | AI SDK: token usage |
| `reasoning` | `ReasoningPart[]` | Agent tool-loop + flow `$.agent()` | AI SDK: reasoning content |
| `sources` | `Source[]` | Agent tool-loop + flow `$.agent()` | AI SDK: cited sources |
| `response` | `LanguageModelResponseMetadata & { messages }` | Agent tool-loop + flow `$.agent()` | AI SDK: response metadata |
| `output` | `unknown` | Flow orchestration steps | Flow step output value |
| `duration` | `number` | Flow orchestration steps | Flow step duration in ms |

## FlowAgentOverrides

Expand Down Expand Up @@ -306,7 +327,7 @@ function createFlowEngine<TCustomSteps extends CustomStepDefinitions>(
| `onStart` | `(event: { input: unknown }) => void \| Promise<void>` | Default start hook for all flow agents |
| `onFinish` | `(event: { input: unknown; result: unknown; duration: number }) => void \| Promise<void>` | Default finish hook |
| `onError` | `(event: { input: unknown; error: Error }) => void \| Promise<void>` | Default error hook |
| `onStepStart` | `(event: { step: StepInfo }) => void \| Promise<void>` | Default step-start hook |
| `onStepStart` | `(event: StepStartEvent) => void \| Promise<void>` | Default step-start hook |
| `onStepFinish` | `(event: StepFinishEvent) => void \| Promise<void>` | Default step-finish hook |

### CustomStepFactory
Expand Down
14 changes: 7 additions & 7 deletions examples/flow-agent-steps/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ const stepsDemo = flowAgent(
throw new Error(`Validation failed: ${stepResult.error.message}`);
}

log.info({ count: stepResult.value.count }, "Input validated");
log.info({ count: stepResult.output.count }, "Input validated");

// -----------------------------------------------------------------------
// $.map — parallel map over items
Expand All @@ -64,7 +64,7 @@ const stepsDemo = flowAgent(
throw new Error(`Map failed: ${mapResult.error.message}`);
}

const doubled = mapResult.value;
const doubled = mapResult.output;

// -----------------------------------------------------------------------
// $.each — sequential side effects (returns void)
Expand All @@ -84,7 +84,7 @@ const stepsDemo = flowAgent(
id: "sum-numbers",
input: doubled,
initial: 0,
execute: async ({ item, accumulator }) => {
execute: async ({ item, accumulator }: { item: number; accumulator: number }) => {
return accumulator + item;
},
});
Expand Down Expand Up @@ -153,11 +153,11 @@ const stepsDemo = flowAgent(

return {
doubled,
sum: reduceResult.value,
sum: reduceResult.output,
logged: eachResult.ok,
countdown: whileResult.ok ? (whileResult.value ?? 0) : 0,
parallel: allResult.value,
fastest: raceResult.value,
countdown: whileResult.ok ? (whileResult.output ?? 0) : 0,
parallel: allResult.output,
fastest: raceResult.output,
};
},
);
Expand Down
14 changes: 6 additions & 8 deletions examples/flow-agent/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,11 @@ const summarizeAndTranslate = flowAgent(
summary: z.string(),
translation: z.string(),
}),
onStepStart: ({ step }) => {
console.log(` → step started: ${step.id} (${step.type})`);
onStepStart: ({ stepId, stepOperation }) => {
console.log(` → step started: ${stepId} (${stepOperation})`);
},
onStepFinish: ({ step, duration }) => {
if (step) {
console.log(` ✓ step finished: ${step.id} (${duration}ms)`);
}
onStepFinish: ({ stepId, duration }) => {
console.log(` ✓ step finished: ${stepId} (${duration}ms)`);
},
},
async ({ input, $ }) => {
Expand All @@ -50,7 +48,7 @@ const summarizeAndTranslate = flowAgent(
throw new Error(`Summarization failed: ${summaryResult.error.message}`);
}

const summary = String(summaryResult.value.output);
const summary = String(summaryResult.output);

// Step 2: Translate the summary
const translationResult = await $.agent({
Expand All @@ -65,7 +63,7 @@ const summarizeAndTranslate = flowAgent(

return {
summary,
translation: String(translationResult.value.output),
translation: String(translationResult.output),
};
},
);
Expand Down
8 changes: 4 additions & 4 deletions examples/prompts-subagents/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ const pipeline = flowAgent(
const draft = await $.agent({
id: "draft",
agent: writer,
input: `Write an article based on these findings:\n${research.value.output}`,
input: `Write an article based on these findings:\n${research.output}`,
});

if (!draft.ok) {
Expand All @@ -63,16 +63,16 @@ const pipeline = flowAgent(
const review = await $.agent({
id: "review",
agent: reviewer,
input: `Review this article:\n${draft.value.output}`,
input: `Review this article:\n${draft.output}`,
});

if (!review.ok) {
throw new Error(`Review failed: ${review.error.message}`);
}

return {
article: String(draft.value.output),
verdict: String(review.value.output),
article: String(draft.output),
verdict: String(review.output),
};
},
);
Expand Down
14 changes: 7 additions & 7 deletions examples/realworld-cli/api/pipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,12 @@ export const createPipeline = (
targetDir: z.string().describe("The directory path being scanned"),
}),
output: pipelineOutputSchema,
onStepStart: ({ step }) => {
emit({ type: "step:start", stepId: step.id, stepType: step.type });
onStepStart: ({ stepId, stepOperation }) => {
emit({ type: "step:start", stepId, stepType: stepOperation });
},
onStepFinish: ({ step, duration }) => {
if (step && duration !== undefined) {
emit({ type: "step:finish", stepId: step.id, duration });
onStepFinish: ({ stepId, duration }) => {
if (duration !== undefined) {
emit({ type: "step:finish", stepId, duration });
}
},
},
Expand All @@ -71,7 +71,7 @@ export const createPipeline = (
throw new Error(`Scanner failed: ${scanResult.error.message}`);
}

const scanOutput = scanResult.value.output as string;
const scanOutput = scanResult.output as string;
const testFilePaths = extractTestFilePaths(scanOutput);

emit({ type: "scan-complete", files: testFilePaths });
Expand Down Expand Up @@ -110,7 +110,7 @@ export const createPipeline = (
});

const summary = analysisResult.ok
? (analysisResult.value.output as string)
? (analysisResult.output as string)
: `Analysis failed: ${analysisResult.error.message}`;

analyses.push({ filePath: testFilePath, summary });
Expand Down
Loading
Loading