milpa/example-agent-ready-blog
Composer 安装命令:
composer create-project milpa/example-agent-ready-blog
包简介
Runnable example of the Milpa loop — plugin → capability → tool → verification → event → result — as a tiny agent-ready blog.
README 文档
README
Milpa Example: Agent-Ready Blog
The Milpa loop, live:
plugin → capability → tool → verification → event → result— as a tiny agent-ready blog you can run in two commands.
This repo doesn't teach you how to build a blog. It teaches you how to make a mutation agent-ready without losing human control.
Most examples let agents mutate state directly. This one does not. Every mutation enters through a declared tool, passes through a confirmation/verification seam, and only becomes application state through an event. The blog is just the smallest honest thing worth mutating.
flowchart TD
A["Agent or human"] --> B["ToolRegistry"]
B --> C["publish_post — mutating"]
C --> D{"Confirm gate<br/>(registry)"}
D -->|"confirm_token redeemed"| E["Verification seam<br/>(HumanVerifier)"]
E -.->|"⚡ verification.requested"| F{"Human decides"}
F -->|approve| G["⚡ verification.granted"]
F -->|reject| H["⚡ verification.rejected"]
G --> I["BlogPlugin event handler"]
I --> J["⚡ post.published — state changed"]
H --> K["post stays a draft"]
Loading
Prefer a guided tour? Read docs/walkthrough.md — seven files, in
the order that makes the loop click.
Quickstart
composer create-project milpa/example-agent-ready-blog blog
cd blog
php bin/blog.php
You'll be asked to approve or reject a publish request interactively. Here's a real run
(a typed at the prompt):
milpa · example-agent-ready-blog — the loop, live
plugin → capability → tool → verification → event → result
✔ Capability graph: StoragePlugin provides PostStorage → BlogPlugin requires it
✔ 3 plugins booted · tools: create_post, human_verify, list_posts, publish_post
→ create_post("Hello Milpa") … draft #1 created (not mutating-gated: no friction)
→ publish_post(#1) … INTERCEPTED by the registry confirm gate → confirm_token 867c9ca7…
⚡ verification.requested
→ token redeemed … the tool ran and asked the VERIFICATION seam (status: pending_verification)
? An agent wants to publish post #1 — [a]pprove / [r]eject: a
⚡ verification.granted
⚡ post.published (id 1)
✔ post #1 is now PUBLISHED — the result arrived via event, handled by BlogPlugin
See it: php -S localhost:8080 -t public → http://localhost:8080
Now look at it:
php -S localhost:8080 -t public
Prefer non-interactive? php bin/blog.php --auto-approve and php bin/blog.php --reject
drive both paths without a prompt — that's exactly what this repo's own CI runs as its
smoke test.
The loop, stage by stage
Every stage below is a real contract from a published package, not an abstraction invented for this example.
| Stage | What runs | Published contract |
|---|---|---|
| plugin | Kernel::boot() instantiates StoragePlugin, BlogPlugin, AgentToolsPlugin |
Milpa\Interfaces\Plugin\PluginInterface + #[Milpa\Attributes\PluginMetadata] (milpa/core) |
| capability | CapabilityGraph::check() reads each plugin's #[PluginMetadata], builds the VOs, and fails before boot if a requires has no provides |
Milpa\ValueObjects\Capability\{CapabilityProvision,CapabilityRequirement} (milpa/core) |
| tool | AgentToolsPlugin::registerTools() scans BlogTools's three #[Tool] methods into the registry |
Milpa\ToolRuntime\{Attributes\Tool,Attributes\Param,ToolScanner,ToolRegistry} + Milpa\Interfaces\Tooling\ToolProviderInterface (milpa/tool-runtime on milpa/core) |
| verification | publish_post asks HumanVerifier::verify(); a human approves or rejects at the terminal prompt |
Milpa\Interfaces\Verification\VerifierInterface (milpa/core) + Milpa\ToolRuntime\Verification\HumanVerifier (milpa/tool-runtime) |
| event | Every step above fires through one shared dispatcher — verification.requested / verification.granted / verification.rejected / post.published — printed live by name |
Milpa\Interfaces\Event\MilpaEventDispatcherInterface (milpa/core), implemented here by App\EventDispatcher |
| result | BlogPlugin's verification.granted handler flips the post to published and dispatches post.published — the result arrives via event, not a return value |
src/Plugins/BlogPlugin/BlogPlugin.php (this repo) |
What an agent sees
The tools are transport-agnostic: what follows is the registry's own
getToolSummaries() output — the exact catalog an MCP host (or any other transport)
would list. This is real output, not documentation prose:
[
{
"name": "publish_post",
"description": "Publish a draft post (requires human verification)",
"inputSchema": {
"type": "object",
"properties": { "id": { "type": "integer", "description": "Post id" } },
"required": ["id"]
}
},
{
"name": "create_post",
"description": "Create a draft post",
"inputSchema": {
"type": "object",
"properties": {
"title": { "type": "string", "description": "Post title" },
"body": { "type": "string", "description": "Post body" }
},
"required": ["title", "body"]
}
}
]
(list_posts and human_verify are also listed — run
$kernel->registry()->getToolSummaries() to see the full catalog.)
From the agent's side, publishing is a two-call choreography — it never mutates on the first try:
publish_post(id: 1)→ the registry intercepts (the tool ismutating) and returns aconfirm_tokeninstead of running the tool.publish_post(id: 1, confirm_token: …)→ the tool runs, asks the verification seam, and returnspending_verificationwith arequest_id.- The actual state change arrives by event (
verification.granted→post.published), never as a return value the agent can force.
About human_verify (the name comes from milpa/tool-runtime): it requests or
resolves a verification — it does not make anyone human. Its principal argument is an
opaque string, and the schema itself says who may resolve is the host's problem: the
runtime trusts the host to authenticate principals. In this example the only resolution
channel wired is the terminal prompt — an agent calling human_verify with
decision: grant would still be an unauthenticated principal unless you build the
policy layer that says otherwise. That is the seam doing its job: the framework hands you
the gate; guarding it is explicitly your half of the contract.
What implements what
The three published packages define the seams; this repo implements the smallest possible host around them — ~940 lines of application code, of which ~440 implement every framework seam. On purpose, so you can read every line:
| Unit | Lines | Implements | Notes |
|---|---|---|---|
App\Container |
146 | Milpa\Interfaces\Di\DIContainerInterface (milpa/core) |
Explicit registerService() plus honest constructor autowiring — exactly what the published docblocks promise, no more. |
App\EventDispatcher |
82 | Milpa\Interfaces\Event\MilpaEventDispatcherInterface (milpa/core) |
Priority ordering, the documented wildcard grammar (* matches exactly one dot-segment), and handler error isolation. |
App\CapabilityGraph |
52 | — (consumes core's CapabilityProvision/CapabilityRequirement VOs) |
The "A provides / B requires" edge of the loop, checked before any plugin boots. |
App\Http\Router |
71 | Milpa\Http\Routing\RouterInterface (milpa/http) |
Exact segments plus single-segment {placeholder}s; never throws, never returns null — RouteResult carries the outcome. |
App\Kernel |
89 | — (orchestrates the four above) | Container → dispatcher → capability check → ordered plugin boot → tool registry wiring. A miniature of a real Milpa host. |
You can implement the seams in an afternoon — this repo is the proof.
What this example is NOT
- Not production. Storage is a plain JSON file (
var/posts.json), there's no auth, andApp\Containerhas no compiled/cached resolution — it's a from-scratch DI container that happens to satisfy the published interface. - Not a template to fork for a real blog. It's a template for understanding the loop.
- Mutations enter via tools, not HTTP — that's the point. The web view
(
php -S localhost:8080 -t public) is read-only by design: publishing a post always goes throughcreate_post→publish_post→ human verification, whether the caller is a human runningbin/blog.phpor an agent calling the same tools — e.g. over MCP; not wired in this example, the tools are transport-agnostic.
The family
This example consumes three published Milpa packages, unmodified, from Packagist:
milpa/core— the contracts core · API referencemilpa/http— PSR-15-native routing contracts · API referencemilpa/tool-runtime— the agent-tool-execution engine · API reference
Contributing
Contributions are welcome — see CONTRIBUTING.md. Please report security issues via SECURITY.md, and note that this project follows a Code of Conduct.
License
Apache-2.0 © TeamX Agency.
Milpa is designed, built, and maintained by TeamX Agency.
统计信息
- 总下载量: 10
- 月度下载量: 0
- 日度下载量: 0
- 收藏数: 0
- 点击次数: 1
- 依赖项目数: 0
- 推荐数: 0
其他信息
- 授权协议: Apache-2.0
- 更新时间: 2026-07-07