Summary
Add a prepare option to AgentConfig — a single async function that resolves multiple config fields at once, avoiding redundant async lookups when multiple Resolvers need the same data.
Current state
funkai agents support per-field Resolver<TInput, T> functions (packages/agents/src/core/agents/types.ts:57) that can be async and receive { input }:
const myAgent = agent({
name: 'support',
input: z.object({ userId: z.string() }),
model: async ({ input }) => pickModel(input),
system: async ({ input }) => buildSystem(input),
tools: async ({ input }) => pickTools(input),
});
This works well for independent fields, but when multiple fields depend on the same async data, each Resolver fetches independently:
const myAgent = agent({
name: 'support',
input: z.object({ userId: z.string() }),
// fetchUser called 3 times for the same userId
model: async ({ input }) => {
const user = await fetchUser(input.userId);
return user.tier === 'pro' ? openai('gpt-4.1') : openai('gpt-4.1-mini');
},
system: async ({ input }) => {
const user = await fetchUser(input.userId);
return `You are a support agent.\nUser: ${user.name} (${user.tier})`;
},
tools: async ({ input }) => {
const user = await fetchUser(input.userId);
return user.role === 'admin' ? allTools : readOnlyTools;
},
});
Users can work around this with external caching, but it's boilerplate the framework should handle.
Background
The Vercel AI SDK's ToolLoopAgent solves this with prepareCall — a single function that receives all settings and returns modified settings. However, prepareCall exists primarily because class constructors can't be async, so they need a separate hook at .generate() time. funkai doesn't have this limitation since we use factory functions + Resolvers.
Rather than porting prepareCall as-is, a prepare resolver fits funkai's existing patterns more naturally.
Proposed API
A prepare function on AgentConfig that receives { input } and returns a partial config. Runs once per .generate() / .stream() call, after input validation. Fields returned by prepare override the static config but are overridden by per-call params.
const myAgent = agent({
name: 'support',
input: z.object({ userId: z.string() }),
prepare: async ({ input }) => {
const user = await fetchUser(input.userId);
return {
model: user.tier === 'pro' ? openai('gpt-4.1') : openai('gpt-4.1-mini'),
system: `You are a support agent.\nUser: ${user.name} (${user.tier})`,
tools: user.role === 'admin' ? allTools : readOnlyTools,
};
},
});
Resolution order
static config → prepare({ input }) → per-call params
Each layer overrides the previous. Per-field Resolvers and prepare are mutually compatible — prepare runs first, then any per-field Resolvers that are also defined run on top.
Fields prepare can return
All config fields that currently accept Resolver should be returnable from prepare:
model
system
tools
agents
maxSteps
logger
Static/structural fields (name, input, output, middleware) are not overridable via prepare.
Subagent benefit
When used as a subagent, prepare fires automatically each time the parent invokes it:
const researcher = agent({
name: 'researcher',
input: z.object({ query: z.string() }),
prepare: async ({ input }) => {
const docs = await vectorSearch(input.query);
return {
system: `You are a research agent.\n\nRelevant docs:\n${docs}`,
};
},
tools: { search: searchTool },
});
const orchestrator = agent({
name: 'orchestrator',
model: openai('gpt-4.1'),
agents: { researcher }, // researcher.prepare fires on each delegation
});
Implementation notes
prepare runs in agent.ts after input validation (~line 341), before prepareGeneration() (~line 353)
- The return type is
Partial<Pick<AgentConfig, 'model' | 'system' | 'tools' | 'agents' | 'maxSteps' | 'logger'>>
- Resolved
prepare values are merged into the config before prepareGeneration() resolves individual Resolvers
prepare is not overridable via per-call params (it's a definition-time concern, not a call-site concern)
- Needs a decision on interaction with per-field Resolvers when both are defined on the same field
Open questions
- Should
prepare receive the static config values (pre-Resolver) so it can extend them, or just { input }?
- If both
prepare and a per-field Resolver are set for the same field (e.g., system), which wins? Suggestion: prepare runs first, per-field Resolver runs on top (but this may be confusing — might be simpler to make them mutually exclusive per-field)
Summary
Add a
prepareoption toAgentConfig— a single async function that resolves multiple config fields at once, avoiding redundant async lookups when multiple Resolvers need the same data.Current state
funkai agents support per-field
Resolver<TInput, T>functions (packages/agents/src/core/agents/types.ts:57) that can be async and receive{ input }:This works well for independent fields, but when multiple fields depend on the same async data, each Resolver fetches independently:
Users can work around this with external caching, but it's boilerplate the framework should handle.
Background
The Vercel AI SDK's
ToolLoopAgentsolves this withprepareCall— a single function that receives all settings and returns modified settings. However,prepareCallexists primarily because class constructors can't be async, so they need a separate hook at.generate()time. funkai doesn't have this limitation since we use factory functions + Resolvers.Rather than porting
prepareCallas-is, aprepareresolver fits funkai's existing patterns more naturally.Proposed API
A
preparefunction onAgentConfigthat receives{ input }and returns a partial config. Runs once per.generate()/.stream()call, after input validation. Fields returned byprepareoverride the static config but are overridden by per-call params.Resolution order
Each layer overrides the previous. Per-field Resolvers and
prepareare mutually compatible —prepareruns first, then any per-field Resolvers that are also defined run on top.Fields
preparecan returnAll config fields that currently accept
Resolvershould be returnable fromprepare:modelsystemtoolsagentsmaxStepsloggerStatic/structural fields (
name,input,output,middleware) are not overridable viaprepare.Subagent benefit
When used as a subagent,
preparefires automatically each time the parent invokes it:Implementation notes
prepareruns inagent.tsafter input validation (~line 341), beforeprepareGeneration()(~line 353)Partial<Pick<AgentConfig, 'model' | 'system' | 'tools' | 'agents' | 'maxSteps' | 'logger'>>preparevalues are merged into the config beforeprepareGeneration()resolves individual Resolversprepareis not overridable via per-call params (it's a definition-time concern, not a call-site concern)Open questions
preparereceive the static config values (pre-Resolver) so it can extend them, or just{ input }?prepareand a per-field Resolver are set for the same field (e.g.,system), which wins? Suggestion:prepareruns first, per-field Resolver runs on top (but this may be confusing — might be simpler to make them mutually exclusive per-field)