Skip to content

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 with june: steps must be rendered inside an <Agent>.
  • maxRetries retries 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 the tools prop, 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
idnames the step; see Step identity
prompta string, or an array of AI SDK ModelMessages
outputstructured output, such as Output.object({ schema })
streamstreams text into the step's partialText on the trace as it's generated
childrenrendered 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.

  1. It renders a step node with status: "queued" or "running", depending on whether the run has a free concurrency slot.
  2. It runs the agent loop: model call, tool calls, model call, until the agent's stopWhen is met or the model stops calling tools. After each round, its tool calls, with their inputs and outputs, are added to the step's toolCalls.
  3. If a tool needs approval, the loop pauses until it's answered, then continues.
  4. It settles: done with its result, or failed with its error.
  5. 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 id means, what the cache key covers, and when a step runs again.