Skip to content
Closed
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/subagent-capability-inheritance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Allow declared subagents to explicitly inherit the immediate parent's live sandbox session and resolved connection definitions while staying isolated by default.
55 changes: 41 additions & 14 deletions docs/subagents.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: "Subagents"
description: "Delegate work to child agents, either a copy of the agent itself or declared specialists with their own sandbox and skills."
description: "Delegate work to child agents, either a copy of the agent itself or declared specialists with isolated or explicitly inherited capabilities."
---

A subagent is a child agent that one agent delegates a focused subtask to. Split work into one to run it in parallel, to give the child a narrower set of tools, or to give a specialist its own identity. There are two kinds, the built-in `agent` tool (a copy of the agent itself) and declared subagents (specialists with their own directory).
Expand Down Expand Up @@ -53,26 +53,53 @@ agent/subagents/researcher/

## The isolation boundary

A declared subagent inherits nothing from the root's authored slots. Discovery treats its directory as its own agent root, so it has only the instructions, tools, connections, skills, sandbox, hooks, and nested subagents authored under `agent/subagents/<id>/`. An absent slot falls back to the framework default, not to the root's version.
A declared subagent is isolated by default. Discovery treats its directory as its own agent root, so it has only the instructions, tools, connections, skills, sandbox, hooks, and nested subagents authored under `agent/subagents/<id>/`. An absent slot falls back to the framework default, not to the root's version, unless the subagent explicitly opts into one of the supported inherited capabilities.

| Slot | Built-in `agent` tool | Declared subagent |
| ------------ | ----------------------------- | -------------------------------------- |
| Instructions | Inherited (copy of the agent) | Own `instructions.{md,ts}`, optional |
| Tools | Inherited | Own `tools/` |
| Connections | Inherited | Own `connections/` |
| Skills | Inherited | Own `skills/` |
| Sandbox | Shared with parent | Own `sandbox/`, else framework default |
| Hooks | Inherited | Own `hooks/` |
| State | Fresh | Fresh |
| Channels | Root-only | Root-only |
| Schedules | Root-only | Root-only |
| Slot | Built-in `agent` tool | Declared subagent |
| ------------ | ----------------------------- | ------------------------------------------------------ |
| Instructions | Inherited (copy of the agent) | Own `instructions.{md,ts}`, optional |
| Tools | Inherited | Own `tools/` |
| Connections | Inherited | Own `connections/`, or explicit inherit |
| Skills | Inherited | Own `skills/` |
| Sandbox | Shared with parent | Own `sandbox/`, framework default, or explicit inherit |
| Hooks | Inherited | Own `hooks/` |
| State | Fresh | Fresh |
| Channels | Root-only | Root-only |
| Schedules | Root-only | Root-only |

For a declared subagent this means duplicating anything the child needs. When two subagents need the same procedure, copy the markdown under each `skills/` directory, or share typed helpers via `lib/`. The sandbox does not inherit from the parent; it falls back to the framework default unless the subagent authors `subagents/<id>/sandbox.ts` or seeds files via `subagents/<id>/sandbox/workspace/`.
For a declared subagent this means duplicating anything the child needs unless the capability is intentionally shared. When two subagents need the same procedure, copy the markdown under each `skills/` directory, or share typed helpers via `lib/`. The sandbox does not inherit from the parent by default; it falls back to the framework default unless the subagent authors `subagents/<id>/sandbox.ts`, seeds files via `subagents/<id>/sandbox/workspace/`, or explicitly inherits the parent's live sandbox session.

The built-in `agent` tool is the exception. Its children share the parent's sandbox and tools because they are copies of the same agent working on the same files.

`defineState` is never shared, for either kind. Each child starts with fresh durable state.

## Explicit inheritance for declared subagents

A declared subagent may opt into sharing selected capabilities from its immediate parent with `inherit`. Only `sandbox` and `connections` are supported today:

```ts title="agent/subagents/reviewer/agent.ts"
import { defineAgent } from "eve";

export default defineAgent({
description: "Review the current checkout and file focused comments.",
model: "anthropic/claude-sonnet-5",
inherit: {
sandbox: true,
connections: true,
},
});
```

`inherit.sandbox: true` runs the child against the parent's live sandbox session, so files already checked out or written by the parent are visible to the child, and child writes are visible to the parent. The child still keeps its own instructions, tools, skills, hooks, durable state, and nested subagent list.

Because inherited sandboxes merge the parent's and child's static seed files into one live sandbox template, static skill names must not collide across that inherited chain. Rename one of the skills before sharing the sandbox.

`inherit.connections: true` reuses the parent's resolved connection definitions. Authorization still goes through the normal connection flow for the current session and principal; eve does not copy credentials into prompts, compiled manifests, durable JSON, or sandbox files. A subagent that inherits connections cannot also declare a connection with the same name, because the effective tool namespace would be ambiguous.

Inheritance is immediate-parent based. If `researcher` inherits root connections and nested `reviewer` inherits from `researcher`, `reviewer` sees `researcher`'s effective connections. If `reviewer` does not opt in, it stays isolated even when its parent inherited something.

