Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-subagents-rest.md
Original file line number Diff line number Diff line change
@@ -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.
99 changes: 50 additions & 49 deletions docs/reference/typescript-api.md

Large diffs are not rendered by default.

18 changes: 18 additions & 0 deletions docs/subagents.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions packages/eve/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<id>/agent.ts` |
| `defineRemoteAgent(...)`, `disableSubagent()` | `eve` | `subagents/<id>.ts` or `<id>/agent.ts` |
| `defineInstructions(...)` | `eve/instructions` | `instructions.ts` (or `instructions.md`) |
| `defineTool(...)`, `defineBashTool(...)`, `defineReadFileTool(...)`, `defineWriteFileTool(...)`, `disableTool(...)` | `eve/tools` | `tools/<name>.ts` |
| `defineSkill(...)`, `getSkill(...)` | `eve/skills` | `skills/<name>.ts` (or `skills/<name>.md`) |
Expand Down
36 changes: 36 additions & 0 deletions packages/eve/src/compiler/normalize-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down Expand Up @@ -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(
Expand Down
12 changes: 12 additions & 0 deletions packages/eve/src/compiler/normalize-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,6 +79,10 @@ export async function compileSubagentGraph(input: {
continue;
}

if (compiledSubagent.kind === "disabled") {
continue;
}

compiledNodes.push(compiledSubagent.node, ...compiledSubagent.descendants.nodes);
compiledEdges.push(
{
Expand Down Expand Up @@ -116,6 +121,9 @@ async function compileSubagentDefinition(input: {
readonly kind: "remote";
readonly node: CompiledRemoteAgentNode;
}
| {
readonly kind: "disabled";
}
> {
const configModule = input.source.manifest.configModule;

Expand All @@ -132,6 +140,10 @@ async function compileSubagentDefinition(input: {
source: configModule,
});

if (isDisabledSubagentSentinel(definition)) {
return { kind: "disabled" };
}

if (readAgentDefinitionKind(definition) === "remote") {
return {
kind: "remote",
Expand Down
13 changes: 13 additions & 0 deletions packages/eve/src/public/definitions/subagent.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
35 changes: 35 additions & 0 deletions packages/eve/src/public/definitions/subagent.ts
Original file line number Diff line number Diff line change
@@ -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
);
}
5 changes: 5 additions & 0 deletions packages/eve/src/public/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,8 @@ export {
type RemoteAgentUrl,
defineRemoteAgent,
} from "#public/definitions/remote-agent.js";
export {
type DisabledSubagentSentinel,
disableSubagent,
isDisabledSubagentSentinel,
} from "#public/definitions/subagent.js";
Loading