Skip to content
This repository was archived by the owner on Jun 15, 2026. It is now read-only.

Commit 2d7b765

Browse files
zrosenbauerclaude
andauthored
fix(agents): pass through full AI SDK StepResult in onStepFinish (#55)
Co-authored-by: Claude <noreply@anthropic.com>
1 parent a9db10b commit 2d7b765

58 files changed

Lines changed: 988 additions & 1328 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@funkai/agents": major
3+
---
4+
5+
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`).
6+
7+
**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 }`.

docs/concepts/flow-agents.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const pipeline = flowAgent(
3131
execute: async () => input.topic.toLowerCase().replace(/\s+/g, "-"),
3232
});
3333

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

45-
return { text: draft.value.output };
45+
return { text: draft.output };
4646
},
4747
);
4848

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

5858
## The $ step builder
5959

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

6262
| Operation | Description |
6363
| ---------- | ------------------------------------------------------ |
@@ -86,7 +86,7 @@ const result = await pipeline.stream({ input: { topic: "closures" } });
8686
if (result.ok) {
8787
for await (const event of result.fullStream) {
8888
if (event.type === "step:finish") {
89-
console.log(event.step.id, "done in", event.duration, "ms");
89+
console.log(event.stepId, "done in", event.duration, "ms");
9090
}
9191
}
9292
}

docs/introduction.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ const pipeline = flowAgent(
5353
execute: async ({ item, $ }) => {
5454
const result = await $.agent({ id: "write", agent: writer, input: item });
5555
if (result.ok) {
56-
return result.value.output;
56+
return result.output;
5757
}
5858
return "";
5959
},
6060
concurrency: 3,
6161
});
6262
if (docs.ok) {
63-
return { docs: docs.value };
63+
return { docs: docs.output };
6464
}
6565
return { docs: [] };
6666
},

docs/principles.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ const counter = flowAgent(
5656
input: input.prompt,
5757
});
5858
if (result.ok) {
59-
answer = result.value.output;
59+
answer = result.output;
6060
}
6161
attempts += 1;
6262
}
@@ -112,7 +112,7 @@ const pipeline = flowAgent(
112112
// Untraced -- plain function call, not in trace
113113
let analysisText;
114114
if (analysis.ok) {
115-
analysisText = analysis.value.output;
115+
analysisText = analysis.output;
116116
} else {
117117
analysisText = input.text;
118118
}
@@ -122,7 +122,7 @@ const pipeline = flowAgent(
122122
const final = await $.step({ id: "format", execute: () => formatOutput(cleaned) });
123123

124124
if (final.ok) {
125-
return { result: final.value };
125+
return { result: final.output };
126126
}
127127
return { result: cleaned };
128128
},

docs/quick-start.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -108,13 +108,13 @@ const pipeline = flowAgent(
108108
const reviewed = await $.agent({
109109
id: "review-draft",
110110
agent: reviewer,
111-
input: { draft: draft.value.output },
111+
input: { draft: draft.output },
112112
});
113113

114114
if (reviewed.ok) {
115-
return { final: reviewed.value.output };
115+
return { final: reviewed.output };
116116
}
117-
return { final: draft.value.output };
117+
return { final: draft.output };
118118
},
119119
);
120120

docs/reference/flow-agent.md