A subagent cannot both inherit the parent sandbox and own a sandbox or sandbox workspace seed. Choose one live filesystem boundary per subagent: inherited parent session, authored subagent sandbox, or the framework default.

## What the parent sees

eve lowers every subagent (built-in copy, declared, or [remote](./guides/remote-agents)) into a model-visible tool with the same `{ message, outputSchema? }` shape. The parent packs `message` with everything the child needs, since the child never sees the parent's history. Set `outputSchema` to run the child in task mode, returning structured output as the tool result.
Expand Down
67 changes: 67 additions & 0 deletions packages/eve/src/cli/commands/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,20 @@ export interface ApplicationInfoJson {
model: string | null;
instructions: string | null;
skills: string[];
subagents: {
name: string;
effective: {
connections: {
inherited: boolean;
owned: number;
};
sandbox: "default" | "inherited" | "owned";
};
inherit: {
connections: boolean;
sandbox: boolean;
};
}[];
tools: string[];
channels: { name: string; kind: string | null; method: string | null; urlPath: string | null }[];
messaging: { create: string; continue: string; stream: string };
Expand Down Expand Up @@ -54,6 +68,28 @@ export function buildApplicationInfoJson(inspection: ApplicationInspection): App
model: compiledState?.manifest.config.model.id ?? null,
instructions: compiledState?.manifest.instructions?.logicalPath ?? null,
skills: (compiledState?.manifest.skills ?? []).map((skill) => skill.name),
subagents: (compiledState?.manifest.subagents ?? []).map((subagent) => {
const inheritsConnections = subagent.agent.config.inherit?.connections === true;
const inheritsSandbox = subagent.agent.config.inherit?.sandbox === true;
return {
name: subagent.name,
effective: {
connections: {
inherited: inheritsConnections,
owned: subagent.agent.connections.length,
},
sandbox: inheritsSandbox
? "inherited"
: subagent.agent.sandbox === null && subagent.agent.sandboxWorkspaces.length === 0
? "default"
: "owned",
},
inherit: {
connections: inheritsConnections,
sandbox: inheritsSandbox,
},
};
}),
tools: (compiledState?.manifest.tools ?? []).map((tool) => tool.name),
channels: (compiledState?.manifest.channels ?? []).map((channel) =>
channel.kind === "channel"
Expand Down Expand Up @@ -90,6 +126,19 @@ function formatDiscoverySummary(errors: number, warnings: number): string {
return `${pluralize(errors, "error")}, ${pluralize(warnings, "warning")}`;
}

function formatSubagentCapabilitySummary(
subagent: ApplicationInfoJson["subagents"][number],
): string {
const connectionMode = subagent.effective.connections.inherited
? "connections inherited"
: "connections isolated";
return [
`sandbox ${subagent.effective.sandbox}`,
connectionMode,
pluralize(subagent.effective.connections.owned, "owned connection"),
].join("; ");
}

function resolveCompileTone(status: string): "danger" | "success" | "warning" {
switch (status) {
case "ready":
Expand Down Expand Up @@ -118,6 +167,7 @@ export async function printApplicationInfo(

const compiledState = inspection.compiledState;
const info = inspection.application;
const applicationInfoJson = buildApplicationInfoJson(inspection);
const theme = createCliTheme();
const applicationRows: CliRow[] = [
{
Expand All @@ -136,6 +186,14 @@ export async function printApplicationInfo(
},
];
const instructionsRows: CliRow[] = [];
const subagentRows: CliRow[] = applicationInfoJson.subagents.map((subagent) => ({
label: subagent.name,
tone:
subagent.effective.sandbox === "inherited" || subagent.effective.connections.inherited
? "subagent"
: "muted",
value: formatSubagentCapabilitySummary(subagent),
}));

if (compiledState !== null) {
applicationRows.push(
Expand Down Expand Up @@ -240,6 +298,15 @@ export async function printApplicationInfo(
title: "Instructions",
}),
]),
...(compiledState === null || subagentRows.length === 0
? []
: [
"",
renderCliSection(theme, {
rows: subagentRows,
title: "Subagents",
}),
]),
"",
renderCliSection(theme, {
rows: [
Expand Down
11 changes: 11 additions & 0 deletions packages/eve/src/client/agent-info-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,17 @@ const schedule = entry.extend({
const subagent = entry.extend({
description: z.string(),
entryPath: z.string(),
effective: z.object({
connections: z.object({
inherited: z.boolean(),
owned: z.number(),
}),
sandbox: z.enum(["default", "inherited", "owned"]),
}),
inherit: z.object({
connections: z.boolean(),
sandbox: z.boolean(),
}),
nodeId: z.string(),
rootPath: z.string(),
summary: z.object({
Expand Down
22 changes: 22 additions & 0 deletions packages/eve/src/compiler/manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,4 +112,26 @@ describe("compiledAgentManifestSchema", () => {
expect(parsed.success).toBe(true);
expect(manifest.config.experimental?.workflow).toEqual({ world: "@acme/eve-world" });
});

it("preserves explicit capability inheritance configuration", () => {
const manifest = createCompiledAgentManifest({
agentRoot: "/app/agent",
appRoot: "/app",
config: {
inherit: {
connections: true,
sandbox: true,
},
model: { id: "openai/gpt-5.5", routing: classifyModelRouting("openai/gpt-5.5") },
name: "app",
},
});

const parsed = compiledAgentManifestSchema.parse(manifest);

expect(parsed.config.inherit).toEqual({
connections: true,
sandbox: true,
});
});
});
19 changes: 19 additions & 0 deletions packages/eve/src/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type {
InternalAgentModelDefinition,
InternalAgentCompactionDefinition,
AgentBuildDefinition,
AgentInheritanceDefinition,
ModelRouting,
} from "#shared/agent-definition.js";
import type { InternalToolDefinition } from "#shared/tool-definition.js";
Expand Down Expand Up @@ -121,6 +122,8 @@ type CompiledAgentCompactionDefinition = Omit<InternalAgentCompactionDefinition,
model?: CompiledRuntimeModelReference;
};

type CompiledAgentInheritanceDefinition = AgentInheritanceDefinition;

/**
* Normalized additive agent configuration preserved in the compiled manifest.
*/
Expand Down Expand Up @@ -401,6 +404,13 @@ const compiledAgentLimitsDefinitionSchema = z
})
.strict();

const compiledAgentInheritanceDefinitionSchema: z.ZodType<CompiledAgentInheritanceDefinition> = z
.object({
connections: z.boolean().optional(),
sandbox: z.boolean().optional(),
})
.strict();

const compiledAgentConfigSchema: z.ZodType<CompiledAgentDefinition> = z
.object({
build: compiledAgentBuildDefinitionSchema.optional(),
Expand All @@ -413,6 +423,7 @@ const compiledAgentConfigSchema: z.ZodType<CompiledAgentDefinition> = z
})
.strict()
.optional(),
inherit: compiledAgentInheritanceDefinitionSchema.optional(),
model: compiledRuntimeModelReferenceSchema,
name: z.string(),
outputSchema: jsonObjectSchema.optional(),
Expand Down Expand Up @@ -804,6 +815,13 @@ export function createCompiledAgentNodeManifest(input: {
world: input.config.experimental.workflow.world,
},
},
inherit:
input.config.inherit === undefined
? undefined
: {
connections: input.config.inherit.connections,
sandbox: input.config.inherit.sandbox,
},
model: cloneCompiledRuntimeModelReference(input.config.model),
name: input.config.name,
outputSchema: input.config.outputSchema,
Expand Down Expand Up @@ -916,6 +934,7 @@ export function createCompiledAgentManifest(input: {
readonly instructions?: CompiledInstructionsDefinition;
readonly tools?: readonly CompiledToolDefinition[];
readonly extensionMounts?: readonly CompiledExtensionMount[];
readonly workspaceResourceRoot?: CompiledWorkspaceResourceRoot;
}): CompiledAgentManifest {
return {
...createCompiledAgentNodeManifest(input),
Expand Down
26 changes: 26 additions & 0 deletions packages/eve/src/compiler/normalize-agent-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,32 @@ describe("compileAgentConfig", () => {
sourceKind: "module",
});
});

it("preserves explicit capability inheritance config", async () => {
mocks.loadModuleBackedDefinition.mockResolvedValue({
inherit: { connections: true, sandbox: true },
model: "openai/gpt-5.5",
});

const manifest = createAgentSourceManifest({
agentId: "research",
agentRoot: "/app/agent/subagents/research",
appRoot: "/app",
configModule: createModuleSourceRef({
logicalPath: "agent.ts",
sourceId: "subagent-config",
}),
});

const compiled = await compileAgentConfig(manifest, {
modelCatalog: createModelCatalog(),
});

expect(compiled.inherit).toEqual({
connections: true,
sandbox: true,
});
});
});

function createModelCatalog(): ManifestCompileContext["modelCatalog"] {
Expand Down
8 changes: 8 additions & 0 deletions packages/eve/src/compiler/normalize-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ export async function compileAgentConfig(
description?: string;
dynamicModel?: CompiledAgentDefinition["dynamicModel"];
experimental?: CompiledAgentDefinition["experimental"];
inherit?: CompiledAgentDefinition["inherit"];
model: CompiledRuntimeModelReference;
name: string;
outputSchema?: JsonObject;
Expand Down Expand Up @@ -108,6 +109,13 @@ export async function compileAgentConfig(
compiledConfig.experimental = experimental;
}

if (definition.inherit !== undefined) {
compiledConfig.inherit = {
connections: definition.inherit.connections,
sandbox: definition.inherit.sandbox,
};
}

if (definition.build !== undefined) {
compiledConfig.build = {
externalDependencies:
Expand Down
Loading
Loading