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/examples/realworld-cli/api/agents/analyzer.ts b/examples/realworld-cli/api/agents/analyzer.ts index 1ba713c..e39c911 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"; /** @@ -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: { @@ -32,7 +38,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..89d9514 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"; /** @@ -8,8 +8,14 @@ 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 }) => +export const createScannerAgent = (tools: { readonly ls: Tool; readonly grep: Tool }): Agent => agent({ name: "scanner", model: openai("gpt-4.1"), diff --git a/packages/agents/src/core/agents/base/agent.ts b/packages/agents/src/core/agents/base/agent.ts index dfa2a4d..6ad5391 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,8 @@ 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 = { + model: config.model, 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..4646115 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> + | Partial> | (( - config: AgentConfig, - ) => Partial>), -): Agent; + config: AgentConfig, + ) => Partial>), +): 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..90e1986 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,7 +677,16 @@ export interface Agent< TOutput = string, TTools extends Record = Record, 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. * 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); }