Lines changed: 53 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function flowAgent<TInput>(
3232
| `logger` | `Resolver<TInput, Logger>` | No | default | Pino-compatible logger |
3333
| `onStart` | `(event: { input: TInput }) => void \| Promise<void>` | No || Fires when flow starts |
3434
| `onError` | `(event: { input: TInput; error: Error }) => void \| Promise<void>` | No || Fires on error |
35-
| `onStepStart` | `(event: { step: StepInfo }) => void \| Promise<void>` | No || Fires when a `$` step starts |
35+
| `onStepStart` | `(event: StepStartEvent) => void \| Promise<void>` | No || Fires when a `$` step starts |
3636
| `onStepFinish` | `(event: StepFinishEvent) => void \| Promise<void>` | No || Fires when a `$` step finishes |
3737

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

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

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

112112
### StepConfig
113113

@@ -220,12 +220,26 @@ interface RaceConfig {
220220
}
221221
```
222222

223-
## StepResult
223+
## FlowStepResult
224224

225225
```typescript
226-
type StepResult<T> =
227-
| { ok: true; value: T; step: StepInfo; duration: number }
228-
| { ok: false; error: StepError; step: StepInfo; duration: number };
226+
type FlowStepResult<T> =
227+
| {
228+
ok: true;
229+
output: T;
230+
stepId: string;
231+
stepOperation: OperationType;
232+
agentChain?: AgentChainEntry[];
233+
duration: number;
234+
}
235+
| {
236+
ok: false;
237+
error: StepError;
238+
stepId: string;
239+
stepOperation: OperationType;
240+
agentChain?: AgentChainEntry[];
241+
duration: number;
242+
};
229243

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

257-
## StepInfo
271+
## StepStartEvent
258272

259273
```typescript
260-
interface StepInfo {
261-
id: string;
262-
index: number; // auto-incrementing, starts at 0
263-
type: OperationType;
274+
interface StepStartEvent {
275+
stepId: string; // from the $ config's `id` field
276+
stepOperation: OperationType; // 'step' | 'agent' | 'map' | 'each' | 'reduce' | 'while' | 'all' | 'race'
277+
agentChain?: AgentChainEntry[];
264278
}
265279
```
266280

267281
## StepFinishEvent
268282

269-
Emitted by `onStepFinish`. Agent tool-loop steps populate the left columns; flow orchestration steps populate the right.
270-
271-
| Field | Type | Present on |
272-
| ------------- | -------------------------------------------------------------------- | ------------------------ |
273-
| `stepId` | `string` | Agent tool-loop steps |
274-
| `toolCalls` | `readonly { toolName: string; argsTextLength: number }[]` | Agent tool-loop steps |
275-
| `toolResults` | `readonly { toolName: string; resultTextLength: number }[]` | Agent tool-loop steps |
276-
| `usage` | `{ inputTokens: number; outputTokens: number; totalTokens: number }` | Agent tool-loop steps |
277-
| `step` | `StepInfo` | Flow orchestration steps |
278-
| `result` | `unknown` | Flow orchestration steps |
279-
| `duration` | `number` | Flow orchestration steps |
283+
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.
284+
285+
| Field | Type | Present on | Description |
286+
| --------------- | ---------------------------------------------- | ---------------------------------- | ---------------------------------------------- |
287+
| `stepId` | `string` | All steps | funkai addition: the `$` config `id` |
288+
| `stepOperation` | `OperationType` | All steps | funkai addition: operation type |
289+
| `agentChain` | `AgentChainEntry[]` | All steps | funkai addition: agent ancestry chain |
290+
| `stepNumber` | `number` | Agent tool-loop + flow `$.agent()` | AI SDK: zero-based step index |
291+
| `text` | `string` | Agent tool-loop + flow `$.agent()` | AI SDK: generated text |
292+
| `toolCalls` | `TypedToolCall<ToolSet>[]` | Agent tool-loop + flow `$.agent()` | AI SDK: full tool call objects with `input` |
293+
| `toolResults` | `TypedToolResult<ToolSet>[]` | Agent tool-loop + flow `$.agent()` | AI SDK: full tool result objects with `output` |
294+
| `finishReason` | `FinishReason` | Agent tool-loop + flow `$.agent()` | AI SDK: why the step ended |
295+
| `usage` | `LanguageModelUsage` | Agent tool-loop + flow `$.agent()` | AI SDK: token usage |
296+
| `reasoning` | `ReasoningPart[]` | Agent tool-loop + flow `$.agent()` | AI SDK: reasoning content |
297+
| `sources` | `Source[]` | Agent tool-loop + flow `$.agent()` | AI SDK: cited sources |
298+
| `response` | `LanguageModelResponseMetadata & { messages }` | Agent tool-loop + flow `$.agent()` | AI SDK: response metadata |
299+
| `output` | `unknown` | Flow orchestration steps | Flow step output value |
300+
| `duration` | `number` | Flow orchestration steps | Flow step duration in ms |
280301

281302
## FlowAgentOverrides
282303

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

312333
### CustomStepFactory

examples/flow-agent-steps/src/index.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ const stepsDemo = flowAgent(
4444
throw new Error(`Validation failed: ${stepResult.error.message}`);
4545
}
4646

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

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

67-
const doubled = mapResult.value;
67+
const doubled = mapResult.output;
6868

6969
// -----------------------------------------------------------------------
7070
// $.each — sequential side effects (returns void)
@@ -84,7 +84,7 @@ const stepsDemo = flowAgent(
8484
id: "sum-numbers",
8585
input: doubled,
8686
initial: 0,
87-
execute: async ({ item, accumulator }) => {
87+
execute: async ({ item, accumulator }: { item: number; accumulator: number }) => {
8888
return accumulator + item;
8989
},
9090
});
@@ -153,11 +153,11 @@ const stepsDemo = flowAgent(
153153

154154
return {
155155
doubled,
156-
sum: reduceResult.value,
156+
sum: reduceResult.output,
157157
logged: eachResult.ok,
158-
countdown: whileResult.ok ? (whileResult.value ?? 0) : 0,
159-
parallel: allResult.value,
160-
fastest: raceResult.value,
158+
countdown: whileResult.ok ? (whileResult.output ?? 0) : 0,
159+
parallel: allResult.output,
160+
fastest: raceResult.output,
161161
};
162162
},
163163
);

