fix(agents): type safety audit for lifecycle hooks and flow engine#46
fix(agents): type safety audit for lifecycle hooks and flow engine#46zrosenbauer merged 9 commits intomainfrom
Conversation
Generic hooks (onStart, onFinish, onError) were forwarded from parent
agents to sub-agents via type casts that erased TInput/TOutput. This
caused parent hooks to receive sub-agent events with wrong shapes at
runtime — e.g., a hook expecting { userId: string } getting
{ query: string } — with no compiler warning.
- Remove onStart/onFinish/onError from ParentAgentContext
- Delete buildWidenedMergedHook (zero casts remain)
- Only forward fixed-type hooks (onStepStart, onStepFinish, logger)
- Add sub-agent hook forwarding docs with lifecycle diagram
- Update integration tests to assert new behavior
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…signal Co-Authored-By: Claude <noreply@anthropic.com>
…onFinish hooks Co-Authored-By: Claude <noreply@anthropic.com>
…engine Use `any` for the AnyHook event parameter — the only TypeScript type that bypasses function parameter contravariance. Detailed JSDoc explains why this is necessary and where type safety is enforced (public API boundary). Removes unnecessary `as AnyHook` casts at call sites since hooks are now directly assignable. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
🦋 Changeset detectedLatest commit: ecdcd62 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (7)
📝 WalkthroughWalkthroughThreads TOutput through agent params, adds ParentAgentContext to forward only fixed-typed lifecycle hooks and logger to sub-agents, moves merged-hook invocation into the shared fire path, and makes framework-provided fields (input, signal, logger) take precedence when building sub-agent params. Changes
Sequence DiagramsequenceDiagram
participant ParentAgent as Parent Agent (TInput,TOutput)
participant Config as Config/Per-Call
participant ParentCtx as ParentAgentContext
participant SubAgent as Sub-Agent Tool (TInput',TOutput')
participant Execution as Sub-Agent Execution
ParentAgent->>Config: assemble hooks, logger, signal
Config->>ParentCtx: build ParentAgentContext (logger, onStepStart, merged onStepFinish)
ParentCtx->>SubAgent: forward logger, onStepStart, onStepFinish, signal (ctx.signal)
Note over ParentCtx: drop generic hooks: onStart/onFinish/onError
SubAgent->>Execution: generate(params with forwarded parent fields)
Execution->>SubAgent: fire onStepStart -> run step -> fire onStepFinish
SubAgent-->>ParentAgent: return result
ParentAgent->>ParentAgent: handle parent onFinish/onError (typed to parent's TOutput)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
packages/agents/src/core/agents/types.ts (1)
300-345:⚠️ Potential issue | 🟠 MajorPer-call
outputoverrides makeonFinishtype-unsafe.
BaseGenerateParamstypesonFinish.resultasGenerateResult<TOutput>, butGenerateParamsallowsoutput?: OutputParamto change the runtime result shape without updating the hook's type. An agent created withTOutput = stringcan call.generate({ output: Output.object({ schema }), onFinish: ({ result }) => result.output.foo })and typecheck even thoughresult.outputis a string at the type-checker's understanding but an object at runtime.The
as TOutputcast on line 336 prevents TypeScript from catching this. Either makeoutputoverride drive the hook'sGenerateResulttype, or disallowoutputoverrides on the typed path.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/agents/src/core/agents/types.ts` around lines 300 - 345, The onFinish hook's result type is unsound because BaseGenerateParams<TOutput> fixes GenerateResult<TOutput> while GenerateParams allows a runtime output override (OutputParam) that changes the actual result shape; fix by making the output override drive the hook type or forbidding overrides when a concrete TOutput is used — e.g. update GenerateParams to be generic over an OutputParam and derive TOutputFromOutputParam to compute the correct GenerateResult type for onFinish (replace the current unsafe `as TOutput` cast in BaseGenerateParams.onFinish), or alternatively disallow the `output` property on the typed path (make `output?: never` when TOutput is explicitly provided) and remove the cast so TypeScript enforces consistency; refer to BaseGenerateParams, GenerateParams, GenerateResult, OutputParam, and the onFinish declaration to locate and apply this change.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@packages/agents/docs/core/hooks.md`:
- Around line 109-138: The fenced diagram block starting with "Parent.generate({
input, onStepFinish })" in packages/agents/docs/core/hooks.md is missing a
language specifier; update the opening triple-backtick to include a language tag
such as "text" or "plaintext" (e.g., ```text) so the diagram block is properly
marked for markdownlint and consistent rendering.
In `@packages/agents/src/integration/lifecycle.test.ts`:
- Line 332: Replace the ternary expressions in this test (e.g., the expression
"return r.ok ? r.value : 0;" that uses variable r) with an if/else or
early-return style to satisfy the no-ternary rule; for example, check r.ok and
return r.value when true, otherwise return 0. Do the same for the other reported
locations in the file (around the occurrences at the ranges noted in the review)
so all ternaries are converted to explicit if/else or early returns.
---
Outside diff comments:
In `@packages/agents/src/core/agents/types.ts`:
- Around line 300-345: The onFinish hook's result type is unsound because
BaseGenerateParams<TOutput> fixes GenerateResult<TOutput> while GenerateParams
allows a runtime output override (OutputParam) that changes the actual result
shape; fix by making the output override drive the hook type or forbidding
overrides when a concrete TOutput is used — e.g. update GenerateParams to be
generic over an OutputParam and derive TOutputFromOutputParam to compute the
correct GenerateResult type for onFinish (replace the current unsafe `as
TOutput` cast in BaseGenerateParams.onFinish), or alternatively disallow the
`output` property on the typed path (make `output?: never` when TOutput is
explicitly provided) and remove the cast so TypeScript enforces consistency;
refer to BaseGenerateParams, GenerateParams, GenerateResult, OutputParam, and
the onFinish declaration to locate and apply this change.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f8af9c5c-63db-42da-930c-771a16f3840c
📒 Files selected for processing (11)
.changeset/type-safety-audit.mdpackages/agents/docs/core/hooks.mdpackages/agents/src/core/agents/base/agent.tspackages/agents/src/core/agents/base/utils.tspackages/agents/src/core/agents/flow/engine.tspackages/agents/src/core/agents/flow/steps/agent.test.tspackages/agents/src/core/agents/flow/steps/factory.test.tspackages/agents/src/core/agents/flow/steps/factory.tspackages/agents/src/core/agents/flow/types.tspackages/agents/src/core/agents/types.tspackages/agents/src/integration/lifecycle.test.ts
- Replace ternaries with if/else blocks (no-ternary) - Add braces to single-line if statements (curly) - Rename outer $ to $outer/$flow/$step to avoid shadowing (no-shadow) - Use oxlint-disable-next-line for necessary `as any` casts - Capitalize first letter of all comments (capitalized-comments) Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Summary
onStart/onFinish/onErrorto sub-agents. These hooks are parameterized byTInput/TOutputwhich differ between parent and child, causing silent type-safety violations at runtime. Only fixed-type hooks (onStepStart,onStepFinish) are forwarded.buildMergedHookinfireHooks— merged hooks now swallow errors like all other hooks in the system, preventing hook failures from crashing agents.input,signal,logger) can no longer be overwritten by userconfig.configspread.TOutputthroughBaseGenerateParams—onFinishhooks now receiveGenerateResult<TOutput>instead of untypedGenerateResult, enabling type-safe result inspection.AnyHookcontravariance — resolve 5 typecheck errors in flow engine by using a properly documentedanyescape hatch for the internal hook merge utility (function parameter contravariance makes strict typing impossible here).Test plan
pnpm typecheck --filter=@funkai/agents)