From 2fe7ea50cf2dfe9920744fb9d8ba557e602935b8 Mon Sep 17 00:00:00 2001 From: Murat Sutunc Date: Sat, 25 Jul 2026 07:46:40 -0400 Subject: [PATCH] feat(eve): add disableSubagent helper Signed-off-by: Murat Sutunc --- .changeset/tidy-subagents-rest.md | 5 + docs/reference/typescript-api.md | 99 ++++++++++--------- docs/subagents.mdx | 18 ++++ packages/eve/README.md | 1 + .../src/compiler/normalize-manifest.test.ts | 36 +++++++ .../eve/src/compiler/normalize-subagent.ts | 12 +++ .../src/public/definitions/subagent.test.ts | 13 +++ .../eve/src/public/definitions/subagent.ts | 35 +++++++ packages/eve/src/public/index.ts | 5 + 9 files changed, 175 insertions(+), 49 deletions(-) create mode 100644 .changeset/tidy-subagents-rest.md create mode 100644 packages/eve/src/public/definitions/subagent.test.ts create mode 100644 packages/eve/src/public/definitions/subagent.ts diff --git a/.changeset/tidy-subagents-rest.md b/.changeset/tidy-subagents-rest.md new file mode 100644 index 000000000..0e5147bee --- /dev/null +++ b/.changeset/tidy-subagents-rest.md @@ -0,0 +1,5 @@ +--- +"eve": patch +--- + +Add `disableSubagent()` so an authored local or remote subagent can be omitted from the compiled agent graph and model-visible capability surface. diff --git a/docs/reference/typescript-api.md b/docs/reference/typescript-api.md index 95b3c40e6..46b9f3398 100644 --- a/docs/reference/typescript-api.md +++ b/docs/reference/typescript-api.md @@ -30,29 +30,30 @@ export default defineTool({ ## The define\* helpers -| Helper | Import from | Authored at | Guide | -| ----------------------------------------------------- | --------------------------------------------- | ------------------------------------ | ------------------------------------------------------ | -| `defineAgent` | `eve` | `agent/agent.ts` | [agent.ts](../agent-config) | -| `defineTool` | `eve/tools` | `agent/tools/.ts` | [Tools](../tools) | -| `defineDynamic` | `eve/tools`, `eve/skills`, `eve/instructions` | `agent/{tools,skills,instructions}/` | [Dynamic capabilities](../guides/dynamic-capabilities) | -| `defineMcpClientConnection` | `eve/connections` | `agent/connections/.ts` | [MCP connections](../connections/mcp) | -| `defineOpenAPIConnection` | `eve/connections` | `agent/connections/.ts` | [OpenAPI connections](../connections/openapi) | -| `defineChannel` | `eve/channels` | `agent/channels/.ts` | [Custom channels](../channels/custom) | -| `eveChannel`, `slackChannel`, and the other platforms | `eve/channels/` | `agent/channels/.ts` | [Channels](../channels/overview) | -| `defineSkill` | `eve/skills` | `agent/skills/.ts` | [Skills](../skills) | -| `defineInstructions` | `eve/instructions` | `agent/instructions.ts` | [Instructions](../instructions) | -| `defineHook` | `eve/hooks` | `agent/hooks/.ts` | [Hooks](../guides/hooks) | -| `defineSchedule` | `eve/schedules` | `agent/schedules/.ts` | [Schedules](../schedules) | -| `defineState` | `eve/context` | tools, hooks, lifecycle | [Session context](../guides/session-context) | -| `defineSandbox` | `eve/sandbox` | `agent/sandbox.ts` | [Sandbox](../sandbox) | -| `defineInstrumentation` | `eve/instrumentation` | `agent/instrumentation.ts` | [instrumentation.ts](../guides/instrumentation) | -| `defineRemoteAgent` | `eve` | `agent/subagents//agent.ts` | [Remote agents](../guides/remote-agents) | -| `defineEval` | `eve/evals` | `evals/*.eval.ts` | [Evals](../evals/overview) | -| `defineEvalConfig` | `eve/evals` | `evals/evals.config.ts` | [Evals](../evals/overview) | -| `mockModel` | `eve/evals` | Deterministic fixture agent models | [Evals](../evals/overview) | -| `useEveAgent` | `eve/react`, `eve/vue`, `eve/svelte` | frontend | [Frontend](../guides/frontend/overview) | - -A few non-`define*` helpers round out the set: `disableTool` and `experimental_workflow` from `eve/tools` (see [Default harness](../concepts/default-harness)), the route verbs `GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`WS` from `eve/channels`, the approval policies `always`/`once`/`never` from `eve/tools/approval`, and the channel auth helpers `localDev`/`vercelOidc`/`placeholderAuth` from `eve/channels/auth`. To wrap a built-in tool, import its default value from `eve/tools/defaults` (`bash`, `readFile`, `writeFile`, `glob`, `grep`, `webFetch`, `webSearch`, `todo`, `loadSkill`). `AgentReasoningDefinition` is exported from `eve` for the top-level `defineAgent({ reasoning })` setting. `AgentLimitsDefinition` is exported for `defineAgent({ limits })`. `AgentWorkflowDefinition` and `AgentWorkflowWorldDefinition` are exported from `eve` for the `defineAgent({ experimental: { workflow } })` config shape. `ExperimentalWorkflowToolInput` is exported from `eve/tools` for the `experimental_workflow(...)` config shape. +| Helper | Import from | Authored at | Guide | +| ----------------------------------------------------- | --------------------------------------------- | --------------------------------------- | ------------------------------------------------------ | +| `defineAgent` | `eve` | `agent/agent.ts` | [agent.ts](../agent-config) | +| `defineTool` | `eve/tools` | `agent/tools/.ts` | [Tools](../tools) | +| `defineDynamic` | `eve/tools`, `eve/skills`, `eve/instructions` | `agent/{tools,skills,instructions}/` | [Dynamic capabilities](../guides/dynamic-capabilities) | +| `defineMcpClientConnection` | `eve/connections` | `agent/connections/.ts` | [MCP connections](../connections/mcp) | +| `defineOpenAPIConnection` | `eve/connections` | `agent/connections/.ts` | [OpenAPI connections](../connections/openapi) | +| `defineChannel` | `eve/channels` | `agent/channels/.ts` | [Custom channels](../channels/custom) | +| `eveChannel`, `slackChannel`, and the other platforms | `eve/channels/` | `agent/channels/.ts` | [Channels](../channels/overview) | +| `defineSkill` | `eve/skills` | `agent/skills/.ts` | [Skills](../skills) | +| `defineInstructions` | `eve/instructions` | `agent/instructions.ts` | [Instructions](../instructions) | +| `defineHook` | `eve/hooks` | `agent/hooks/.ts` | [Hooks](../guides/hooks) | +| `defineSchedule` | `eve/schedules` | `agent/schedules/.ts` | [Schedules](../schedules) | +| `defineState` | `eve/context` | tools, hooks, lifecycle | [Session context](../guides/session-context) | +| `defineSandbox` | `eve/sandbox` | `agent/sandbox.ts` | [Sandbox](../sandbox) | +| `defineInstrumentation` | `eve/instrumentation` | `agent/instrumentation.ts` | [instrumentation.ts](../guides/instrumentation) | +| `defineRemoteAgent` | `eve` | `agent/subagents//agent.ts` | [Remote agents](../guides/remote-agents) | +| `disableSubagent` | `eve` | `agent/subagents/.ts` or `agent.ts` | [Subagents](../subagents) | +| `defineEval` | `eve/evals` | `evals/*.eval.ts` | [Evals](../evals/overview) | +| `defineEvalConfig` | `eve/evals` | `evals/evals.config.ts` | [Evals](../evals/overview) | +| `mockModel` | `eve/evals` | Deterministic fixture agent models | [Evals](../evals/overview) | +| `useEveAgent` | `eve/react`, `eve/vue`, `eve/svelte` | frontend | [Frontend](../guides/frontend/overview) | + +A few non-`define*` helpers round out the set: `disableSubagent` from `eve`, `disableTool` and `experimental_workflow` from `eve/tools` (see [Default harness](../concepts/default-harness)), the route verbs `GET`/`POST`/`PUT`/`PATCH`/`DELETE`/`WS` from `eve/channels`, the approval policies `always`/`once`/`never` from `eve/tools/approval`, and the channel auth helpers `localDev`/`vercelOidc`/`placeholderAuth` from `eve/channels/auth`. To wrap a built-in tool, import its default value from `eve/tools/defaults` (`bash`, `readFile`, `writeFile`, `glob`, `grep`, `webFetch`, `webSearch`, `todo`, `loadSkill`). `AgentReasoningDefinition` is exported from `eve` for the top-level `defineAgent({ reasoning })` setting. `AgentLimitsDefinition` is exported for `defineAgent({ limits })`. `AgentWorkflowDefinition` and `AgentWorkflowWorldDefinition` are exported from `eve` for the `defineAgent({ experimental: { workflow } })` config shape. `ExperimentalWorkflowToolInput` is exported from `eve/tools` for the `experimental_workflow(...)` config shape. ## Runtime context (`ctx`) @@ -68,32 +69,32 @@ A few non-`define*` helpers round out the set: `disableTool` and `experimental_w ## Imports at a glance -| Import | Holds | -| ----------------------------------------------------------- | --------------------------------------------------------------------- | -| `eve` | `defineAgent`, `defineRemoteAgent`, agent config types | -| `eve/tools` | `defineTool`, `defineDynamic`, `disableTool`, `experimental_workflow` | -| `eve/tools/defaults` | the built-in tools as plain values | -| `eve/tools/approval` | `always`, `once`, `never` | -| `eve/connections` | `defineMcpClientConnection`, `defineOpenAPIConnection` | -| `eve/channels` | `defineChannel`, route verbs | -| `eve/channels/eve` | `eveChannel` | -| `eve/channels/auth` | `localDev`, `vercelOidc`, `placeholderAuth` | -| `eve/channels/{slack,discord,teams,telegram,twilio,github}` | platform channel factories | -| `eve/hooks` | `defineHook` | -| `eve/schedules` | `defineSchedule` | -| `eve/skills` | `defineSkill`, `defineDynamic` | -| `eve/instructions` | `defineInstructions`, `defineDynamic` | -| `eve/context` | `defineState`, session and state types | -| `eve/sandbox` | `defineSandbox`, backends | -| `eve/instrumentation` | `defineInstrumentation`, `isChannel` | -| `eve/models/openai` | `experimental_chatgpt` | -| `eve/evals` | `defineEval`, `defineEvalConfig`, `mockModel`, eval types | -| `eve/evals/expect` | `includes`, `equals`, `matches`, `similarity` | -| `eve/evals/reporters` | `Braintrust`, `JUnit`, `EvalReporter` | -| `eve/evals/loaders` | `loadJson`, `loadYaml` | -| `eve/react`, `eve/vue`, `eve/svelte` | `useEveAgent` | -| `eve/next`, `eve/nuxt`, `eve/sveltekit` | framework bundler plugins | -| [`eve/client`](../guides/client/overview) | `Client`, `ClientSession` | +| Import | Holds | +| ----------------------------------------------------------- | ------------------------------------------------------------------------- | +| `eve` | `defineAgent`, `defineRemoteAgent`, `disableSubagent`, agent config types | +| `eve/tools` | `defineTool`, `defineDynamic`, `disableTool`, `experimental_workflow` | +| `eve/tools/defaults` | the built-in tools as plain values | +| `eve/tools/approval` | `always`, `once`, `never` | +| `eve/connections` | `defineMcpClientConnection`, `defineOpenAPIConnection` | +| `eve/channels` | `defineChannel`, route verbs | +| `eve/channels/eve` | `eveChannel` | +| `eve/channels/auth` | `localDev`, `vercelOidc`, `placeholderAuth` | +| `eve/channels/{slack,discord,teams,telegram,twilio,github}` | platform channel factories | +| `eve/hooks` | `defineHook` | +| `eve/schedules` | `defineSchedule` | +| `eve/skills` | `defineSkill`, `defineDynamic` | +| `eve/instructions` | `defineInstructions`, `defineDynamic` | +| `eve/context` | `defineState`, session and state types | +| `eve/sandbox` | `defineSandbox`, backends | +| `eve/instrumentation` | `defineInstrumentation`, `isChannel` | +| `eve/models/openai` | `experimental_chatgpt` | +| `eve/evals` | `defineEval`, `defineEvalConfig`, `mockModel`, eval types | +| `eve/evals/expect` | `includes`, `equals`, `matches`, `similarity` | +| `eve/evals/reporters` | `Braintrust`, `JUnit`, `EvalReporter` | +| `eve/evals/loaders` | `loadJson`, `loadYaml` | +| `eve/react`, `eve/vue`, `eve/svelte` | `useEveAgent` | +| `eve/next`, `eve/nuxt`, `eve/sveltekit` | framework bundler plugins | +| [`eve/client`](../guides/client/overview) | `Client`, `ClientSession` | Exported types ship from the same entrypoint as the helper they describe (for example `ToolDefinition` and `ToolContext` from `eve/tools`). For the exhaustive list, read `packages/eve/src/public/index.ts`. diff --git a/docs/subagents.mdx b/docs/subagents.mdx index 883998953..33a981b36 100644 --- a/docs/subagents.mdx +++ b/docs/subagents.mdx @@ -47,6 +47,24 @@ export default defineAgent({ `description` is required. The parent reads it to decide whether to delegate, so the compiler rejects any subagent whose `agent.ts` leaves it out. +To omit an unavailable or unnecessary subagent, export `disableSubagent()` +from its config module. This works for both single-file and directory +subagents, and is useful for environment-specific configuration: + +```ts title="agent/subagents/researcher.ts" +import { defineRemoteAgent, disableSubagent } from "eve"; + +export default process.env.RESEARCHER_URL + ? defineRemoteAgent({ + description: "Investigate ambiguous questions before the parent agent responds.", + url: process.env.RESEARCHER_URL, + }) + : disableSubagent(); +``` + +The disabled subagent is omitted from the compiled agent graph and does not +appear in the parent agent's capability surface. + Minimum files: ```text diff --git a/packages/eve/README.md b/packages/eve/README.md index 97ff903b4..6bcbf8e07 100644 --- a/packages/eve/README.md +++ b/packages/eve/README.md @@ -58,6 +58,7 @@ Every authored directory has a typed helper. Import each from the matching subpa | Helper | Subpath | Authored Location | | ------------------------------------------------------------------------------------------------------------------- | ------------------------------------- | ------------------------------------------------ | | `defineAgent(...)` | `eve` | `agent.ts`, `subagents//agent.ts` | +| `defineRemoteAgent(...)`, `disableSubagent()` | `eve` | `subagents/.ts` or `/agent.ts` | | `defineInstructions(...)` | `eve/instructions` | `instructions.ts` (or `instructions.md`) | | `defineTool(...)`, `defineBashTool(...)`, `defineReadFileTool(...)`, `defineWriteFileTool(...)`, `disableTool(...)` | `eve/tools` | `tools/.ts` | | `defineSkill(...)`, `getSkill(...)` | `eve/skills` | `skills/.ts` (or `skills/.md`) | diff --git a/packages/eve/src/compiler/normalize-manifest.test.ts b/packages/eve/src/compiler/normalize-manifest.test.ts index aa2586e92..aa8bed69c 100644 --- a/packages/eve/src/compiler/normalize-manifest.test.ts +++ b/packages/eve/src/compiler/normalize-manifest.test.ts @@ -10,6 +10,7 @@ import { classifyModelRouting } from "#internal/classify-model-routing.js"; import type { CompiledAgentDefinition } from "#compiler/manifest.js"; import { compileAgentManifest } from "#compiler/normalize-manifest.js"; import { experimental_workflow } from "#public/definitions/tool.js"; +import { disableSubagent } from "#public/definitions/subagent.js"; const mocks = vi.hoisted(() => ({ compileAgentConfig: vi.fn(), @@ -93,6 +94,41 @@ describe("compileAgentManifest", () => { expect(compiled.workflowTool).toEqual({ maxSubagents: 6 }); }); + + it("omits a subagent that exports disableSubagent()", async () => { + const disabledManifest = createAgentSourceManifest({ + agentId: "optional", + agentRoot: "/app/agent", + appRoot: "/app", + configModule: createModuleSourceRef({ + logicalPath: "subagents/optional.ts", + }), + }); + const manifest = createAgentSourceManifest({ + agentId: "root", + agentRoot: "/app/agent", + appRoot: "/app", + subagents: [ + createLocalSubagentSourceRef({ + entryPath: "/app/agent/subagents/optional.ts", + logicalPath: "subagents/optional.ts", + manifest: disabledManifest, + rootPath: "/app/agent", + subagentId: "optional", + }), + ], + }); + + mocks.compileAgentConfig.mockResolvedValue(createConfig({ name: "root" })); + mocks.loadModuleBackedDefinition.mockResolvedValue(disableSubagent()); + + const compiled = await compileAgentManifest(manifest); + + expect(compiled.remoteAgents).toEqual([]); + expect(compiled.subagentEdges).toEqual([]); + expect(compiled.subagents).toEqual([]); + expect(mocks.compileAgentConfig).toHaveBeenCalledOnce(); + }); }); function createConfig( diff --git a/packages/eve/src/compiler/normalize-subagent.ts b/packages/eve/src/compiler/normalize-subagent.ts index e48211057..ae02c49ac 100644 --- a/packages/eve/src/compiler/normalize-subagent.ts +++ b/packages/eve/src/compiler/normalize-subagent.ts @@ -23,6 +23,7 @@ import { import { EVE_CREATE_SESSION_ROUTE_PATH } from "#protocol/routes.js"; import { serializeOutputSchema, type ToolSchemaSource } from "#shared/tool-schema.js"; import type { JsonObject } from "#shared/json.js"; +import { isDisabledSubagentSentinel } from "#public/definitions/subagent.js"; /** * Callback the subagent compiler uses to recurse into the per-node @@ -78,6 +79,10 @@ export async function compileSubagentGraph(input: { continue; } + if (compiledSubagent.kind === "disabled") { + continue; + } + compiledNodes.push(compiledSubagent.node, ...compiledSubagent.descendants.nodes); compiledEdges.push( { @@ -116,6 +121,9 @@ async function compileSubagentDefinition(input: { readonly kind: "remote"; readonly node: CompiledRemoteAgentNode; } + | { + readonly kind: "disabled"; + } > { const configModule = input.source.manifest.configModule; @@ -132,6 +140,10 @@ async function compileSubagentDefinition(input: { source: configModule, }); + if (isDisabledSubagentSentinel(definition)) { + return { kind: "disabled" }; + } + if (readAgentDefinitionKind(definition) === "remote") { return { kind: "remote", diff --git a/packages/eve/src/public/definitions/subagent.test.ts b/packages/eve/src/public/definitions/subagent.test.ts new file mode 100644 index 000000000..2ad839277 --- /dev/null +++ b/packages/eve/src/public/definitions/subagent.test.ts @@ -0,0 +1,13 @@ +import { describe, expect, it } from "vitest"; + +import { disableSubagent, isDisabledSubagentSentinel } from "#public/definitions/subagent.js"; + +describe("disableSubagent", () => { + it("returns a recognizable disabled subagent sentinel", () => { + const sentinel = disableSubagent(); + + expect(sentinel).toEqual({ kind: "eve:disabled-subagent" }); + expect(isDisabledSubagentSentinel(sentinel)).toBe(true); + expect(isDisabledSubagentSentinel({ kind: "remote" })).toBe(false); + }); +}); diff --git a/packages/eve/src/public/definitions/subagent.ts b/packages/eve/src/public/definitions/subagent.ts new file mode 100644 index 000000000..147828805 --- /dev/null +++ b/packages/eve/src/public/definitions/subagent.ts @@ -0,0 +1,35 @@ +/** + * Marker discriminator written into every {@link DisabledSubagentSentinel}. + */ +const DISABLED_SUBAGENT_SENTINEL_KIND = "eve:disabled-subagent"; + +/** + * Marker value returned from {@link disableSubagent}. Export this as the + * default export of a file in `agent/subagents/` to omit the subagent whose + * name matches the file or directory slug. + */ +export interface DisabledSubagentSentinel { + readonly kind: typeof DISABLED_SUBAGENT_SENTINEL_KIND; +} + +/** + * Returns a sentinel that disables the subagent whose name matches the + * containing file or directory slug. + */ +export function disableSubagent(): DisabledSubagentSentinel { + return { + kind: DISABLED_SUBAGENT_SENTINEL_KIND, + }; +} + +/** + * Type guard: returns whether `value` is a {@link DisabledSubagentSentinel} + * produced by {@link disableSubagent}. + */ +export function isDisabledSubagentSentinel(value: unknown): value is DisabledSubagentSentinel { + return ( + typeof value === "object" && + value !== null && + (value as { kind?: unknown }).kind === DISABLED_SUBAGENT_SENTINEL_KIND + ); +} diff --git a/packages/eve/src/public/index.ts b/packages/eve/src/public/index.ts index 89c12df7f..4bfd96e0a 100644 --- a/packages/eve/src/public/index.ts +++ b/packages/eve/src/public/index.ts @@ -22,3 +22,8 @@ export { type RemoteAgentUrl, defineRemoteAgent, } from "#public/definitions/remote-agent.js"; +export { + type DisabledSubagentSentinel, + disableSubagent, + isDisabledSubagentSentinel, +} from "#public/definitions/subagent.js";