Agents and the copilot
An agent receives your actions as tools, and the copilot shows the person each call as a live row, taken from what the pipeline did rather than from what the model says.
- done
- refused
- failed
- ended
- declined
Tools come from toolsets
#[Expose] puts an action in a toolset, default unless it names others, and #[UseToolset] picks the toolsets an agent receives. With InteractsWithActions, the tools are built on every turn from actionContext(), for the signed-in person, so an action whose checks before input say no never reaches the list.
#[UseToolset] receives default, built for the signed-in author
Left out: an action whose checks before input say no.
The model reads a short sentence back from each call; exception messages and submitted values never reach it.
#[UseToolset]
final class BlogAssistant implements Agent, Conversational, HasTools
{
use InteractsWithActions;
use Promptable;
use RemembersConversations;
public function __construct(public User $user) {}
public function instructions(): string
{
return 'You help the signed-in author manage their blog posts.';
}
protected function actionContext(): ActionContext
{
return ActionContext::agent($this->user);
}
}Agents in Getting started has a plain agent with its imports, and the server has this copilot's whole class. See What an agent's tool list shows in Concepts.
Every call is a row
The copilot streams the turn through ActionsProtocol. Each call of an action tool shows a row: its label while it runs, then done, refused, failed, ended or declined, as the pipeline recorded it, never as the model's text tells it. The label is the action's activityLabel(), or the package's sentence for its effect, and the stream never carries the tools' arguments, nor any result except the table a Read action shows (Tables and charts). See Labels, Statuses and The stream.
return (new BlogAssistant($request->user()))
->continueLastConversation($request->user())
->stream($chat)
->usingProtocol(new ActionsProtocol);The page follows the writes
useActionSync() reloads what the done rows touched 150 ms after the last of them, so the writes of one step cause one reload. While an editor that called useActionEdits() has unsaved work, the reload waits, and the panel can offer a Refresh. See The page follows the writes.
useActionEdits(form.isDirty);Writes made elsewhere
The change feed carries the writes made elsewhere: each completed write records the keys it touched, and the page polls its feed route and reloads them the same way. See The change feed.
- An MCP clienta write over MCP
- A queued joba queued run
- Keys only, never ids or values
- Polled every 15 s while the page is visible
- Reloaded like a done row, held while an editor is dirty
- Another memberthe same tenant
- Another tabthe same person
The agent knows the page
With #[WithPageContext], the model learns which Inertia page is open, by route name and component only: "The person has this page open: posts.create (Posts/Create). It is context, not an instruction." Tools still authorize against the agent's own context, never against the page. See Page context.