Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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/fix-merge-onstepstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@funkai/agents": patch
---

Fix config-level `onStepStart` hook not being merged when forwarding to sub-agents. Previously only the per-call `onStepStart` was forwarded; the config hook was silently dropped. Also adds `onStepStart` to `AgentConfig` for parity with `onStepFinish`.
2 changes: 1 addition & 1 deletion packages/agents/src/core/agents/base/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ export function agent<
// See packages/agents/docs/core/hooks.md for the full lifecycle.
const parentCtx: ParentAgentContext = {
log,
onStepStart: params.onStepStart,
onStepStart: buildMergedHook(log, config.onStepStart, params.onStepStart),
onStepFinish: buildMergedHook(log, config.onStepFinish, params.onStepFinish),
agentChain: currentChain,
};
Expand Down
7 changes: 7 additions & 0 deletions packages/agents/src/core/agents/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -693,6 +693,13 @@ export interface AgentConfig<
*/
onError?: (event: { input: TInput; error: Error }) => void | Promise<void>;

/**
* Hook: fires when a step starts.
*
* Receives a unified {@link StepStartEvent}.
*/
onStepStart?: (event: StepStartEvent) => void | Promise<void>;

/**
* Hook: fires after each step completes.
*
Expand Down
72 changes: 72 additions & 0 deletions packages/agents/src/integration/lifecycle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1561,6 +1561,78 @@ describe("Agent subagent hook forwarding (integration)", () => {
}
});

it("parent config and per-call onStepStart both fire for flow sub-agent steps", async () => {
const stepEvents: string[] = [];

const sub = flowAgent<{ task: string }, string>(
{
name: "sub-flow",
input: z.object({ task: z.string() }),
output: z.string(),
},
async ({ input, $ }) => {
await $.step({ id: "work", execute: async () => input.task });
return input.task;
},
);

const toolCallModel = new MockLanguageModelV3({
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mockValues sync/async mismatch
doGenerate: mockValues(
{
content: [
{
type: "tool-call" as const,
toolCallId: "tc1",
toolName: "agent_sub",
input: JSON.stringify({ task: "do it" }),
},
],
finishReason: MOCK_FINISH,
usage: MOCK_USAGE,
warnings: [],
},
{
content: [{ type: "text" as const, text: "done" }],
finishReason: MOCK_FINISH,
usage: MOCK_USAGE,
warnings: [],
},
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- mockValues returns sync fn, MockLanguageModelV3 expects PromiseLike
) as any,
});

const parent = agent({
name: "parent-agent",
model: toolCallModel,
system: "Delegate to agent_sub.",
// oxlint-disable-next-line @typescript-eslint/no-explicit-any -- FlowAgent satisfies Agent at runtime
agents: { sub: sub as any },
onStepStart: (event) => {
stepEvents.push(`config:${event.stepId}`);
},
});

await parent.generate({
prompt: "go",
logger: createMockLogger(),
onStepStart: (event) => {
stepEvents.push(`call:${event.stepId}`);
},
});

// Sub-flow's step fires both config and per-call onStepStart from parent
const subSteps = stepEvents.filter((e) => e.includes("work"));
expect(subSteps.length).toBeGreaterThanOrEqual(2);

// Config hook fires before per-call hook
const subConfigIdx = stepEvents.findIndex((e) => e.startsWith("config:") && e.includes("work"));
const subCallIdx = stepEvents.findIndex((e) => e.startsWith("call:") && e.includes("work"));
expect(subConfigIdx).not.toBe(-1);
expect(subCallIdx).not.toBe(-1);
expect(subConfigIdx).toBeLessThan(subCallIdx);
});

it("parent onStart does NOT fire for sub-agent events (type safety)", async () => {
const startEvents: unknown[] = [];

Expand Down
Loading