hammrouni / nanoagent
NanoAgent is a lightweight, local-first PHP library that strips away the complexity of modern AI agent development — no Python sidecar, no framework. For Industry 4.0 / industrial IoT and edge contexts.
Requires
- php: ^8.0
- ext-curl: *
- ext-json: *
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^9.5
Suggests
- ext-pdo_sqlite: Persist conversation history in SQLite with NanoAgent\Memory\PdoMemory
Provides
None
Conflicts
None
Replaces
None
README
NanoAgent PHP
Add AI agents to your PHP app in an afternoon - local-first, no Python sidecar, no framework to learn.
composer require hammrouni/nanoagent · PHP 8.0–8.4 · 0 runtime dependencies · MIT
📖 Full documentation & interactive examples
Why this exists
Most "AI agent" libraries assume you'll bolt a Python microservice behind your app, manage a fleet of config files, or adopt a whole framework. In existing PHP environments - where your service is already talking to legacy databases, internal APIs, and core business logic - that overhead is the wrong shape. You don't need a sidecar. You need a small, boring, local library you can composer require into the PHP you already run, hand it your own functions, and ship.
NanoAgent is that library. It is deliberately lightweight and local-first: one Composer package, zero runtime dependencies, no DSL, no framework lock-in, and memory that lives in your own files or database. It points at whichever provider you configure - a hosted API or a local model behind a compatible endpoint - so the agent can run right alongside your existing infrastructure.
What it does
- ⚡ One config array, any provider. OpenAI, Groq, Anthropic, DeepSeek, OpenRouter - or any OpenAI-compatible endpoint, including a local model server. Swap in one line.
- 🛠️ Tool-first architecture. Map plain PHP functions as tools the agent calls - your database, your APIs, your device telemetry.
- 🔌 MCP support. Register any Model Context Protocol server (Streamable HTTP); its tools behave exactly like your local ones - and you can allowlist which ones to register.
- 🧠 Persistent memory. Conversations survive across requests via pluggable drivers: files, SQLite, MySQL, PostgreSQL, or your own.
- 💎 Minimal footprint. One package, no hidden magic, easy to audit.
🚀 Quick Start
Prerequisites
- PHP 8.0 or higher (8.4 tested in CI)
- Composer
1. Install
mkdir my-agent-project && cd my-agent-project composer require hammrouni/nanoagent
2. Point it at a provider
A minimal agent - note it takes a plain config array, so a local model behind an OpenAI-compatible base_url works the same way as a hosted one:
<?php use NanoAgent\Agent; $agent = new Agent(llm: [ 'provider' => 'groq', 'model' => 'llama-3.3-70b-versatile', 'api_key' => getenv('GROQ_API_KEY'), // 'base_url' => 'http://localhost:8080/v1', // e.g. a local model server ]); echo $agent->chat('Explain what NanoAgent does, in one sentence.');
Or load the array from a file (see NanoAgent/config.php.example):
$config = require __DIR__ . '/config.php'; $agent = new Agent(llm: $config);
3. Give it a tool
use NanoAgent\Tools\FunctionTool; $calculator = new FunctionTool( name: 'calculator', description: 'Add two numbers', parameters: [ 'type' => 'object', 'properties' => [ 'a' => ['type' => 'integer', 'description' => 'First number'], 'b' => ['type' => 'integer', 'description' => 'Second number'], ], 'required' => ['a', 'b'], ], callable: fn(array $args) => $args['a'] + $args['b'], ); $agent = new Agent(llm: [ /* ... */ ], tools: [$calculator]); echo $agent->chat('What is 17 + 25?');
Want to run it fully locally? Point
base_urlat a local OpenAI-compatible server (e.g.http://localhost:8080/v1) withprovider => 'openai'. The agent, your tools, and your memory all stay on the same host - no cloud round-trip.
🛠️ Tools & MCP
MCP: register every tool a Model Context Protocol server exposes in one call - they behave exactly like local tools from there on.
use NanoAgent\Mcp\McpClient; $mcpClient = new McpClient('https://mcp.deepwiki.com/mcp'); $names = $agent->registerMcpServer($mcpClient); echo $agent->chat('Ask the facebook/react repo what it does.');
Every registered MCP tool ships its full JSON Schema to the model on every turn, so a server with 50 tools costs 50 tool definitions per request. Pass an allowlist to register only the ones you need:
$names = $agent->registerMcpServer($mcpClient, allowlist: ['ask_question']);
Which names? Ask the server directly with McpClient::listTools() — it returns every tool it exposes, with descriptions, and registers nothing:
use NanoAgent\Mcp\McpClient; $mcpClient = new McpClient('https://mcp.deepwiki.com/mcp'); foreach ($mcpClient->listTools() as $tool) { echo $tool['name'], "\n"; // -> the name you put in the allowlist echo ($tool['description'] ?? ''), "\n"; // -> so you know what it's for }
Each entry is ['name' => ..., 'description' => ..., 'inputSchema' => [...]]. Skip the allowlist on a first call to just register everything and read the names back:
$names = $agent->registerMcpServer($mcpClient); // no allowlist -> registers all print_r($names); // then re-register narrowed
Keep the same $mcpClient object for both: the MCP handshake is memoized per client, so re-using it skips a re-initialize (the tools/list call itself is repeated). Note that there is no Agent::getTools() — the return value of registerMcpServer() is the only listing of what the agent currently holds.
Calling registerMcpServer() again on the same client reconciles rather than duplicates: tools you keep are untouched, tools you drop are removed from the agent, and locally registered tools are never affected. A name the server doesn't expose is ignored, not an error - handy when the allowlist comes from config.
🧠 Persistent Memory
PHP forgets everything between requests. Attach a memory driver and the agent loads the conversation for a session, then saves it after every completed chat() / stream() call.
use NanoAgent\Memory\FileMemory; $agent = new Agent(llm: [ /* ... */ ]); $agent->setMemory(new FileMemory(__DIR__ . '/storage/memory'), sessionId: "user-$userId"); echo $agent->chat($_POST['message']);
| Driver | Storage | Requires |
|---|---|---|
ArrayMemory |
Current process only (tests, workers) | - |
FileMemory |
One JSON file per session | - |
PdoMemory |
SQL table (SQLite, MySQL, PostgreSQL) | ext-pdo + driver |
use NanoAgent\Memory\PdoMemory; $agent->setMemory(PdoMemory::sqlite(__DIR__ . '/memory.sqlite'), $sessionId); $agent->setMemory(new PdoMemory($existingPdo), $sessionId); // creates `nanoagent_memory` if missing
See the PdoMemory guide for MySQL/PostgreSQL connections, table schemas and cleanup.
Need another backend (Redis, a framework cache, Eloquent)? Implement NanoAgent\Contracts\Memory: load(), save(), clear().
📂 Examples
The examples/ directory has runnable demos (open examples/index.php in a browser or run any file with php):
| Example | Description |
|---|---|
| Basic Usage | Agent init, a custom tool, and a task with context. |
| Chat | Stateful conversation loop using PHP sessions. |
| Memory Chat | Persistent history with a pluggable memory driver. |
| Structured Output | Extract JSON from unstructured text. |
| Knowledge Base | RAG: ask questions against private documents. |
| Streaming | Real-time token streaming (SSE). |
| Agent Chain | Multi-agent workflow (Researcher feeds Writer). |
| Multi-Tool | Orchestrate several tools for one request. |
| MCP Tools | Discover a remote MCP server's tools and allowlist which ones to register. |
| Multi-Provider | Switch providers programmatically. |
| API Integration | Fetch data from external APIs. |
| Advanced Tools | Multi-step tool use with simulated DB state. |
Supported Providers
Every provider below is covered by the automated test suite (see the CI badge). Groq has also been verified against the real API.
- Groq
- OpenAI
- OpenRouter
- DeepSeek
- Anthropic - experimental. Built separately from the others, not yet verified against the real API, and streaming isn't fully wired up yet.
- Any OpenAI-compatible endpoint - including a local model server, via
base_url.
Changelog
0.6.0
- MCP allowlist:
registerMcpServer($client, ['tool_a'])registers only the tools you name. Every registered tool ships its JSON Schema on every turn, so this is how you keep a large server's cost down. Unknown names are ignored, not an error. - Calling
registerMcpServer()again on the same client now reconciles instead of duplicating: tools dropped from the allowlist are removed from the agent, kept ones are untouched, and locally registered tools are never affected.
0.5.0
- Persistent memory:
Agent::setMemory()keeps conversations across requests, withArrayMemory,FileMemoryandPdoMemory(SQLite, MySQL, PostgreSQL) drivers, or your own viaNanoAgent\Contracts\Memory. See the PdoMemory guide. - Clarified in the README that all providers are covered by the mocked unit test suite.
0.4.0
- MCP (Model Context Protocol) client support: connect to any Streamable HTTP MCP server and register its tools with
registerMcpServer().
0.3.0
- Inject a custom provider directly into
Agent, no config array needed. - Configure
temperature,max_tokens, and a custombase_urlper provider. - Streaming now supports tool calls.
- More reliable request encoding for non-English text.
0.2.0
- HTTP requests now have timeouts and surface real API errors instead of hanging or failing silently.
- Tool-calling loops are capped to avoid runaway API usage.
- Task context no longer leaks between calls.
- Added static analysis (PHPStan) and a wider PHP version test matrix (8.0–8.4).
0.1.0
First public release.
License
Author
Khaled Hammrouni - Technical PM & Software Architect, working in Industry 4.0 / industrial IoT with local-first AI tooling.