diff --git a/README.md b/README.md index a972776..6edcf98 100644 --- a/README.md +++ b/README.md @@ -8,10 +8,10 @@ A single binary that: -- **Accepts a prompt and returns a result** (Mode A, single-turn): `amplifier-agent run "your prompt"` -- **Speaks JSON-RPC over stdio for multi-turn conversations** (Mode B): `amplifier-agent run --stdio` +- **Accepts a prompt and returns a result** (single-turn): `amplifier-agent run "your prompt"` +- **Emits one JSON envelope on stdout per invocation** — wrappers spawn one process per turn and pass `--session-id` for continuity -It is *not* a server, daemon, or long-lived service. Each invocation is a fresh process that exits when its caller closes stdin or sends `agent/shutdown`. +It is *not* a server, daemon, or long-lived service. Each invocation is a fresh process that runs one turn and exits. Multi-turn conversations are managed at the wrapper or session-ID layer — not inside a persistent process. The engine library inside (`amplifier_agent_lib`) is transport-free Python that any Python app can also embed in-process — no subprocess needed. @@ -19,19 +19,24 @@ The engine library inside (`amplifier_agent_lib`) is transport-free Python that Existing AI agent infrastructure assumes you're building a chat product. `amplifier-agent` is the opposite: it's an *engine you point other software at*. The CLI is the universal adapter — wherever you can shell out, you can use Amplifier. -The wire protocol intentionally mirrors [MCP](https://modelcontextprotocol.io/) (JSON-RPC over stdio, server-initiated bidirectional requests, capability negotiation) so existing host clients can integrate with minimal new infrastructure. +The Mode A wire protocol is intentionally simple: the engine takes a single invocation (argv + env), runs one turn, and writes one JSON result envelope to stdout. Wrapper SDKs (TypeScript and Python) handle spawning, result parsing, and session continuity on top. ## Install +The Python engine is not yet published to a package registry, but `uv tool install` installs it directly from git: + ```bash -uv tool install amplifier-agent +uv tool install git+https://github.com/microsoft/amplifier-agent amplifier-agent doctor # verify environment ``` -Other install methods: +Pin to a specific engine release by appending a tag: + +```bash +uv tool install git+https://github.com/microsoft/amplifier-agent@engine-v0.3.0 +``` -- `pipx install amplifier-agent` -- From source: `git clone … && cd amplifier-agent && uv sync && uv tool install -e .` +Engine and wrapper releases are tagged separately (`engine-v0.3.0`, `wrapper-v0.4.0`). For local development against a checkout, `git clone` the repo and run `uv tool install -e .` from inside it. First-run will prepare the built-in bundle and cache it to `$XDG_CACHE_HOME/amplifier-agent/`. Subsequent invocations skip this step. @@ -49,12 +54,7 @@ Run a one-shot turn: amplifier-agent run "Summarize the README of github.com/microsoft/amplifier" ``` -Or wire it into a host as a JSON-RPC subprocess: - -```bash -amplifier-agent run --stdio -# Then write JSON-RPC requests to stdin, read events from stdout -``` +The TypeScript and Python wrapper SDKs handle subprocess management automatically. See `wrappers/typescript/` (`amplifier-agent-ts` on npm) and `wrappers/python/` for ready-to-use clients. ## Provider configuration @@ -71,10 +71,9 @@ Override with `--provider `. No `settings.yaml` to maintain. | Mode | Invocation | Caller | Lifecycle | |---|---|---|---| -| **A** (single-turn) | `amplifier-agent run "prompt"` | Shell scripts, OpenClaw skills, ad-hoc CLI use | Spawn → init → one turn → exit | -| **B** (multi-turn stdio) | `amplifier-agent run --stdio` | Wrapper SDKs, conversational host adapters | Spawn → init → many turns → exit on EOF | +| **A** (single-turn) | `amplifier-agent run "prompt"` | Shell scripts, wrapper SDKs, host adapters, ad-hoc CLI use | Spawn → one turn → JSON envelope on stdout → exit | -Both modes share the same engine; the only difference is who drives the I/O loop. +Multi-turn conversations are built by the caller: spawn one process per turn, passing `--session-id` for continuity. A persistent stdio JSON-RPC mode (Mode B) was originally designed but was superseded by the Mode A subprocess driver in PR #8 (see `docs/designs/2026-05-24-aaa-v2-mode-a-pivot-amendment.md`). ## Session continuity @@ -95,6 +94,8 @@ Sessions are persisted as transcript JSONL in `$XDG_STATE_HOME/amplifier-agent/s ```bash amplifier-agent doctor # Diagnose env, providers, paths, bundle cache +amplifier-agent prepare # Pre-warm bundle cache (run once after install) +amplifier-agent verify # Verify install integrity amplifier-agent config show # Print resolved config with source annotations amplifier-agent cache clear # Invalidate the prepared-bundle cache amplifier-agent --version # Print version @@ -102,31 +103,86 @@ amplifier-agent --version # Print version ## Approval flow -Some tools (file writes, command execution) request approval before acting. In single-turn mode: +Some tools (file writes, command execution) request approval before acting: -- **Interactive terminal**: prompted on stderr; respond `y` / `N` / `c` +- **Interactive terminal**: prompted on stderr; respond `y` to approve, anything else to decline - **Non-interactive (CI, pipe, background)**: denied by default - **Override**: `-y` accepts all, `-n` denies all (apt-style) -In stdio mode, approval flows over the wire as `approval/request` server-initiated JSON-RPC requests. Wrapper SDKs implement the host-side handler (callback, message-back, email, or anything else creative — adapter's choice). +Wrapper SDKs can install their own approval handler (callback, message-back, email, or anything else creative — adapter's choice) via the `ApprovalSystem` protocol point on the engine library. ## Embedding in your own Python host Skip the CLI entirely if your host is Python: ```python -from amplifier_agent_lib import Engine +import sys +from amplifier_agent_lib import __version__ +from amplifier_agent_lib._runtime import make_turn_handler +from amplifier_agent_lib.bundle.cache import load_and_prepare_cached +from amplifier_agent_lib.engine import Engine +from amplifier_agent_lib.protocol import PROTOCOL_VERSION from amplifier_agent_lib.protocol_points.defaults_cli import CliApprovalSystem, CliDisplaySystem -engine = await Engine.boot( - approval_system=CliApprovalSystem(mode="auto"), - display_system=CliDisplaySystem(verbosity="normal"), +prepared = await load_and_prepare_cached(aaa_version=__version__) +handler = make_turn_handler(prepared, cwd=None, is_resumed=False, mcp_config_path=None) + +engine = Engine( + turn_handler=handler, + protocol_points={ + "approval": CliApprovalSystem(mode="no"), + "display": CliDisplaySystem(verbosity="normal", stream=sys.stderr), + }, ) -result = await engine.submit_turn(prompt="Hello!", session_id="my-session") +await engine.boot({ + "protocolVersion": PROTOCOL_VERSION, + "clientInfo": {"name": "my-host", "version": "0.1.0"}, + "capabilities": {}, + "sessionId": "my-session", + "resume": False, +}) +result = await engine.submit_turn({ + "sessionId": "my-session", + "turnId": "turn-1", + "prompt": "Hello!", +}) await engine.shutdown() ``` -See `src/amplifier_agent_lib/` for the full library surface. +`Engine.boot()` is an instance method that takes a params dict. The constructor requires both `turn_handler` (built from a `PreparedBundle`) and the `protocol_points` dict. See `src/amplifier_agent_lib/` for the full library surface. + +## TypeScript / Node.js SDK + +For Node.js and TypeScript hosts, use the `amplifier-agent-ts` npm package. It is a thin process supervisor that spawns the Python `amplifier-agent` CLI per turn (Mode A) and exposes a typed async API — all inference, tool execution, and session state live in the Python engine. + +You need **both** packages installed: the npm SDK *and* the Python engine (see [Install](#install) above — the Python CLI must be on `PATH`). + +```bash +npm install amplifier-agent-ts +``` + +```typescript +import { spawnAgent, AaaError } from 'amplifier-agent-ts'; +import { randomUUID } from 'node:crypto'; + +const session = await spawnAgent({ + lifecycle: 'one-shot', + sessionId: randomUUID(), +}); + +try { + const result = await session.submit({ prompt: 'Hello, agent.' }); + console.log(result.reply); +} catch (err) { + if (err instanceof AaaError) { + console.error(`[${err.code}] ${err.message}`); + } else { + throw err; + } +} +``` + +Requires Node.js ≥ 20. Zero npm runtime dependencies. Full API surface in [`wrappers/typescript/README.md`](wrappers/typescript/README.md) and the type definitions at `wrappers/typescript/dist/index.d.ts`. The Python sibling SDK lives at [`wrappers/python/`](wrappers/python/). ## Architecture at a glance @@ -138,7 +194,7 @@ Host Application ← your code Adapter (host-specific glue) ← per-host integration ↓ Language Wrapper (TypeScript or Python) ← typed SDK - ↓ JSON-RPC over stdio (or in-process) + ↓ subprocess (argv in / JSON envelope out, or in-process) amplifier-agent CLI ← this repo ↓ (in-process) amplifier_agent_lib (engine library) ← this repo @@ -148,22 +204,46 @@ Amplifier Kernel (amplifier-core, amplifier-foundation) The CLI binary (`amplifier-agent`) is a thin I/O adapter on top of `amplifier_agent_lib`. The library is transport-free — Python hosts can skip the subprocess entirely. -## Wire protocol (Mode B) +## Wire protocol (Mode A) + +Protocol version: **`0.2.0`** (defined in `src/amplifier_agent_lib/protocol/methods.py`; breaking changes bump this). Wrappers must pass `--protocol-version 0.2.0` — version mismatches return a `protocol_version_mismatch` error and exit non-zero rather than silently misbehave. -Mode B speaks JSON-RPC 2.0 over newline-delimited stdin/stdout: +Mode A uses a single subprocess invocation per turn. The wrapper passes flags as argv; the engine writes one JSON envelope line to stdout on completion. -| Method | Direction | Purpose | +**Input (selected argv flags):** + +| Flag | Type | Purpose | |---|---|---| -| `agent/initialize` | Host → Agent | Capability negotiation | -| `session/create` | Host → Agent | Open a new session | -| `turn/submit` | Host → Agent | Submit a turn; agent streams notifications + returns result | -| `turn/cancel` | Host → Agent | Cancel an in-flight turn | -| `session/end` | Host → Agent | Close session and persist state | -| `agent/shutdown` | Host → Agent | Graceful exit | -| `approval/request` | Agent → Host | Request approval for a sensitive action | -| `notifications/*` | Agent → Host | Streaming events (`result/delta`, `result/final`, `tool/started`, `tool/completed`, `progress`, `thinking/*`, `usage`, `error`) | +| `PROMPT` | positional | The turn prompt | +| `--session-id` | str | Session ID for continuity | +| `--resume` | flag | Resume from saved transcript | +| `--fresh` | flag | Discard saved state and start over | +| `--protocol-version` | str | Wrapper's pinned protocol version; engine validates match | +| `--mcp-config-path` | path | Path to MCP server config JSON (written by the wrapper) | +| `--host-capabilities` | JSON | Host capability advertisement | +| `-y` / `-n` | flag | Auto-approve / auto-deny all approval requests | +| `--output` | text \| json | `json` (default) emits full envelope; `text` emits the reply only | + +**Output (stdout, single JSON line):** + +```json +{ + "protocolVersion": "0.2.0", + "sessionId": "...", + "turnId": "turn-1", + "reply": "...", + "error": null, + "metadata": { + "tokensIn": 0, "tokensOut": 0, "durationMs": 0, + "bundleDigest": "...", "engineVersion": "...", + "protocolVersion": "0.2.0", "correlationId": "..." + } +} +``` + +Diagnostic events (tool calls, thinking, progress) go to **stderr** only — stdout is reserved for the single envelope so callers can parse it with `JSON.parse(line)` without filtering. -Notifications are one-way (no `id`). Server-initiated requests use the same ID-correlation as host-initiated ones, just in reverse. +The TypeScript and Python wrapper SDKs (`wrappers/typescript/`, `wrappers/python/`) handle all of this: they spawn `amplifier-agent run`, write the MCP config tmpfile, parse the envelope, and expose a typed async API. ## Related repositories @@ -176,13 +256,21 @@ Notifications are one-way (no `id`). Server-initiated requests use the same ID-c ## Status -Phase 1 — Layer 4 (engine library + CLI) ships in this release. Roadmap: +Current versions: engine `0.3.0`, TypeScript wrapper `amplifier-agent-ts@0.4.0`, Python wrapper `amplifier_agent_client` (see `wrappers/python/`). Wire protocol: `0.2.0`. + +**Shipped:** + +- L4 — Engine library + CLI (Mode A) +- L3 — TypeScript + Python wrapper SDKs (`amplifier-agent-ts` on npm, `amplifier_agent_client` in `wrappers/python/`) +- Protocol schemas + cross-language conformance test suite (`wrappers/conformance/`) +- Path-based MCP config delivery (`--mcp-config-path`) + +**In progress / next:** -- L3 language wrappers (TypeScript + Python SDKs) — designed, implementation next -- L2 host adapters (NanoClaw, Paperclip) — designed, implementation after L3 -- Install paths, container packaging, full execution plan — deferred sections of the design checkpoint +- L2 — Host adapters (NanoClaw, Paperclip) — see `docs/designs/2026-05-22-aaa-v2-amplifier-agent-nc-provider.md` +- Container packaging, install-path finalization -See [`docs/designs/`](docs/designs/) and the [pull requests](https://github.com/microsoft/amplifier-agent/pulls) for design history and roadmap. +See [`docs/designs/`](docs/designs/) and the [pull requests](https://github.com/microsoft/amplifier-agent/pulls) for design history and roadmap. The Mode A pivot is captured in [`docs/designs/2026-05-24-aaa-v2-mode-a-pivot-amendment.md`](docs/designs/2026-05-24-aaa-v2-mode-a-pivot-amendment.md). ## Contributing