Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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).toBeGreaterThan(0);

// 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