examples/flow-agent/src/index.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,11 @@ const summarizeAndTranslate = flowAgent(
2929
summary: z.string(),
3030
translation: z.string(),
3131
}),
32-
onStepStart: ({ step }) => {
33-
console.log(` → step started: ${step.id} (${step.type})`);
32+
onStepStart: ({ stepId, stepOperation }) => {
33+
console.log(` → step started: ${stepId} (${stepOperation})`);
3434
},
35-
onStepFinish: ({ step, duration }) => {
36-
if (step) {
37-
console.log(` ✓ step finished: ${step.id} (${duration}ms)`);
38-
}
35+
onStepFinish: ({ stepId, duration }) => {
36+
console.log(` ✓ step finished: ${stepId} (${duration}ms)`);
3937
},
4038
},
4139
async ({ input, $ }) => {
@@ -50,7 +48,7 @@ const summarizeAndTranslate = flowAgent(
5048
throw new Error(`Summarization failed: ${summaryResult.error.message}`);
5149
}
5250

53-
const summary = String(summaryResult.value.output);
51+
const summary = String(summaryResult.output);
5452

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

6664
return {
6765
summary,
68-
translation: String(translationResult.value.output),
66+
translation: String(translationResult.output),
6967
};
7068
},
7169
);

examples/prompts-subagents/src/index.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ const pipeline = flowAgent(
5353
const draft = await $.agent({
5454
id: "draft",
5555
agent: writer,
56-
input: `Write an article based on these findings:\n${research.value.output}`,
56+
input: `Write an article based on these findings:\n${research.output}`,
5757
});
5858

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

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

7373
return {
74-
article: String(draft.value.output),
75-
verdict: String(review.value.output),
74+
article: String(draft.output),
75+
verdict: String(review.output),
7676
};
7777
},
7878
);

examples/realworld-cli/api/pipeline.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,12 +42,12 @@ export const createPipeline = (
4242
targetDir: z.string().describe("The directory path being scanned"),
4343
}),
4444
output: pipelineOutputSchema,
45-
onStepStart: ({ step }) => {
46-
emit({ type: "step:start", stepId: step.id, stepType: step.type });
45+
onStepStart: ({ stepId, stepOperation }) => {
46+
emit({ type: "step:start", stepId, stepType: stepOperation });
4747
},
48-
onStepFinish: ({ step, duration }) => {
49-
if (step && duration !== undefined) {
50-
emit({ type: "step:finish", stepId: step.id, duration });
48+
onStepFinish: ({ stepId, duration }) => {
49+
if (duration !== undefined) {
50+
emit({ type: "step:finish", stepId, duration });
5151
}
5252
},
5353
},
@@ -71,7 +71,7 @@ export const createPipeline = (
7171
throw new Error(`Scanner failed: ${scanResult.error.message}`);
7272
}
7373

74-
const scanOutput = scanResult.value.output as string;
74+
const scanOutput = scanResult.output as string;
7575
const testFilePaths = extractTestFilePaths(scanOutput);
7676

7777
emit({ type: "scan-complete", files: testFilePaths });
@@ -110,7 +110,7 @@ export const createPipeline = (
110110
});
111111

112112
const summary = analysisResult.ok
113-
? (analysisResult.value.output as string)
113+
? (analysisResult.output as string)
114114
: `Analysis failed: ${analysisResult.error.message}`;
115115

116116
analyses.push({ filePath: testFilePath, summary });

0 commit comments

Comments
 (0)