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/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.
57 changes: 43 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 root-agent copies or declared specialists with their own tools and sandbox."
description: "Delegate work to root-agent copies or declared specialists with isolated or explicitly inherited capabilities."
---

eve supports two ways to delegate work: the root-only built-in `agent` tool, which runs a fresh copy of the root agent, and declared subagents, which are specialists with their own directories. Use a subagent to run independent work in parallel, narrow the available tools, or give a task to a specialist.
Expand Down Expand Up @@ -63,26 +63,55 @@ 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 | Root built-in `agent` tool | Declared subagent |
| ------------ | ----------------------------- | -------------------------------------- |
| Instructions | Inherited (copy of the agent) | Own `instructions.{md,ts}`, optional |
| Tools | Inherited except root-only | 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 | Root built-in `agent` tool | Declared subagent |
| ------------ | ----------------------------- | ------------------------------------------------------ |
| Instructions | Inherited (copy of the agent) | Own `instructions.{md,ts}`, optional |
| Tools | Inherited except root-only | 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 root built-in `agent` tool is the exception. Its children share the root'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.

Sharing sandbox or connections expands the child's trust boundary to that parent capability. Treat inherited reviewers as able to read whatever is already in the parent sandbox and to invoke the parent's connections under the same session principal. Prefer isolation when the specialist should not see that workspace or those credentials, and keep concurrent parent/child writes non-overlapping when they share a live sandbox.

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 visible to the current agent (the root 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { defineDynamic, defineInstructions } from "#public/instructions/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineInstructions({
markdown: "You are a retained epoch-1 dynamic instructions prompt.",
}),
},
});
11 changes: 11 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicSkill/v1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineDynamic, defineSkill } from "#public/skills/index.js";

export default defineDynamic({
events: {
"session.started": () =>
defineSkill({
description: "Share a retained epoch-1 dynamic skill.",
markdown: "# Playbook\n\nFollow the retained skill contract.\n",
}),
},
});
26 changes: 26 additions & 0 deletions packages/eve/extension-contracts/compatibility/dynamicTool/v4.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { defineDynamic, defineTool } from "#public/tools/index.js";

export default defineDynamic({
events: {
"session.started": (_event, ctx) => ({
summarize_report: defineTool({
description: "Summarize a report for the model",
inputSchema: { type: "object", properties: {} },
async execute(_input, toolContext) {
return {
internal: "details",
resolverSessionId: ctx.session.id,
sessionId: toolContext.session.id,
summary: "Report generated",
};
},
toModelOutput(output) {
return {
type: "text",
value: (output as { summary: string }).summary,
};
},
}),
}),
},
});
12 changes: 12 additions & 0 deletions packages/eve/extension-contracts/compatibility/hook/v2.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { defineHook } from "#public/hooks/index.js";

export default defineHook({
events: {
"session.started"(_event, ctx) {
console.info("session started", {
agentName: ctx.agent.name,
sessionId: ctx.session.id,
});
},
},
});
19 changes: 19 additions & 0 deletions packages/eve/extension-contracts/compatibility/tool/v3.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { defineTool } from "#public/tools/index.js";

export default defineTool({
description: "Summarize a report for the model",
inputSchema: { type: "object", properties: {} },
async execute(_input, ctx) {
return {
internal: "details",
sessionId: ctx.session.id,
summary: "Report generated",
};
},
toModelOutput(output) {
return {
type: "text",
value: (output as { summary: string }).summary,
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicInstructions",
"epoch": 2,
"sha256": "62a931451a6170ad9b38de01b10d48f49d6f75973313e68dda6cb63ca9ac62a5",
"exports": ["defineDynamic"]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicSkill/v2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicSkill",
"epoch": 2,
"sha256": "62a931451a6170ad9b38de01b10d48f49d6f75973313e68dda6cb63ca9ac62a5",
"exports": ["defineDynamic"]
}
13 changes: 13 additions & 0 deletions packages/eve/extension-contracts/reports/dynamicTool/v5.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"kind": "eve-extension-capability-contract",
"capability": "dynamicTool",
"epoch": 5,
"sha256": "8950812b35538f149d102df2364abfacdf50107aca234005b8b71874d38a3aed",
"exports": [
"DynamicToolEntry",
"DynamicToolEvents",
"DynamicToolResult",
"DynamicToolSet",
"defineDynamic"
]
}
7 changes: 7 additions & 0 deletions packages/eve/extension-contracts/reports/hook/v3.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"kind": "eve-extension-capability-contract",
"capability": "hook",
"epoch": 3,
"sha256": "41c72e151a7df9afefeb506a3e5ab9ec1160187ecac749556bf06e9e2e33c45f",
"exports": ["defineHook"]
}
21 changes: 21 additions & 0 deletions packages/eve/extension-contracts/reports/tool/v4.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"kind": "eve-extension-capability-contract",
"capability": "tool",
"epoch": 4,
"sha256": "831ffd895eae757f952882d11e547c01e8d5ded44f24f7ebceca59f083995586",
"exports": [
"defineBashTool",
"defineGlobTool",
"defineGrepTool",
"defineReadFileTool",
"defineTool",
"defineWriteFileTool",
"disableTool",
"experimental_workflow",
"isDisabledToolSentinel",
"isExperimentalWorkflowToolDefinition",
"toolOutput",
"toolOutputPart",
"toolResultFrom"
]
}
69 changes: 59 additions & 10 deletions packages/eve/src/cli/commands/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,18 +38,35 @@ function makeSchedule(name: string): CompiledScheduleDefinition {
};
}

function makeSubagent(name: string): CompiledSubagentNode {
function makeSubagent(
name: string,
options: {
inherit?: { connections?: boolean; sandbox?: boolean };
} = {},
): CompiledSubagentNode {
const config: {
inherit?: { connections?: boolean; sandbox?: boolean };
model: {
id: string;
routing: { kind: "gateway"; target: string };
};
name: string;
} = {
model: {
id: "anthropic/claude-sonnet-5",
routing: { kind: "gateway", target: "anthropic" },
},
name,
};
if (options.inherit !== undefined) {
config.inherit = options.inherit;
}

return {
agent: createCompiledAgentNodeManifest({
agentRoot: `${AGENT_ROOT}/subagents/${name}`,
appRoot: APP_ROOT,
config: {
model: {
id: "anthropic/claude-sonnet-5",
routing: { kind: "gateway", target: "anthropic" },
},
name,
},
config,
}),
description: `${name} subagent description`,
entryPath: `subagents/${name}/agent.ts`,
Expand Down Expand Up @@ -174,12 +191,44 @@ describe("buildApplicationInfoJson", () => {
application: getApplicationInfo(APP_ROOT),
compiledState: makeCompiledState({
schedules: [makeSchedule("morning-digest"), makeSchedule("weekly-report")],
subagents: [makeSubagent("research")],
subagents: [
makeSubagent("research"),
makeSubagent("reviewer", { inherit: { connections: true, sandbox: true } }),
],
}),
messaging: MESSAGING,
});

expect(json.subagents).toEqual(["research"]);
expect(json.subagents).toEqual([
{
name: "research",
effective: {
connections: {
inherited: false,
owned: 0,
},
sandbox: "default",
},
inherit: {
connections: false,
sandbox: false,
},
},
{
name: "reviewer",
effective: {
connections: {
inherited: true,
owned: 0,
},
sandbox: "inherited",
},
inherit: {
connections: true,
sandbox: true,
},
},
]);
expect(json.schedules).toEqual(["morning-digest", "weekly-report"]);
});

Expand Down
Loading