Capabilities
A capability is something an <Agent> can do: call a tool, today. June declares capabilities in JSX, as children of the agent they belong to, so the shape of an agent reads straight off its source:
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Step, Tool } from "june";
import { z } from "zod";
const readFile = tool({
description: "Read a file",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => Bun.file(path).text(),
});
const writeFile = tool({
description: "Write a file",
inputSchema: z.object({ path: z.string(), text: z.string() }),
execute: async ({ path, text }) => Bun.write(path, text).then(() => "ok"),
});
const search = tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `results for ${query}`,
});
const deploy = tool({
description: "Deploy to an environment",
inputSchema: z.object({ env: z.enum(["staging", "prod"]) }),
needsApproval: true,
execute: async ({ env }) => `Deployed to ${env}`,
});
const model = openai("gpt-5-mini");
export default function Release() {
return (
<Agent model={model}>
<Tool name="readFile" value={readFile} />
<Tool name="deploy" value={deploy} />
<Step id="release" prompt="Check the changelog, then deploy to staging" />
</Agent>
);
}
Capabilities end in ordinary AI SDK tools. The agent hands its steps one ToolSet, and approvals, traces and the model loop work as they do for any tool. June doesn't add a tool type or a plugin system: composition decides who sees what.
A few rules explain all of it:
- JSX structure defines scope. What an agent can do is what is declared in its own children.
- Agents encapsulate. A nested
<Agent>has its own capabilities and doesn't see its parent's. - Capabilities cross only when named.
<Inherit>is how a tool goes into a nested agent, and a composite tool or callable agent keeps everything it uses to itself. - Wrappers transform what they contain.
<RequireApproval>and<Namespace>apply to the declarations inside them, and nowhere else. - Execution ends in ordinary AI SDK tools. Declaring a capability doesn't change how a tool runs, is approved or is traced.
- The workflow stays dynamic. Capabilities are resolved per
<Agent>, when that agent renders. A step that renders a new agent later gets that agent's capabilities then.
Scope
An agent's capabilities are the ones in its own children. The agent finds them before it renders anything else, resolves them, and only then runs its steps, so a step never sees a partial set. This happens once per <Agent>, when it appears; a step that renders a new agent later resolves that agent's capabilities then. Nothing here compiles the workflow ahead of time.
A nested <Agent> starts empty. It doesn't see the outer agent's capabilities, in the same way it doesn't inherit the outer agent's other settings.
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Step, Tool } from "june";
import { z } from "zod";
const readFile = tool({
description: "Read a file",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => Bun.file(path).text(),
});
const writeFile = tool({
description: "Write a file",
inputSchema: z.object({ path: z.string(), text: z.string() }),
execute: async ({ path, text }) => Bun.write(path, text).then(() => "ok"),
});
const search = tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `results for ${query}`,
});
const deploy = tool({
description: "Deploy to an environment",
inputSchema: z.object({ env: z.enum(["staging", "prod"]) }),
needsApproval: true,
execute: async ({ env }) => `Deployed to ${env}`,
});
const model = openai("gpt-5-mini");
export default function Nested() {
return (
<Agent model={model}>
<Tool name="readFile" value={readFile} />
<Step id="outer" prompt="Read package.json" />
<Agent model={model}>
<Tool name="search" value={search} />
<Step id="inner" prompt="Search for its dependencies" />
</Agent>
</Agent>
);
}
inner can call search and not readFile. To let it call readFile too, say so with <Inherit>.
Capabilities go directly in the agent's children, or in a fragment, an array or a bundle there. A capability inside a component of your own isn't visible to the agent, and rendering one there fails with an error that says so. Conditions work as usual: {writable && <Tool name="write" value={write} />}.
Names are unique. Two capabilities that expose one tool name, including one from the tools prop, fail the run with an error that names both declarations and the bundles they came from. Nothing silently replaces anything.
Bundles
defineCapabilities makes a reusable group, so a package can export what an agent needs and a workflow can add it with one line:
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Step, Tool, defineCapabilities } from "june";
import { z } from "zod";
const readFile = tool({
description: "Read a file",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => Bun.file(path).text(),
});
const writeFile = tool({
description: "Write a file",
inputSchema: z.object({ path: z.string(), text: z.string() }),
execute: async ({ path, text }) => Bun.write(path, text).then(() => "ok"),
});
const search = tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `results for ${query}`,
});
const deploy = tool({
description: "Deploy to an environment",
inputSchema: z.object({ env: z.enum(["staging", "prod"]) }),
needsApproval: true,
execute: async ({ env }) => `Deployed to ${env}`,
});
const model = openai("gpt-5-mini");
export const Files = defineCapabilities(
({ writable = false }: { writable?: boolean }) => (
<>
<Tool name="readFile" value={readFile} />
{writable && <Tool name="writeFile" value={writeFile} />}
</>
),
{ name: "Files" },
);
export default function Fix() {
return (
<Agent model={model}>
<Files writable />
<Tool name="deploy" value={deploy} />
<Step id="fix" prompt="Fix the failing test" />
</Agent>
);
}
- A bundle is transparent. It adds what it composes to the agent it's in. It isn't a boundary and has no scope of its own.
- Bundles nest. A bundle can return other bundles,
<Tool>s, fragments and conditions on its props. - A bundle is a pure function of its props. June runs it while the agent renders to see what it declares, so don't use hooks, state or side effects in it. Anything that does work belongs in the capability it returns.
- A bundle contains only capabilities. Returning a
<Step>or a component of your own is an error. Put those in the<Agent>'s children. - A bundle can't contain itself. A cycle fails with the chain, for example
<A> > <B> > <A>. nameis for errors. Pass it, or give the function a name, so a collision says<Files>and not<Capabilities>.
Inheriting a parent's tools
A capability crosses into a nested agent only when you name it. <Inherit capabilities={[...]}> in the nested agent passes down the enclosing agent's tools of those names:
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Inherit, Step, Tool } from "june";
import { z } from "zod";
const readFile = tool({
description: "Read a file",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => Bun.file(path).text(),
});
const deploy = tool({
description: "Deploy to an environment",
inputSchema: z.object({ env: z.enum(["staging", "prod"]) }),
needsApproval: true,
execute: async ({ env }) => `Deployed to ${env}`,
});
export default function Summarize() {
return (
<Agent model={openai("gpt-5-mini")}>
<Tool name="readFile" value={readFile} />
<Tool name="deploy" value={deploy} />
<Agent model={openai("gpt-5-nano")}>
<Inherit capabilities={["readFile"]} />
<Step id="summarize" prompt="Summarize README.md" />
</Agent>
</Agent>
);
}
summarize can call readFile and not deploy.
- Nothing is inherited unless named. There's no "everything from the parent".
- The parent is the agent directly around. A grandchild can reach the child's tools, not the grandparent's, and no agent can reach a sibling's. To pass something down two levels, each level inherits it.
- A name the parent doesn't have fails the run, with the names it does have. So does a name listed twice, and an
<Inherit>with no agent around it. - What crosses is the parent's own tool, as it stands in the parent's scope, approvals included. A grant can't loosen what the parent's tool needs.
- A granted tool is a declaration like any other, so a nested agent can't also declare a tool of the same name, and the error says which
<Inherit>brought it. - A grant carries the parent's tool as the parent sees it, after any wrapper around it has applied. A tool the parent wrapped with
<RequireApproval>or renamed with<Namespace>arrives that way. - Only tools cross. A nested agent's model, instructions and conversation are its own settings, set on that
<Agent>.
Wrappers
A wrapper applies to the capabilities declared inside it, so a policy or a naming scheme is visible in the source, right around what it governs:
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Namespace, RequireApproval, Step, Tool } from "june";
import { z } from "zod";
const searchIssues = tool({
description: "Search GitHub issues",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `issues for ${query}`,
});
const searchDocs = tool({
description: "Search the docs",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `docs for ${query}`,
});
const closeIssue = tool({
description: "Close a GitHub issue",
inputSchema: z.object({ number: z.number() }),
execute: async ({ number }) => `closed #${number}`,
});
export default function Triage() {
return (
<Agent model={openai("gpt-5-mini")}>
<Namespace name="github">
<Tool name="search" value={searchIssues} />
<RequireApproval>
<Tool name="close" value={closeIssue} />
</RequireApproval>
</Namespace>
<Namespace name="docs">
<Tool name="search" value={searchDocs} />
</Namespace>
<Step id="triage" prompt="Close the issues the docs already answer" />
</Agent>
);
}
The agent's tools are github_search, github_close and docs_search, and only github_close waits for approval.
<RequireApproval>makes every tool inside it wait for approval through the AI SDK's ownneedsApproval. It can only add the requirement: a tool inside that saysneedsApproval: false, like<Bash needsApproval={false} />, still waits, and nothing inside it can remove it.<Namespace name>putsname_in front of the name of every tool inside it, so tools from different sources can share an agent.nameis letters, digits and hyphens, starting with a letter. A collision error still names the original tool and the namespace.- Wrappers nest, innermost first. The outer wrapper has the last word on what comes out:
<Namespace name="a"><Namespace name="b">givesa_b_tool, and a<RequireApproval>around anything applies to it whatever is inside. - A wrapper only governs what it contains, including bundles and fragments inside it. It doesn't reach tools declared outside it, or in a nested agent.
- A wrapper holds only capabilities, like a bundle, and changes the tools by returning new ones. It never alters an agent that has already resolved.
- They aren't a security boundary.
<RequireApproval>puts a person in front of a tool call. It doesn't decide what the tool is allowed to do.
Composite tools
One tool can be a whole workflow. Give <Tool> a description, an input schema and a function as its child, and each call renders that function's workflow with the model's input:
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Step, Tool } from "june";
import { z } from "zod";
const searchWeb = tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `results for ${query}`,
});
export default function Report() {
return (
<Agent model={openai("gpt-5-mini")}>
<Tool
name="research"
description="Research a question and report what was found"
input={z.object({ question: z.string() })}
>
{({ question }) => (
<Agent model={openai("gpt-5-mini")}>
<Tool name="search" value={searchWeb} />
<Step id="findings" prompt={`Research: ${question}`} />
</Agent>
)}
</Tool>
<Step
id="report"
prompt="Research how Suspense works, then write a summary"
/>
</Agent>
);
}
The outer agent can call research and nothing else: it can't see search. When it calls research, June runs the workflow as a run of its own and gives the model what the workflow produced.
- The workflow's agents are the ones you write inside it, with their own models and tools. Their scope is private by the same rule as any nested
<Agent>. To let one use a tool of the calling agent, say so with<Inherit>inside it: the calling agent is its parent. - Steps in the workflow need an
<Agent>of their own, as everywhere. They don't borrow the caller's. outputpicks what the model gets. By default that's the result of the workflow's only step, or an object of every step's result when there are several. Passoutput={(result) => ...}to choose, for exampleresult.steps.findings. A string goes back as text and anything else as JSON.- The trace keeps the whole run. The tool call's
outputholds{ output, trace, usage }: what the model got, the workflow's own trace tree, and the tokens it used. The model never sees the trace. - Approvals go through the calling run. A tool inside the workflow with
needsApprovalasks whoever answers the outer run, for examplejune run's prompt orhandle.approve(), and its request names the step asresearch:findings. To gate entering the tool at all, wrap it in<RequireApproval>. - Cancellation and errors carry across. Aborting the call, or the run, aborts the workflow and waits for its cleanup. A failure inside becomes a tool error the model can see:
june: the tool "research" failed: <message>.
Callable agents
A nested <Agent> is a scope for the steps inside it, not something an agent can call. defineAgent makes one that can be called: a reusable agent that other agents declare as a capability and use as a single tool.
import { openai } from "june/sdk/openai";
import { tool } from "ai";
import { Agent, Step, Tool, defineAgent } from "june";
import { z } from "zod";
const searchWeb = tool({
description: "Search the web",
inputSchema: z.object({ query: z.string() }),
execute: async ({ query }) => `results for ${query}`,
});
const Researcher = defineAgent({
name: "researcher",
description: "Researches a question and reports what it found",
input: z.object({ question: z.string() }),
model: openai("gpt-5-mini"),
instructions: "You research carefully and cite what you find.",
prompt: ({ question }) => `Research: ${question}`,
capabilities: () => <Tool name="search" value={searchWeb} />,
});
export default function Article() {
return (
<Agent model={openai("gpt-5")}>
<Researcher />
<Step id="write" prompt="Write a short article on how Suspense works" />
</Agent>
);
}
The writer sees one tool, researcher. Calling it runs the researcher as a run of its own, with its own model, instructions and search tool, and the writer's model gets its answer.
defineAgent takes an agent's settings (model, instructions, stopWhen and the rest) and:
name,descriptionandinputare what the calling model sees, as for any composite tool.promptturns the input into the agent's prompt. It's built from the input alone: the caller's conversation never crosses, so the prompt has to say what the agent needs to know.capabilitiesreturns the agent's private capabilities as JSX, which can be tools, bundles, wrappers, other defined agents, and<Inherit>to take named tools from the caller. Nothing else of the caller's is visible.outputis an AI SDK output such asOutput.object({ schema }). The caller gets the parsed object as JSON. Without it, the caller gets the answer as text.
Defined agents compose: capabilities can return another defined agent, which then is a single tool to this one. Everything a composite tool does applies: approvals the agent's tools need are asked through the calling run, aborting the caller aborts the agent, a failure is a tool error the caller's model sees, and the agent's trace is kept in the tool call's output.
The tools prop and bashTool()
tools={{ ... }} on an <Agent> is the AI SDK's own setting, and bashTool() from june/tools returns a plain AI SDK tool. Both keep working and are the low-level interface: use them when you already hold a ToolSet, or build tools outside a component. They feed the same scope as <Tool> and <Bash />, so they follow the same rules: one name once, nothing inherited into a nested agent, and the same trace.
Capabilities that wait
Declarations resolve before the agent's steps start, and a capability may take time to resolve: connecting to a service and listing its tools, for example. The run stays open while it waits, a failure fails the run (or a <Catch> that retries it), and aborting the run aborts the wait. Several agents that declare the same thing in one run share one resolution. This is the shape a capability backed by a live resource, such as a connection to an MCP server, takes: the agent sees its tools as any other tools, and what keeps the connection alive is a separate concern from which agents can use it.
Seeing what an agent has
An agent that declares capabilities records on its trace node where each tool came from. capabilities is a list of plain data:
[
{ "name": "bash", "source": "<Bash> in <Coding>" },
{
"name": "github_search",
"source": "<Tool name=\"search\"> in <Namespace name=\"github\">"
},
{
"name": "research",
"source": "<Tool name=\"research\">",
"composite": true
},
{
"name": "files",
"source": "<Inherit capabilities={[\"files\"]}>",
"granted": "<Tool name=\"files\"> in the enclosing <Agent>"
}
]
source is the declaration, followed by the bundles and wrappers it came through, outermost first. granted says what an <Inherit> took the tool from, and composite marks a tool that runs a workflow of its own. It never holds a tool, a function, or the props of a component, so a command's cwd or env doesn't end up in a trace. An agent that only uses the tools prop has nothing to explain and records no capabilities.
June Studio shows it in an agent's inspector under Capabilities, with what an agent inherited under Inherited. Studio reads the trace; the run works it out.
The same answers are in the error when two declarations collide, which names both with their bundles and wrappers.