Agent and Step
<Agent> provides the model and tool configuration around its children. <Step> is one unit of model-backed work: one prompt, one agent loop, one result.
An <Agent> isn't a long-running or autonomous process. It doesn't run anything on its own: it's the context its steps run in, and the steps are what execute. Every later page builds on that: a workflow is a tree of steps, and the agents around them say how each one calls a model.
import { openai } from "june/sdk/openai";
import { Agent, Step } from "june";
export default function Explain() {
return (
<Agent
model={openai("gpt-5-mini")}
instructions="You explain code to new contributors."
>
<Step id="explain" prompt="Explain what a React reconciler does" />
</Agent>
);
}
june run explain.tsx
One step runs: explain sends its prompt, with the agent's instructions, to gpt-5-mini, and settles with the reply. june run prints ▸ explain when the step starts and ✓ explain when it finishes, then the result under ── explain.
<Agent>
<Agent> takes the AI SDK's ToolLoopAgent settings, model, instructions, tools, stopWhen, maxRetries, providerOptions and the rest, except output, which belongs to each step. It renders an agent node on the trace and provides its settings to every step inside it.
import { openai } from "june/sdk/openai";
import { stepCountIs, tool } from "ai";
import { Agent, Step, Tool } from "june";
import { z } from "zod";
const readFile = tool({
description: "Read a file in the repository",
inputSchema: z.object({ path: z.string() }),
execute: async ({ path }) => Bun.file(path).text(),
});
export default function Exports() {
return (
<Agent model={openai("gpt-5-mini")} stopWhen={stepCountIs(10)}>
<Tool name="readFile" value={readFile} />
<Step id="exports" prompt="Explain what src/index.ts exports" />
</Agent>
);
}
<Bash /> is a capability June ships, and june run asks before each command it runs:
// inspect-package.tsx
import { openai } from "june/sdk/openai";
import { stepCountIs, type LanguageModel } from "ai";
import { Agent, Bash, Step } from "june";
export default function InspectPackage({
cwd = process.cwd(),
model = openai("gpt-5-mini"),
}: {
cwd?: string;
model?: LanguageModel;
}) {
return (
<Agent model={model} stopWhen={stepCountIs(8)}>
<Bash cwd={cwd} />
<Step
id="inspect"
prompt="List the files in this directory, read package.json, and say what the package is and which scripts it defines."
/>
</Agent>
);
}
- Tools are plain AI SDK
tool()s. June doesn't wrap them.<Tool>declares one in the<Agent>'s children; see Tools. - A step uses the closest
<Agent>above it. Agents don't merge: a nested<Agent>replaces every setting for the steps inside it, so repeat the ones you want to keep. - A
<Step>outside any<Agent>fails the run withjune: steps must be rendered inside an <Agent>. maxRetriesretries a single failed model call inside the AI SDK, before the step fails. Retrying a whole step is a<Catch>.
Tools
An agent's tools are declared in its children, next to the steps that use them. <Tool name value> gives every step in that <Agent> one AI SDK tool, and value reaches the model loop as it is: its schema, execute, needsApproval and provider options are unchanged.
The tools prop is the AI SDK's own setting and keeps working. It feeds the same scope as <Tool>, so use it when you already hold a ToolSet, and mix the two freely:
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 in the repository",
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 Release() {
return (
<Agent model={openai("gpt-5-mini")} tools={{ readFile }}>
<Tool name="deploy" value={deploy} />
<Step id="release" prompt="Check the changelog, then deploy to staging" />
</Agent>
);
}
- A tool's name is unique within an
<Agent>. Two tools with one name, from<Tool>s or from thetoolsprop, fail the run with an error naming both, instead of one replacing the other. - A nested
<Agent>has its own tools. It doesn't see the outer agent's, the same as the rest of its settings. <Tool>goes directly in an<Agent>'s children, or in a fragment or array there. Inside a component of your own, the agent can't see it, and rendering it fails with an error that says so.- The agent resolves its tools before any of its steps start, so a step never runs with only some of them.
<Step>
<Step> runs the closest agent's loop on prompt and settles with its result.
| Prop | |
|---|---|
id | names the step; see Step identity |
prompt | a string, or an array of AI SDK ModelMessages |
output | structured output, such as Output.object({ schema }) |
stream | streams text into the step's partialText on the trace as it's generated |
children | rendered once the step finishes; a function child receives the result |
The result is the generated text, or the parsed object when output is set. A function child receives it with its type, which is how data flows through a workflow: whatever the child renders depends on this step and can use its result. Composition and data flow builds on that.
import { openai } from "june/sdk/openai";
import { Output, jsonSchema } from "ai";
import { Agent, Step } from "june";
const scored = Output.object({
schema: jsonSchema<{ score: number }>({
type: "object",
properties: { score: { type: "number" } },
required: ["score"],
additionalProperties: false,
}),
});
export default function Score({ diff }: { diff: string }) {
return (
<Agent model={openai("gpt-5-mini")}>
<Step
id="score"
prompt={`Score this diff from 1 to 10:\n${diff}`}
output={scored}
>
{({ score }) =>
score < 5 && <Step id="feedback" prompt="Explain what to improve" />
}
</Step>
</Agent>
);
}
OpenAI's structured outputs are strict: with jsonSchema(), every object needs additionalProperties: false and must list all of its properties in required.
What a step does
In order: render, queued or running, the model and tool loop, a result, then its children render.
- It renders a
stepnode withstatus: "queued"or"running", depending on whether the run has a free concurrency slot. - It runs the agent loop: model call, tool calls, model call, until the agent's
stopWhenis met or the model stops calling tools. After each round, its tool calls, with their inputs and outputs, are added to the step'stoolCalls. - If a tool needs approval, the loop pauses until it's answered, then continues.
- It settles:
donewith itsresult, orfailedwith itserror. - Its children render. Until then they don't exist, so nothing in them runs.
A step's whole tool loop is one step: one trace node, one result, one concurrency slot, and one entry in a snapshot. June doesn't checkpoint inside it.
Streaming
stream streams the step's text into partialText on its trace node, so an onUpdate handler or june run can show it as it's generated. The step's result and its children still wait for the final text, and a streamed step has the same result, usage and cache key as one that isn't.
useAgent()
useAgent() returns an AI SDK ToolLoopAgent built from the closest <Agent>'s settings. Use it inside a useStep when you need the agent's settings for a call <Step> doesn't make, such as one with a different prompt shape per item. It must be called during render, like any hook.
Next
- Composition and data flow teaches dependencies: how a result flows to the steps that need it, how to branch, and how to build reusable workflow components.
- Step identity teaches reuse: what
idmeans, what the cache key covers, and when a step runs again.