From d50d32d5dc8185851c4cfc8ecdf1ddfabe0e783c Mon Sep 17 00:00:00 2001 From: Zac Rosenbauer Date: Thu, 19 Mar 2026 20:49:53 -0400 Subject: [PATCH 1/4] feat(agents): add TModel generic for discriminated model types in evolve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a 5th generic parameter `TModel` to `AgentConfig` and `Agent` that tracks whether the model was set as a static `Model` or a dynamic `Resolver`. This flows through `evolve()` so the mapper callback receives the correctly narrowed type — enabling direct access to `.modelId` without unnecessary `isFunction()` narrowing. Co-Authored-By: Claude --- .changeset/evolve-model-generic.md | 9 +++++++++ packages/agents/src/core/agents/base/agent.ts | 9 +++++---- packages/agents/src/core/agents/base/utils.ts | 6 +++--- packages/agents/src/core/agents/evolve.ts | 16 +++++++++------- packages/agents/src/core/agents/flow/types.ts | 2 +- packages/agents/src/core/agents/types.ts | 6 ++++-- packages/agents/src/lib/runnable.ts | 2 +- 7 files changed, 32 insertions(+), 18 deletions(-) create mode 100644 .changeset/evolve-model-generic.md diff --git a/.changeset/evolve-model-generic.md b/.changeset/evolve-model-generic.md new file mode 100644 index 0000000..59e3065 --- /dev/null +++ b/.changeset/evolve-model-generic.md @@ -0,0 +1,9 @@ +--- +"@funkai/agents": minor +--- + +Add `TModel` generic to `AgentConfig` and `Agent` for discriminated model types in `evolve()`. + +Previously, `evolve(base, (config) => ...)` always typed `config.model` as the full `Resolver` union, even when the base agent was created with a static `LanguageModel`. This required unnecessary narrowing with `isFunction()` before accessing `.modelId`. + +Now the 5th generic `TModel` is inferred from `agent()` and threaded through `evolve()`, so `config.model` is correctly typed as `Model` (with `.modelId`) when the base agent uses a static model. diff --git a/packages/agents/src/core/agents/base/agent.ts b/packages/agents/src/core/agents/base/agent.ts index dfa2a4d..ccb16ab 100644 --- a/packages/agents/src/core/agents/base/agent.ts +++ b/packages/agents/src/core/agents/base/agent.ts @@ -25,7 +25,7 @@ import { createDefaultLogger } from "@/core/logger.js"; import type { Logger } from "@/core/logger.js"; import type { LanguageModel } from "@/core/provider/types.js"; import type { Tool } from "@/core/tool.js"; -import type { StepFinishEvent, StreamPart } from "@/core/types.js"; +import type { Model, StepFinishEvent, StreamPart } from "@/core/types.js"; import { fireHooks, wrapHook } from "@/lib/hooks.js"; import { withModelMiddleware } from "@/lib/middleware.js"; import { AGENT_CONFIG, RUNNABLE_META } from "@/lib/runnable.js"; @@ -86,9 +86,10 @@ export function agent< TTools extends Record = {}, // oxlint-disable-next-line typescript-eslint/ban-types TSubAgents extends SubAgents = {}, + TModel extends Resolver = Resolver, >( - config: AgentConfig, -): Agent { + config: AgentConfig, +): Agent { /** * Extract the raw input from unified params. * @@ -538,7 +539,7 @@ export function agent< } // eslint-disable-next-line no-shadow -- Local variable is the return value constructed inside its own factory function - const agent: Agent = { + const agent: Agent = { generate, stream, fn: () => generate, diff --git a/packages/agents/src/core/agents/base/utils.ts b/packages/agents/src/core/agents/base/utils.ts index cd9fdb3..b177106 100644 --- a/packages/agents/src/core/agents/base/utils.ts +++ b/packages/agents/src/core/agents/base/utils.ts @@ -29,7 +29,7 @@ import type { RunnableMeta } from "@/lib/runnable.js"; export function buildAITools( tools?: Record, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Agent generic params are contravariant; `unknown` breaks assignability - agents?: Record>, + agents?: Record>, // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ToolSet requires `any` values; `unknown` breaks assignability with AI SDK ): Record | undefined { const hasTools = isNotNil(tools) && Object.keys(tools).length > 0; @@ -251,7 +251,7 @@ function resolveToolName(meta: RunnableMeta | undefined, fallback: string): stri */ function buildAgentTools( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Agent generic params are contravariant; `unknown` breaks assignability - agents: Record> | undefined, + agents: Record> | undefined, tools: Record | undefined, ): Record { if (!agents) { @@ -294,7 +294,7 @@ function buildAgentTools( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- ToolSet requires `any` values; `unknown` breaks assignability with AI SDK function buildAgentTool( // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Agent generic params are contravariant; `unknown` breaks assignability - runnable: Agent, + runnable: Agent, meta: RunnableMeta | undefined, toolName: string, tools: Record | undefined, diff --git a/packages/agents/src/core/agents/evolve.ts b/packages/agents/src/core/agents/evolve.ts index 9bfd5f3..30affba 100644 --- a/packages/agents/src/core/agents/evolve.ts +++ b/packages/agents/src/core/agents/evolve.ts @@ -11,8 +11,9 @@ import type { FlowAgentHandler, FlowSubAgents, } from "@/core/agents/flow/types.js"; -import type { Agent, AgentConfig, SubAgents } from "@/core/agents/types.js"; +import type { Agent, AgentConfig, Resolver, SubAgents } from "@/core/agents/types.js"; import type { Tool } from "@/core/tool.js"; +import type { Model } from "@/core/types.js"; import { getAgentConfig, getFlowAgentConfig, isAgent, isFlowAgent } from "@/lib/runnable.js"; /** @@ -79,14 +80,15 @@ export function evolve< TOutput, TTools extends Record, TSubAgents extends SubAgents, + TModel extends Resolver, >( - base: Agent, + base: Agent, overrides: | Partial> | (( - config: AgentConfig, + config: AgentConfig, ) => Partial>), -): Agent; +): Agent; /** * Create a new flow agent from an existing one with config overrides. @@ -154,7 +156,7 @@ function evolveAgent( overridesOrMapper: Record | ((config: any) => Record), ): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- internal: stored config type is erased - const baseConfig = getAgentConfig>(base); + const baseConfig = getAgentConfig>(base); if (isNil(baseConfig)) { throw new Error("Cannot evolve: agent does not have stored configuration."); } @@ -203,9 +205,9 @@ function evolveFlowAgent( */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- internal merge operates on erased types function mergeAgentConfigs( - base: AgentConfig, + base: AgentConfig, overrides: Record, -): AgentConfig { +): AgentConfig { const { tools: overrideTools, agents: overrideAgents, ...restOverrides } = overrides; return { ...base, diff --git a/packages/agents/src/core/agents/flow/types.ts b/packages/agents/src/core/agents/flow/types.ts index cd2c647..bd18b34 100644 --- a/packages/agents/src/core/agents/flow/types.ts +++ b/packages/agents/src/core/agents/flow/types.ts @@ -42,7 +42,7 @@ export type { StepInfo } from "@/core/types.js"; * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type FlowSubAgents = Record | FlowAgent>; +export type FlowSubAgents = Record | FlowAgent>; /** * Result of a completed flow agent generation. diff --git a/packages/agents/src/core/agents/types.ts b/packages/agents/src/core/agents/types.ts index 1321d72..4921153 100644 --- a/packages/agents/src/core/agents/types.ts +++ b/packages/agents/src/core/agents/types.ts @@ -117,7 +117,7 @@ export type ToolName = S extends "" * ``` */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -export type SubAgents = Record>; +export type SubAgents = Record>; /** * Chat message type. @@ -500,6 +500,7 @@ export interface AgentConfig< TOutput, TTools extends Record, TSubAgents extends SubAgents, + TModel extends Resolver = Resolver, > { /** * Unique agent name. @@ -518,7 +519,7 @@ export interface AgentConfig< * @see {@link Model} * @see {@link Resolver} */ - model: Resolver; + model: TModel; /** * Zod schema for the agent's typed input. @@ -676,6 +677,7 @@ export interface Agent< TOutput = string, TTools extends Record = Record, TSubAgents extends SubAgents = Record, + TModel extends Resolver = Resolver, > { /** * Run the agent to completion. diff --git a/packages/agents/src/lib/runnable.ts b/packages/agents/src/lib/runnable.ts index 160f29a..d463bd9 100644 --- a/packages/agents/src/lib/runnable.ts +++ b/packages/agents/src/lib/runnable.ts @@ -63,7 +63,7 @@ export interface RunnableMeta { * @internal */ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Widest instantiation; concrete generics are unknown at runtime -export function isAgent(value: unknown): value is Agent { +export function isAgent(value: unknown): value is Agent { return isObject(value) && has(value, AGENT_CONFIG); } From b16c3923bd0fa6553f4adcfa10383e237e83046f Mon Sep 17 00:00:00 2001 From: Zac Rosenbauer Date: Thu, 19 Mar 2026 20:57:10 -0400 Subject: [PATCH 2/4] fix(examples): add explicit Agent return types for TS2742 portability The new TModel generic causes TypeScript to infer LanguageModelV3 from @ai-sdk/provider in the return type. Without an explicit annotation, TS2742 fires because the example package doesn't directly depend on that module. Adding `Agent` return types resolves the portability error. Co-Authored-By: Claude --- examples/realworld-cli/api/agents/analyzer.ts | 4 ++-- examples/realworld-cli/api/agents/scanner.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/realworld-cli/api/agents/analyzer.ts b/examples/realworld-cli/api/agents/analyzer.ts index 1ba713c..b2afe8e 100644 --- a/examples/realworld-cli/api/agents/analyzer.ts +++ b/examples/realworld-cli/api/agents/analyzer.ts @@ -1,6 +1,6 @@ import { openai } from "@ai-sdk/openai"; import { agent, evolve } from "@funkai/agents"; -import type { Tool } from "@funkai/agents"; +import type { Agent, Tool } from "@funkai/agents"; import { prompts } from "~prompts"; /** @@ -32,7 +32,7 @@ export const createAnalyzerAgent = ( readonly ls: Tool; }, testFilePath: string, -) => +): Agent => evolve(baseAnalyzer, { system: prompts.agents.analyzer.render({ testFilePath, diff --git a/examples/realworld-cli/api/agents/scanner.ts b/examples/realworld-cli/api/agents/scanner.ts index 06960a9..4f2b5a6 100644 --- a/examples/realworld-cli/api/agents/scanner.ts +++ b/examples/realworld-cli/api/agents/scanner.ts @@ -1,6 +1,6 @@ import { openai } from "@ai-sdk/openai"; import { agent } from "@funkai/agents"; -import type { Tool } from "@funkai/agents"; +import type { Agent, Tool } from "@funkai/agents"; import { prompts } from "~prompts"; /** @@ -9,7 +9,7 @@ import { prompts } from "~prompts"; * @param tools - The filesystem tools scoped to the target directory. * @returns An agent configured to scan for test files. */ -export const createScannerAgent = (tools: { readonly ls: Tool; readonly grep: Tool }) => +export const createScannerAgent = (tools: { readonly ls: Tool; readonly grep: Tool }): Agent => agent({ name: "scanner", model: openai("gpt-4.1"), From a7635901aefa93d062dbd680fadaa25e7fb29447 Mon Sep 17 00:00:00 2001 From: Zac Rosenbauer Date: Thu, 19 Mar 2026 21:01:45 -0400 Subject: [PATCH 3/4] fix: address PR review feedback Resolved review threads: - evolve.ts: preserve TModel in override Partial types for type soundness - analyzer.ts: add @example to exported function JSDoc - scanner.ts: add @example to exported function JSDoc Co-Authored-By: Claude --- examples/realworld-cli/api/agents/analyzer.ts | 6 ++++++ examples/realworld-cli/api/agents/scanner.ts | 6 ++++++ packages/agents/src/core/agents/evolve.ts | 4 ++-- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/examples/realworld-cli/api/agents/analyzer.ts b/examples/realworld-cli/api/agents/analyzer.ts index b2afe8e..e39c911 100644 --- a/examples/realworld-cli/api/agents/analyzer.ts +++ b/examples/realworld-cli/api/agents/analyzer.ts @@ -24,6 +24,12 @@ const baseAnalyzer = agent({ * @param tools - The filesystem tools scoped to the target directory. * @param testFilePath - The path to the test file being analyzed. * @returns An agent configured to analyze test quality. + * + * @example + * ```ts + * const analyzer = createAnalyzerAgent(fsTools, "src/foo.test.ts") + * const result = await analyzer.generate({ prompt: "Review this test file" }) + * ``` */ export const createAnalyzerAgent = ( tools: { diff --git a/examples/realworld-cli/api/agents/scanner.ts b/examples/realworld-cli/api/agents/scanner.ts index 4f2b5a6..89d9514 100644 --- a/examples/realworld-cli/api/agents/scanner.ts +++ b/examples/realworld-cli/api/agents/scanner.ts @@ -8,6 +8,12 @@ import { prompts } from "~prompts"; * * @param tools - The filesystem tools scoped to the target directory. * @returns An agent configured to scan for test files. + * + * @example + * ```ts + * const scanner = createScannerAgent(fsTools) + * const result = await scanner.generate({ prompt: "Find test files" }) + * ``` */ export const createScannerAgent = (tools: { readonly ls: Tool; readonly grep: Tool }): Agent => agent({ diff --git a/packages/agents/src/core/agents/evolve.ts b/packages/agents/src/core/agents/evolve.ts index 30affba..4646115 100644 --- a/packages/agents/src/core/agents/evolve.ts +++ b/packages/agents/src/core/agents/evolve.ts @@ -84,10 +84,10 @@ export function evolve< >( base: Agent, overrides: - | Partial> + | Partial> | (( config: AgentConfig, - ) => Partial>), + ) => Partial>), ): Agent; /** From 23481e7f158c50c50baa07f50dc6af001919ec32 Mon Sep 17 00:00:00 2001 From: Zac Rosenbauer Date: Thu, 19 Mar 2026 21:17:43 -0400 Subject: [PATCH 4/4] fix(agents): expose model on Agent interface to fix unused TModel lint error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TModel was a phantom generic on Agent — structurally invisible to TypeScript, so evolve() could not infer concrete model types. Adds readonly model property to Agent interface and exposes config.model on the agent factory return value. Co-Authored-By: Claude --- packages/agents/src/core/agents/base/agent.ts | 1 + packages/agents/src/core/agents/types.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/packages/agents/src/core/agents/base/agent.ts b/packages/agents/src/core/agents/base/agent.ts index ccb16ab..6ad5391 100644 --- a/packages/agents/src/core/agents/base/agent.ts +++ b/packages/agents/src/core/agents/base/agent.ts @@ -540,6 +540,7 @@ export function agent< // eslint-disable-next-line no-shadow -- Local variable is the return value constructed inside its own factory function const agent: Agent = { + model: config.model, generate, stream, fn: () => generate, diff --git a/packages/agents/src/core/agents/types.ts b/packages/agents/src/core/agents/types.ts index 4921153..90e1986 100644 --- a/packages/agents/src/core/agents/types.ts +++ b/packages/agents/src/core/agents/types.ts @@ -679,6 +679,14 @@ export interface Agent< TSubAgents extends SubAgents = Record, TModel extends Resolver = Resolver, > { + /** + * The model (or resolver) used by this agent. + * + * Exposes the value passed via `AgentConfig.model` so that + * `evolve()` can infer and preserve the concrete model type. + */ + readonly model: TModel; + /** * Run the agent to completion. *