Architecture
Every send(), stream() and sendInConversation() runs the same pipeline, whether the message
arrives from the command line, an HTTP request, a queued job or a stream. This page walks through it
once, in order, and links to the page that covers each part in depth.
User message command line, HTTP, queue or stream 1. augment memory context and matched skills are added to the message 2. guard the guard chain scans; a match throws GuardException 3. select tool schemas are formatted and trimmed to a per-turn budget 4. loop the ReAct loop: think, act, observe, up to maxIterations ├─ approval gate before a mutating tool ├─ tool output guard on every tool result └─ result cap on every tool result 5. clean up PHP tags and dangerous calls are removed from the answer 6. respond AgentResponse: text, provider, model, tokens, iterations, durationSteps 1, 2, 4 and 6 are the four stages shown on the phpClaw home page. This page adds the two the diagram there leaves out: tool selection before the loop, and the cleanup pass on the answer.
1. Augment
Section titled “1. Augment”MessageAugmenter builds the text the model receives. It adds, in order:
- up to 3 entries from the
defaultmemory namespace that share words with the message, under[Context from memory] - the content of up to 3 matching skills, under
[Skill context]
Conversation history is not added here. On sendInConversation() it is passed alongside the message
as real prior turns.
2. Guard
Section titled “2. Guard”The guard chain scans the augmented text, so memory and skill text are scanned too. Guards that
implement RawInputGuardInterface scan only what the user typed.
The first guard to match throws GuardException, a guard.blocked hook fires, and the model is never
called.
See Guards.
3. Select tools
Section titled “3. Select tools”Tool schemas are formatted for the provider and, when there are more tools than the per-turn budget allows, ranked against the message and trimmed. This happens once, before the loop, so the tool set is fixed for the whole run.
See Tools: which tools a turn sees.
4. The agent loop
Section titled “4. The agent loop”repeat up to maxIterations: ask the model if it answered → done if it asked for tools → for each call: approval gate (only for mutating tools) run the tool sanitise the output cap the output add the result to the history| Setting | Builder method | Default |
|---|---|---|
| iteration cap | maxIterations() | 20 |
| provider retries on a transient failure | maxRetries() | 0, a single attempt |
| tool result cap, in estimated tokens | maxToolResultTokens() | 2500 |
| history compaction by message count | maxHistoryLength() | 0, off |
| history compaction by estimated tokens | maxHistoryTokens() | 0, off |
| tools offered per turn | maxToolsPerTurn() | 0, derived from the model |
When the cap is reached without an answer, MaxIterationsException is thrown. Settings are fixed
when you call build(); send() takes no per-call options.
History compaction is off unless you set a threshold. compactHistory() defaults to true, but it
only acts when maxHistoryLength() or maxHistoryTokens() is above 0. When it does act, the oldest
half of the history is summarised and context.overflow fires.
Approval
Section titled “Approval”Tools that implement MutatingToolInterface pass through the approval gate before they run, when one
is installed. Core installs none; every adapter installs CliApprovalGate.
CliApprovalGate asks Y/n on the terminal, and refuses when there is no interactive terminal.
Web requests, cron jobs and piped commands have none, so there a mutating tool is always refused.
A refusal does not end the run. The tool does not execute, and the model receives this as the tool’s result, then continues:
{ "status": "denied", "tool": "file_write", "message": "Action denied by human. Propose an alternative approach or ask what to do instead." }The message says “by human” even when the refusal came from having no terminal.
| Call | Terminal | Result |
|---|---|---|
file_read | none | runs |
shell_exec with ls -la | none | runs, read-only command |
shell_exec with rm -rf x | none | refused |
file_write | none | refused |
The mutating tools are core’s file_write, file_edit, zip_package and non-read-only
shell_exec, plus WordPress’s and Joomla’s ZIP builder tools. No other adapter tool is marked
mutating.
Tools called through the MCP server run directly and do not pass through this gate.
Tool output
Section titled “Tool output”Every tool result is cleaned by ToolOutputGuard and then cut to the result cap. The cap is the
token limit times four characters. A cut result ends with a note telling the model to narrow the
request.
The system prompt
Section titled “The system prompt”The system prompt is composed once, at build(). When any tools are registered, Claw::AGENTIC_DOCTRINE
is placed first and your systemPrompt() follows it. With no tools, only your prompt is sent. Memory
and skill text never goes into the system prompt; it is added to the message in step 1.
Which tools exist
Section titled “Which tools exist”Core has nine tools. Seven are marked as defaults: shell_exec, http_request, file_read,
file_write, file_edit, code_search and project_info. db_query needs a PDO connection and
zip_package is opt-in, so neither is a default. A plain Claw::builder()->build() registers no
tools at all; the adapters add the seven defaults with ToolCatalogue::instantiateDefaults(), plus
their own platform tools, such as Laravel’s six and WordPress’s twelve, with ten more when WooCommerce
is active. See Tools and each adapter’s page.
5. Clean up the answer
Section titled “5. Clean up the answer”OutputSanitiser runs on the final answer. It is on by default.
| Found in the answer | Replaced with | Hook |
|---|---|---|
<?php, <?=, ?> | [PHP_REMOVED] | guard.output_php_tag_removed |
eval(, system(, exec(, shell_exec(, passthru(, proc_open(, pcntl_exec( | [REDACTED]( | guard.output_function_redacted |
This applies to legitimate code too. An answer showing a PHP snippet arrives with its opening tag
replaced. Turn it off with Claw::builder()->sanitiseOutput(false) if your application renders code
the model writes.
6. The response
Section titled “6. The response”$response = $agent->send('How many errors are in the log today?');
$response->text;$response->provider;$response->model;$response->iterations;$response->durationMs;$response->inputTokens; // null when the provider reports no usage$response->outputTokens;$response->totalTokens(); // null when either count is missing$response->toolsCalled;$response->cacheReadTokens;$response->cacheWriteTokens;$response->thinking;$response->runId;Helpers: hasUsage(), cacheHit(), usedTools(), uniqueToolsCalled(), hasThinking(),
isMultiStep() and toArray().
Use iterations and totalTokens() to watch cost, and durationMs to spot slow provider calls.
Conversations
Section titled “Conversations”send() neither loads nor stores history. To keep one:
$agent = Claw::builder()->memory(new FileMemory)->build();
$turn = $agent->sendInConversation($agent->conversation(), 'My name is Alice.');$turn = $agent->sendInConversation($turn->conversation, 'What is my name?');After each turn, the user’s message as typed, not the augmented version, and the answer are appended
and saved to the conversations namespace.
Streaming
Section titled “Streaming”stream() streams tokens only when no tools are registered. With tools, it runs the loop above and
then replays the final answer in small pieces. See Providers: streaming.
Hooks along the way
Section titled “Hooks along the way”Events fire at each step. The groups:
| Prefix | Step |
|---|---|
agent.* | start, each iteration, end, error, iteration cap |
provider.* | each model request, response, retry, error and streamed token |
tool.* | before, after and failure of each tool call |
guard.* | a blocked message, a rate limit, redacted output |
context.*, compaction.* | history compaction |
conversation.* | a conversation starting and ending |
stream.* | a stream starting, ending or aborting |
memory.* | driver reads and writes |
shell.* | shell commands run or refused |
skill.* | skills registered and matched |
job.* | queued runs |
A listener that throws is logged and does not stop the run. See Hooks.
Extension points
Section titled “Extension points”| To add | Implement | Page |
|---|---|---|
| a tool | ToolInterface, with your adapter’s binding trait | Tools |
| a guard | GuardInterface | Guards |
| a hook | HookInterface, or any callable | Hooks |
| a memory driver | MemoryInterface | Memory |
| a skill | SkillInterface | Skills |
| a provider | ProviderInterface | Providers |
| an approval policy | ApprovalGateInterface | Tools |
| an MCP transport | TransportInterface in phpclaw/phpclaw-mcp | MCP |
| an adapter for a framework or CMS | no base class; wire the registries into the platform’s own bootstrap | Building an Adapter |
phpclaw/phpclaw-cloud builds on the same contracts, with CloudScanGuard and CloudWebhookHook,
which it registers only when a cloud key is set.