fix(eve): keep agent router models serializable

Signed-off-by: Casey Gowrie <ctgowrie@gmail.com>
This commit is contained in:
Casey Gowrie
2026-09-18 17:25:13 -04:00
parent 25b7ad50cc
commit 87bdcb7474
5 changed files with 16 additions and 15 deletions
+4 -2
View File
@@ -426,7 +426,7 @@ const target = await auto({
});
```
`message` and `agents` are required. `model`, `instructions`, and `abortSignal` are optional; `model` accepts an evaluation model instance or ID, and the helper defaults to `typesafe-ai/jev` and `"Which subagent should handle this task?"`. It trims descriptions, ignores empty entries, skips evaluation for a sole candidate, and returns the selected agent name.
`message` and `agents` are required. `model`, `instructions`, and `abortSignal` are optional; `model` accepts a serializable evaluation model ID, and the helper defaults to `typesafe-ai/jev` and `"Which subagent should handle this task?"`. It trims descriptions, ignores empty entries, skips evaluation for a sole candidate, and returns the selected agent name.
`ctx.agents` is a replay-stable metadata snapshot taken when the workflow starts. In a top-level root workflow, `ctx.agents.agent` always represents the root-copy target and carries the root's authored `description` when provided. The snapshot also includes declared agents hidden from the parent model with `tool: false` or `disableTool()`, but exposes no model definitions, credentials, or callbacks. A delegated root copy omits `agent`, so `agentRouter()` cannot select another root copy recursively. Invocation still checks each target's availability through `ctx.agent()`.
@@ -440,7 +440,7 @@ import { agentRouter } from "eve/tools/agent-router";
export default agentRouter();
```
Pass `model` or `instructions` to customize the provided router. Because factory options become durable workflow configuration, `model` must be a serializable model ID; use `auto()` directly when you need an evaluation model instance:
Pass `model` or `instructions` to customize the provided router:
```ts title="agent/tools/agent.ts"
import { agentRouter } from "eve/tools/agent-router";
@@ -451,6 +451,8 @@ export default agentRouter({
});
```
Both `auto()` and `agentRouter()` accept model IDs only because their configuration crosses durable workflow boundaries. To use a live evaluation model instance, write your own `"use step"` helper and create or import the model inside that step instead of passing it as an argument.
Its input is `{ message: string, outputSchema?: object }`. `agentRouter()` ignores entries without a non-empty description. With two or more described targets, it sends the message and effective descriptions to JEV, then invokes the selected name through `ctx.agent()`. It invokes a sole target without evaluation and forwards an optional `outputSchema` unchanged. In a top-level root session, the candidates include the root-copy `agent`; delegated root copies and declared subagent contexts include only their declared targets.
When an authored tool accepts a JSON Schema supplied by the model, represent that input with a permissive object such as `z.looseObject({})`. Avoid `z.record(z.string(), z.json())`: its generated JSON Schema uses `propertyNames`, which OpenAI does not support. Validate the supplied value at the point where your tool consumes it.
@@ -2,7 +2,7 @@
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 53,
"sha256": "cec3f516eccdd2bf4b08ec7bc75048b29cb50233e9effa983707b232b3282455",
"sha256": "f591e6389c338895fbb342d81d10507d1b3374fef6a5ce2dd90efe84e9fdd856",
"exports": [
"AgentRouterAutoOptions",
"AgentRouterInput",
@@ -46,7 +46,7 @@ export async function auto({
if (typeof instructions !== "string" || instructions.trim().length === 0) {
throw new Error("agentRouter auto requires non-empty instructions when provided.");
}
if (typeof model === "string" && model.trim().length === 0) {
if (typeof model !== "string" || model.trim().length === 0) {
throw new Error("agentRouter auto requires a non-empty model ID when provided.");
}
const criteria = Object.fromEntries(
@@ -1,22 +1,11 @@
import { z } from "#compiled/zod/index.js";
import type { JsonObject } from "#shared/json.js";
import type { evaluate } from "#ai/evaluate.js";
export { auto, executeAgentRouterTool } from "#execution/tools/agent-router-workflow.js";
export const AGENT_ROUTER_TOOL_DESCRIPTION =
"Route a task to the best available subagent based on each subagent's declared description.";
export interface AgentRouterAutoOptions {
readonly abortSignal?: AbortSignal;
readonly agents: Readonly<Record<string, string>>;
/** Instructions used to select an agent. */
readonly instructions?: string;
readonly message: string;
/** Evaluation model instance or ID. Defaults to TypeSafe Jev. */
readonly model?: Parameters<typeof evaluate>[0]["model"];
}
export interface AgentRouterOptions {
/** Instructions used to select an agent. */
readonly instructions?: string;
@@ -24,6 +13,12 @@ export interface AgentRouterOptions {
readonly model?: string;
}
export interface AgentRouterAutoOptions extends AgentRouterOptions {
readonly abortSignal?: AbortSignal;
readonly agents: Readonly<Record<string, string>>;
readonly message: string;
}
export interface AgentRouterInput {
readonly message: string;
readonly outputSchema?: JsonObject;
@@ -184,6 +184,10 @@ describe("agentRouter", () => {
error: "agentRouter auto requires a non-empty model ID when provided.",
options: { agents: { researcher: "Research" }, message: "Research", model: "" },
},
{
error: "agentRouter auto requires a non-empty model ID when provided.",
options: { agents: { researcher: "Research" }, message: "Research", model: {} },
},
{
error: 'agentRouter auto requires a string description for agent "researcher".',
options: { agents: { researcher: 42 }, message: "Research" },