diff --git a/docs/typescript/advanced.md b/docs/typescript/advanced.md new file mode 100644 index 0000000..36a2ac1 --- /dev/null +++ b/docs/typescript/advanced.md @@ -0,0 +1,341 @@ +# Advanced + +Beyond the basic spawn-and-iterate pattern: approval handling, MCP servers, env allowlist, custom binary paths, listing models, manual subprocess control. + +## Environment allowlist + +The SDK passes a **filtered** environment to the engine subprocess. Only these are forwarded: + +1. Keys with `AMPLIFIER_` prefix (always). +2. Keys with `LC_` prefix (always). +3. Keys you list in `env.allowlist`. +4. Keys you set in `env.extra` (last writer wins, but must not be in `BLOCKED_ENV_KEYS`). + +### `DEFAULT_ALLOWLIST` + +```ts +const DEFAULT_ALLOWLIST = ['PATH', 'HOME', 'USER', 'LANG', 'TERM', 'TMPDIR']; +``` + +`PATH`, `HOME`, and `USER` are essential. The others are widely-needed but not required. + +### Provider keys are NOT in the default allowlist + +`ANTHROPIC_API_KEY`, `OPENAI_API_KEY`, `AZURE_OPENAI_API_KEY`, `OLLAMA_HOST` — none of these are `AMPLIFIER_*` prefixed, so they are **not** passed through unless you add them: + +```ts +await spawnAgent({ + // ... + env: { + allowlist: [ + ...DEFAULT_ALLOWLIST, + 'ANTHROPIC_API_KEY', // forward the host's anthropic key + 'OPENAI_API_KEY', // ...and openai + ], + }, +}); +``` + +Or pass them via `extra` if you want to provide them explicitly: + +```ts +await spawnAgent({ + // ... + env: { + allowlist: DEFAULT_ALLOWLIST, + extra: { + ANTHROPIC_API_KEY: process.env.ANTHROPIC_API_KEY!, + }, + }, +}); +``` + +### Blocked keys + +`BLOCKED_ENV_KEYS` cannot be set via `env.extra` — attempting to do so throws `AaaError('env_injection_rejected')`: + +```ts +const BLOCKED_ENV_KEYS = new Set([ + 'PYTHONPATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'PYTHONSTARTUP', + 'PATH', 'PYTHONHOME', 'PYTHONNOUSERSITE', + 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH', +]); +``` + +These are dynamic-linker hooks that could be used for code injection into the engine subprocess. The SDK refuses to pass them through `extra`. (`PATH` is blocked from `extra` only — it's a normal allowlist key and is forwarded from the caller's env by default.) + +If you really need to set one — and you almost never do — the workaround is to set it in the parent process's env before spawn, *and* add it to `allowlist`. This requires both an explicit grant from the caller and from the wrapper, and that's the point. + +--- + +## Custom binary paths + +The SDK resolves the engine binary in this order: + +1. `AMPLIFIER_AGENT_BIN` env var (if set, used verbatim — even if the path doesn't exist on disk, so the error is descriptive). +2. `which amplifier-agent` via the shell. +3. Throws `Error('binary_not_found')`. + +### Override per-call + +```ts +import { resolveBinaryPath, spawnAgent } from 'amplifier-agent-ts'; + +const customBin = '/opt/my-amplifier-agent/bin/amplifier-agent'; + +const handle = await spawnAgent({ + // ... + env: { + allowlist: DEFAULT_ALLOWLIST, + extra: { AMPLIFIER_AGENT_BIN: customBin }, + }, +}); +``` + +### Override globally + +```bash +export AMPLIFIER_AGENT_BIN=/path/to/custom/amplifier-agent +``` + +Then all SDK calls in that process pick it up via the default `process.env`. + +### Test/sandbox injection + +For tests, you can replace the resolver and the version probe directly: + +```ts +await spawnAgent({ + // ... + _binaryResolver: () => '/fake/bin/amplifier-agent', + _engineVersionProbe: async () => ({ version: '0.5.2', protocolVersion: '0.3.0' }), +}); +``` + +These leading-underscore parameters are part of the public type but are documented as test-only. + +--- + +## Approval + +The `approval.mode` field controls how the engine handles tool calls. + +```ts +type ApprovalMode = 'yes' | 'no' | 'prompt'; + +await spawnAgent({ + // ... + approval: { mode: 'yes' }, +}); +``` + +| Mode | Engine flag | Behavior | +|---|---|---| +| `'yes'` | `-y` | Auto-approve every tool call. | +| `'no'` | `-n` | Auto-deny every tool call. | +| `'prompt'` | (none) | Defer to engine's policy resolution: host_config.approval.mode → TTY check → fail. | +| `undefined` | `-y` | Historical default for backward compat. Will change. **Set this explicitly.** | + +### Mid-turn approval prompts (not yet supported) + +The protocol reserves space for a future approval channel where the engine pauses and asks the host to approve each tool call interactively. The SDK type accepts `approval.onRequest`, but **currently rejects** it at spawn time: + +```ts +await spawnAgent({ + approval: { mode: 'prompt', onRequest: handler }, +}); +// → throws AaaError; mid-turn channel is not implemented in v1 +``` + +The `makeApprovalHandler()` and `ApprovalAdapter`/`ApprovalRequest`/`ApprovalResponse` types exist for forward compatibility. Until the channel ships, use `'yes'`, `'no'`, or `'prompt'` (the latter relying on `host_config.approval.mode`). + +--- + +## MCP servers + +You can configure [MCP](https://modelcontextprotocol.io/) servers per-call by passing `mcpServers`: + +```ts +await spawnAgent({ + // ... + mcpServers: { + 'my-server': { + command: 'node', + args: ['/path/to/server.js'], + env: { /* server-side env */ }, + }, + 'other-server': { command: 'python', args: ['-m', 'my_mcp_server'] }, + }, +}); +``` + +The SDK: + +1. Writes the configuration as JSON to a `0600` tempfile. +2. Sets `AMPLIFIER_MCP_CONFIG=` in the subprocess env. +3. Unlinks the tempfile when you call `cancel()` or when the subprocess exits. + +If you prefer to manage the file yourself, set `AMPLIFIER_MCP_CONFIG` directly via `env.extra` (and skip `mcpServers`). + +### `resolveMcpConfigPath` / `cleanupSpillFile` + +These helpers let you do the spill yourself: + +```ts +import { resolveMcpConfigPath, cleanupSpillFile } from 'amplifier-agent-ts'; + +const { path, cleanup } = await resolveMcpConfigPath({ + servers: { 'my-server': { command: 'node', args: ['server.js'] } }, +}); + +try { + // Use `path` as you wish, e.g. set AMPLIFIER_MCP_CONFIG yourself. +} finally { + await cleanup(); // or cleanupSpillFile(path) +} +``` + +--- + +## Listing models + +```ts +import { listModels } from 'amplifier-agent-ts'; + +// One provider, full catalog +const result = await listModels({ provider: 'anthropic', timeoutMs: 15000 }); +console.log(result.provider, '→', result.models.length, 'models'); +console.log(result.models.map(m => m.id)); + +// One provider, latest per family only +const latest = await listModels({ provider: 'anthropic', latest: true }); + +// All providers in parallel (aggregate envelope) +const all = await listModels({ timeoutMs: 30000 }); +for (const entry of all.results) { + console.log(entry.provider, entry.status, entry.models?.length ?? 0); +} +``` + +Same shape as `amplifier-agent models list --output json` — see [CLI: models list](../user/cli-reference.md#models-list). + +Throws `ListModelsError` if the underlying CLI call fails (provider down, binary missing, malformed JSON). + +--- + +## Protocol version skew + +`spawnAgent` aborts with `AaaError('protocol_version_mismatch')` if the engine reports a wire protocol version different from `PROTOCOL_VERSION_REQUIRED_BY_WRAPPER` (currently `"0.3.0"`). + +Override in dev: + +```ts +await spawnAgent({ + // ... + allowProtocolSkew: true, +}); +``` + +Or via env (read by the engine boot path itself): + +```bash +export AMPLIFIER_AGENT_ALLOW_PROTOCOL_SKEW=1 +``` + +(or put `"allowProtocolSkew": true` in your host config). + +Don't ship this in production. Mismatch means the wrapper and engine may disagree on event shapes, error codes, or argv flags. + +--- + +## Manual subprocess control + +If you want to bypass `spawnAgent` and `SessionHandle`, the building blocks are public: + +```ts +import { + assembleArgv, + resolveBinaryPath, + buildEnv, + probeEngineVersion, + checkProtocolVersion, + parseRunOutput, + parseNdjsonStream, + PROTOCOL_VERSION_REQUIRED_BY_WRAPPER, + DEFAULT_ALLOWLIST, +} from 'amplifier-agent-ts'; +import { spawn } from 'child_process'; + +const bin = resolveBinaryPath(); +const env = buildEnv({ + processEnv: process.env, + allowlist: [...DEFAULT_ALLOWLIST, 'ANTHROPIC_API_KEY'], +}); + +// Probe & check +const probed = await probeEngineVersion(bin, env); +const check = checkProtocolVersion({ + wrapper: PROTOCOL_VERSION_REQUIRED_BY_WRAPPER, + engine: probed.protocolVersion, + allowSkew: false, +}); +if (!check.ok) { + throw new Error(check.remediation); +} + +// Build argv +const argv = assembleArgv({ + sessionId: 'manual-1', + resume: false, + protocolVersion: PROTOCOL_VERSION_REQUIRED_BY_WRAPPER, + displayMode: 'ndjson', + approvalMode: 'yes', + prompt: 'Hello', +}); + +// Spawn +const child = spawn(bin, argv, { env, stdio: ['ignore', 'pipe', 'pipe'] }); + +// Consume NDJSON from stderr +for await (const note of parseNdjsonStream(child.stderr, { + onParseError: (raw, e) => console.warn('drop', raw), +})) { + console.log('wire:', note); +} + +// Wait for exit and parse stdout +let stdout = ''; +child.stdout.on('data', (d) => { stdout += d; }); +const exitCode = await new Promise((res) => + child.on('exit', (c) => res(c ?? 0)), +); + +// Build a DisplayEvent from the outcome +const ev = parseRunOutput({ + stdout, stderr: '', exitCode, signal: null, +}); +console.log('Final:', ev); +``` + +This is what `SessionHandle.submit()` does internally. The public helpers exist so hosts with unusual requirements (sandboxed spawns, custom logging, alternative event pipelines) can compose their own driver while reusing the SDK's argv discipline and parsing. + +--- + +## Timeouts + +`timeoutMs` caps a single `submit()` call's wall-clock duration. + +```ts +await spawnAgent({ + // ... + timeoutMs: 60_000, // 60s +}); +``` + +| Value | Behavior | +|---|---| +| `undefined` or `0` | No timeout. (The CLI itself has no per-turn cap.) | +| `DEFAULT_TIMEOUT_MS` (600_000 / 10 min) | The conservative recommended cap for interactive UIs. | +| `N > 0` | After `N` ms, the SDK calls `cancel()` and the `submit()` iterator emits an `error` event with `code: 'timeout'` (or similar) before ending. | + +The CLI itself does not enforce a timeout. The SDK enforces it by SIGTERM-ing the subprocess. diff --git a/docs/typescript/api-reference.md b/docs/typescript/api-reference.md new file mode 100644 index 0000000..6d1aa4e --- /dev/null +++ b/docs/typescript/api-reference.md @@ -0,0 +1,369 @@ +# TypeScript SDK API reference + +Every public export from `amplifier-agent-ts`. Types are paraphrased — see the package's `.d.ts` for canonical signatures. + +## Entry point + +```ts +import { + // Main API + spawnAgent, SessionHandle, AaaError, + // Constants + PROTOCOL_VERSION_REQUIRED_BY_WRAPPER, + DEFAULT_TIMEOUT_MS, + STDERR_TAIL_BYTES, + DEFAULT_ALLOWLIST, + BLOCKED_ENV_KEYS, + // Helpers + assembleArgv, + resolveBinaryPath, + buildEnv, + probeEngineVersion, + checkProtocolVersion, + parseRunOutput, + parseNdjsonStream, + Transport, + makeApprovalHandler, + listModels, ListModelsError, + resolveMcpConfigPath, cleanupSpillFile, +} from 'amplifier-agent-ts'; +``` + +## Constants + +| Constant | Value | Source | +|---|---|---| +| `PROTOCOL_VERSION_REQUIRED_BY_WRAPPER` | `"0.3.0"` | `index.ts` | +| `DEFAULT_TIMEOUT_MS` | `600000` (10 min) | `session.ts` | +| `STDERR_TAIL_BYTES` | `4096` | `run-output-parser.ts` | +| `DEFAULT_ALLOWLIST` | `['PATH', 'HOME', 'USER', 'LANG', 'TERM', 'TMPDIR']` | `spawn.ts` | +| `BLOCKED_ENV_KEYS` | `Set` of `['PYTHONPATH', 'LD_PRELOAD', 'LD_LIBRARY_PATH', 'PYTHONSTARTUP', 'PATH', 'PYTHONHOME', 'PYTHONNOUSERSITE', 'DYLD_INSERT_LIBRARIES', 'DYLD_LIBRARY_PATH']` | `spawn.ts` | + +--- + +## `spawnAgent(params): Promise` + +Validates parameters, resolves the engine binary, probes its version, builds the subprocess env, and constructs a `SessionHandle`. **No subprocess is spawned.** + +```ts +interface SpawnAgentParams { + lifecycle: 'one-shot'; // only value supported + sessionId: string; // required + resume: boolean; // required — true = --resume, false = --fresh + cwd?: string; + configPath?: string; // forwarded as --config + approval?: { + mode?: 'yes' | 'no' | 'prompt'; + // (other fields reserved for future approval-channel work) + }; + displayMode?: 'text' | 'ndjson'; // set to 'ndjson' to receive 'notification' events + workspace?: string; + mcpServers?: Record; // see Advanced + timeoutMs?: number; // wall-clock cap; 0/undefined = no timeout + allowProtocolSkew?: boolean; + env: { + allowlist: string[]; // required + extra?: Record; + }; + // Test/sandbox injection points (advanced): + runChildProcess?: ChildProcessFactory; + _binaryResolver?: () => string; + _engineVersionProbe?: (bin: string, env: Record) => Promise; +} +``` + +### Behavior + +1. Reject `approval.onRequest` (mid-turn callbacks not supported in v1). +2. Validate `lifecycle === 'one-shot'`. +3. Resolve the binary: `_binaryResolver` → `resolveBinaryPath()`. +4. Build the subprocess env via `buildEnv()`. +5. Probe the engine: `probeEngineVersion(binary, env)`. +6. `checkProtocolVersion({ wrapper: '0.3.0', engine: , allowSkew })`. Throws `AaaError('protocol_version_mismatch', ...)` on fail unless `allowProtocolSkew: true`. +7. Construct and return a `SessionHandle`. + +### Throws + +- `AaaError('protocol_version_mismatch', ...)` — engine and wrapper disagree on protocol version. +- `AaaError('env_injection_rejected', ...)` — caller passed a blocked env key in `env.extra`. +- `AaaError('binary_not_found', ...)` — no engine on PATH and no `AMPLIFIER_AGENT_BIN`. +- Any error thrown by `probeEngineVersion()` if the engine is broken. + +--- + +## `SessionHandle` + +```ts +class SessionHandle { + submit(prompt: string): AsyncIterable; + cancel(): Promise; + dispose(): Promise; // alias for cancel() + getEngineInfo(): EngineInfo; +} + +interface EngineInfo { + binaryPath: string; + protocolVersion: string; + engineVersion: string; + bundleDigest: string; +} +``` + +### `submit(prompt)` + +Spawns the engine subprocess and yields events. **One-shot** — calling `submit()` a second time on the same handle throws. + +Yields events in this rough order: + +1. `init` — yielded **before** the subprocess spawn so callers can see the resolved session ID. +2. `notification` events from the engine's stderr (only when `displayMode: 'ndjson'`). +3. `activity` events every ~2 seconds while the engine is alive. +4. Either a single `result` event (success) or a single `error` event (failure). + +The iterator ends when the subprocess exits. + +### `cancel()` + +Sends `SIGTERM` to the engine's process group. Five seconds later, sends `SIGKILL` if still alive. Removes any temp files written for `mcpServers` (see Advanced). + +`dispose()` is an alias. + +### `getEngineInfo()` + +Returns the metadata captured during `spawnAgent()`. `bundleDigest` is reserved and currently empty. + +--- + +## `DisplayEvent` + +A discriminated union yielded by `submit()`: + +```ts +type DisplayEvent = + | { type: 'init'; sessionId: string } + | { type: 'activity' } + | { type: 'result'; text: string } + | { type: 'error'; + code: string; + classification: 'engine' | 'protocol' | 'approval' | 'transport' | 'unknown'; + severity: 'error'; + correlationId: string; + message: string; + stderrTail?: string; + retryable?: boolean; + } + | { type: 'notification'; method: string; params: unknown }; +``` + +See [Events](events.md) for examples of every event type. + +--- + +## `AaaError` + +```ts +class AaaError extends Error { + code: string; + classification: 'engine' | 'protocol' | 'approval' | 'transport' | 'unknown'; + severity: 'error'; + correlationId?: string; + remediation?: string; + stderrTail?: string; + + constructor( + code: string, + message: string, + opts?: { classification, severity, correlationId?, remediation?, stderrTail? } + ); +} +``` + +Thrown from synchronous validation paths (`spawnAgent`, `buildEnv`, etc.). For runtime errors during a `submit()`, you receive an `error` `DisplayEvent` instead. + +--- + +## Helper functions + +These are the lower-level building blocks. Use them if you want to drive the engine yourself or test in isolation. + +### `assembleArgv(input): string[]` + +Build the canonical argv vector. + +```ts +interface AssembleArgvInput { + sessionId: string; + resume: boolean; // true → --resume, false → --fresh + cwd?: string; + configPath?: string; + protocolVersion: string; + displayMode?: 'text' | 'ndjson'; + workspace?: string; + approvalMode?: 'yes' | 'no' | 'prompt'; // 'prompt' emits neither -y nor -n; undefined defaults to 'yes' + prompt: string; +} +``` + +Example: + +```ts +assembleArgv({ + sessionId: 's1', resume: false, protocolVersion: '0.3.0', + approvalMode: 'yes', prompt: 'hi', +}) +// → [ +// 'run', '--session-id', 's1', '--fresh', +// '--output', 'json', '--protocol-version', '0.3.0', +// '-y', 'hi' +// ] +``` + +The canonical order is: `run`, `--session-id`, ``, `--resume|--fresh`, `[--cwd]`, `[--config]`, `--output json`, `--protocol-version`, ``, `[--display]`, `[--workspace]`, `[-y|-n]`, ``. + +### `resolveBinaryPath(opts?): string` + +```ts +interface ResolveBinaryPathOptions { + env?: Record; // defaults to process.env +} +``` + +Resolution order: + +1. `env.AMPLIFIER_AGENT_BIN` if set (returned even if path doesn't exist on disk, so caller can produce a useful error). +2. `which amplifier-agent` via shell. +3. Throws `Error` with `code: 'binary_not_found'`. + +### `buildEnv(opts): Record` + +```ts +interface BuildEnvOptions { + processEnv: Record; // typically process.env + allowlist: string[]; // exact-match keys to pass through + extra?: Record; // merged last; throws on BLOCKED_ENV_KEYS +} +``` + +Passes through: keys in `allowlist`, keys with `AMPLIFIER_` prefix, keys with `LC_` prefix. `extra` is merged last (overrides allowlisted values). Throws `AaaError('env_injection_rejected')` if `extra` contains any key in `BLOCKED_ENV_KEYS`. + +### `probeEngineVersion(bin, env, timeoutMs?): Promise` + +```ts +interface EngineVersionPayload { + version: string; + protocolVersion: string; +} +``` + +Spawns `amplifier-agent version --json` with a 5-second default timeout. Returns the parsed payload or throws. + +### `checkProtocolVersion(opts): VersionCheckResult` + +```ts +interface CheckProtocolVersionOptions { + wrapper: string; // e.g. PROTOCOL_VERSION_REQUIRED_BY_WRAPPER + engine: string; + allowSkew: boolean; +} + +type VersionCheckResult = + | { ok: true } + | { ok: false; code: 'protocol_version_mismatch'; remediation: string }; +``` + +When `allowSkew` is true, always returns `{ ok: true }`. When false, returns `{ ok: false, ... }` if the strings don't match exactly. + +### `parseRunOutput(outcome): DisplayEvent` + +```ts +interface SubprocessOutcome { + stdout: string; + stderr: string; + exitCode: number | null; + signal?: NodeJS.Signals | null; +} +``` + +Parses the captured stdout envelope. If the envelope is valid, returns a `result` or `error` event based on the envelope's `error` field. If the envelope is missing or malformed, synthesizes an `error` event from exit code and the last `STDERR_TAIL_BYTES` (4096) of stderr. + +### `parseNdjsonStream(stream, options): AsyncIterable<...>` + +```ts +interface ParseNdjsonStreamOptions { + onParseError?: (raw: string, err: Error) => void; +} +``` + +Reads a Node `Readable` and yields parsed JSON objects, one per line. Malformed lines invoke `onParseError` (default: silent skip) and are dropped. + +### `Transport` + +A lower-level wrapper around the engine subprocess that exposes raw stdout/stderr streams. Use only if you need to bypass the default event-loop pipeline. + +```ts +class Transport { + constructor(opts: TransportOptions); + start(): Promise; + stop(): Promise; + stdout: NodeJS.ReadableStream; + stderr: NodeJS.ReadableStream; +} +``` + +### `makeApprovalHandler(adapter): ApprovalHandler` + +Reserved for future approval channel work. Currently constructs an opaque handler. See [Advanced: approval](advanced.md#approval). + +### `listModels(params): Promise` + +```ts +interface ListModelsParams { + provider?: 'anthropic' | 'openai' | 'azure-openai' | 'ollama'; // omit for aggregate + latest?: boolean; + timeoutMs?: number; + // ...same env/binary injection as spawnAgent +} +``` + +Invokes `amplifier-agent models list --output json [...flags]` and parses the result. Returns the same single-provider or aggregate envelope documented in [CLI: models list](../user/cli-reference.md#models-list). + +Throws `ListModelsError` if the underlying CLI call exits non-zero or emits malformed JSON. + +### `resolveMcpConfigPath(opts) / cleanupSpillFile(path)` + +Spill a runtime `mcpServers` object to a `0600` tempfile so the engine can read it via `AMPLIFIER_MCP_CONFIG`. See [Advanced: MCP](advanced.md#mcp-servers). + +--- + +## Public types (summary) + +```ts +type DisplayEvent = ...; // discriminated union, see above +type ApprovalResponse = ...; +type EngineVersionPayload = ...; +type EngineInfo = ...; +type McpSpillResult = ...; +type McpServerConfig = ...; +type ListModelsParams = ...; +type ModelInfo = ...; +type ModelsListEnvelope = ...; +type TransportOptions = ...; +type ExitInfo = ...; +type ParseNdjsonStreamOptions = ...; +type ResolveBinaryPathOptions = ...; +type BuildEnvOptions = ...; +type ApprovalAdapter = ...; +type ApprovalRequest = ...; +type ApprovalHandler = ...; +type ChildProcessFactory = ...; +type AssembleArgvInput = ...; +type SubprocessOutcome = ...; +type VersionCheckOk = ...; +type VersionCheckFail = ...; +type VersionCheckResult = VersionCheckOk | VersionCheckFail; +type CheckProtocolVersionOptions = ...; +type SessionHandleParams = ...; +type SpawnAgentParams = ...; // see top of this file +``` + +For exact shapes, see `dist/*.d.ts` in the installed package. diff --git a/docs/typescript/events.md b/docs/typescript/events.md new file mode 100644 index 0000000..890ad46 --- /dev/null +++ b/docs/typescript/events.md @@ -0,0 +1,224 @@ +# Events + +`SessionHandle.submit()` yields a stream of `DisplayEvent` objects — a discriminated union of five types. + +```ts +type DisplayEvent = + | { type: 'init'; sessionId: string } + | { type: 'activity' } + | { type: 'result'; text: string } + | { type: 'error'; code: string; classification: string; severity: 'error'; + correlationId: string; message: string; stderrTail?: string; + retryable?: boolean } + | { type: 'notification'; method: string; params: unknown }; +``` + +## Event ordering + +Every `submit()` yields events in roughly this order: + +``` +init ← yielded before the subprocess spawn +notification (many) ← if displayMode: 'ndjson' + - usage + - tool/started, tool/completed + - thinking/delta, thinking/final + - result/delta, result/final + - usage (session total) +activity ← every ~2 seconds while subprocess alive +result OR error ← terminal — iterator ends after this +``` + +The iterator ends when the engine subprocess exits. After a `result` or `error` event, no further events are emitted. + +--- + +## `init` + +```ts +{ type: 'init', sessionId: string } +``` + +Yielded **before** the engine subprocess is spawned. Useful so your UI can show "Session s1 starting…" without waiting for the engine to boot. + +The `sessionId` field is exactly what you passed in `SpawnAgentParams.sessionId`. + +```ts +for await (const event of handle.submit('Hello')) { + if (event.type === 'init') { + console.log('Session:', event.sessionId); + } +} +``` + +--- + +## `activity` + +```ts +{ type: 'activity' } +``` + +A keep-alive heartbeat. Yielded every ~2 seconds while the engine subprocess is alive. Use to keep your UI animated and to detect freezes. + +`activity` carries no payload. The fact that it arrives is the signal. + +```ts +let lastActivity = Date.now(); +for await (const event of handle.submit('Long task')) { + if (event.type === 'activity') { + lastActivity = Date.now(); + updateProgressIndicator(); + } +} +``` + +--- + +## `result` + +```ts +{ type: 'result', text: string } +``` + +Terminal event. The engine completed successfully and produced `text` as the reply. + +This is the same `reply` field from the CLI's `--output json` envelope. If you want streamed text deltas, watch for `notification` events with `method: 'result/delta'` (see below). + +```ts +for await (const event of handle.submit('Hello')) { + if (event.type === 'result') { + console.log('Final reply:', event.text); + } +} +``` + +--- + +## `error` + +```ts +{ + type: 'error', + code: string, + classification: 'engine' | 'protocol' | 'approval' | 'transport' | 'unknown', + severity: 'error', + correlationId: string, + message: string, + stderrTail?: string, + retryable?: boolean, +} +``` + +Terminal event. The engine failed. + +| Field | Notes | +|---|---| +| `code` | Stable identifier. See [CLI output formats: error codes](../user/output-formats.md#error-codes-reference). | +| `classification` | Drives exit code mapping in the CLI; useful for UI styling here. | +| `correlationId` | Same UUID v4 as in the CLI envelope and audit record. Surface it in bug reports. | +| `message` | Human-readable. Safe to display verbatim. | +| `stderrTail` | Set when the SDK synthesizes the error from exit code + stderr tail (envelope was missing/malformed). Up to `STDERR_TAIL_BYTES` (4096) bytes. | +| `retryable` | Set by the SDK if the error is known-transient. Currently rarely set. | + +The SDK distinguishes two error sources: + +1. **Envelope error** — the engine produced a valid JSON envelope with `error` populated. All fields come from the engine. +2. **Synthesized error** — the engine crashed before producing an envelope. The SDK builds a fallback from the exit code (mapped to a classification) and the stderr tail. + +Both look identical from the consumer's perspective; check `stderrTail` to know which. + +--- + +## `notification` + +```ts +{ type: 'notification', method: string, params: unknown } +``` + +A structured wire event from the engine's NDJSON stderr stream. **Only emitted when `displayMode: 'ndjson'`**. With `displayMode: 'text'` (or unset), no `notification` events are emitted. + +The shape mirrors JSON-RPC notifications (without the `"jsonrpc": "2.0"` field — it's NDJSON, not full JSON-RPC). + +### Canonical wire methods + +| Method | `params` shape | When | +|---|---|---| +| `result/delta` | `{ sessionId, turnId, text }` | A chunk of the model's reply. Fired many times. | +| `result/final` | `{ sessionId, turnId, text }` | End-of-reply marker. `text` typically empty. | +| `tool/started` | `{ sessionId, turnId, tool, ... }` | A tool call started. | +| `tool/completed` | `{ sessionId, turnId, tool, result, ... }` | A tool call completed. | +| `thinking/delta` | `{ sessionId, turnId, text }` | Extended-thinking chunk. | +| `thinking/final` | `{ sessionId, turnId, text }` | End-of-thinking. | +| `usage` (per-call) | `{ sessionId, turnId, inputTokens, outputTokens, llmDurationMs, model, provider, cacheReadTokens, cacheWriteTokens, cost }` | After each LLM call. | +| `usage` (session total) | `{ sessionId, turnId, inputTokens: 0, outputTokens: 0, sessionCostTotal }` | At session end. | + +These are emitted whenever `displayMode === 'ndjson'`, regardless of verbosity flags (the verbosity flags only affect `--display text`). + +### Streaming the reply + +```ts +let buffer = ''; +for await (const event of handle.submit('Tell me a story')) { + if (event.type === 'notification' && event.method === 'result/delta') { + const delta = (event.params as any).text as string; + buffer += delta; + process.stdout.write(delta); // stream to console + } else if (event.type === 'result') { + console.log('\n\nFull reply length:', buffer.length); + } +} +``` + +### Tracking tool calls + +```ts +for await (const event of handle.submit('Search the codebase')) { + if (event.type === 'notification') { + if (event.method === 'tool/started') { + const params = event.params as any; + console.log(`Tool started: ${params.tool}`); + } else if (event.method === 'tool/completed') { + const params = event.params as any; + console.log(`Tool completed: ${params.tool}`); + } + } +} +``` + +### Token accounting + +```ts +let totalIn = 0, totalOut = 0; +for await (const event of handle.submit('Hello')) { + if (event.type === 'notification' && event.method === 'usage') { + const params = event.params as any; + if (typeof params.inputTokens === 'number') totalIn += params.inputTokens; + if (typeof params.outputTokens === 'number') totalOut += params.outputTokens; + if (params.sessionCostTotal) { + console.log('Session cost:', params.sessionCostTotal); + } + } +} +console.log('Total tokens:', totalIn, '/', totalOut); +``` + +--- + +## Event source map + +For debugging, here's where each event comes from: + +| Event | Source | +|---|---| +| `init` | Synthesized by the SDK before spawn. | +| `activity` | Synthesized by the SDK on a 2s timer. | +| `notification` | Parsed from the engine's stderr (`--display ndjson` lines). | +| `result` | Parsed from the engine's stdout envelope `reply` field (when `error: null`). | +| `error` | Either the engine's envelope `error` field, or synthesized from exit code + stderr tail. | + +If you're seeing fewer events than expected, check: + +- `displayMode: 'ndjson'` is set — without it, no `notification` events. +- The engine actually finished — `activity` events should be visible during long runs. +- Your iterator's `if/else` chain handles `init`/`activity` (otherwise they'll seem invisible). diff --git a/docs/typescript/overview.md b/docs/typescript/overview.md new file mode 100644 index 0000000..d3d5de2 --- /dev/null +++ b/docs/typescript/overview.md @@ -0,0 +1,73 @@ +# TypeScript SDK overview + +**`amplifier-agent-ts`** is the TypeScript SDK for hosts that want to embed amplifier-agent: IDE extensions, chat UIs, web servers, agent-of-agents systems. + +It is a **process driver**, not an LLM client. It spawns the `amplifier-agent` Python binary for each turn, drives its argv and environment, and consumes its structured event stream. + +``` +┌─ Your host (Node.js / Electron / VS Code extension) ─────────────────────┐ +│ │ +│ import { spawnAgent } from 'amplifier-agent-ts'; │ +│ │ +│ const handle = await spawnAgent({ │ +│ lifecycle: 'one-shot', sessionId: 's1', resume: false, │ +│ approval: { mode: 'yes' }, displayMode: 'ndjson', │ +│ env: { allowlist: [...DEFAULT_ALLOWLIST, 'ANTHROPIC_API_KEY'] }, │ +│ }); │ +│ │ +│ for await (const event of handle.submit('Hello')) { │ +│ // DisplayEvent: init | activity | result | error | notification │ +│ } │ +│ │ +└────────────────────┬─────────────────────────────────────────────────────┘ + │ child_process.spawn(amplifier-agent, [...argv]) + v + amplifier-agent run --session-id s1 --fresh --output json + --display ndjson --protocol-version 0.3.0 -y + "Hello" + │ + ├─ stdout: {}\n + └─ stderr: {}\n × N +``` + +## Subprocess per turn + +The SDK launches a **fresh subprocess for every `submit()` call**. There is no daemon, no IPC channel beyond the engine's stdout/stderr. This matches the engine's single-turn model. + +Implications: + +- **`spawnAgent()` does not spawn a subprocess.** It validates parameters, resolves the binary, builds the env, and constructs a `SessionHandle`. The subprocess is spawned when you call `submit()`. +- **`SessionHandle.submit()` is single-use.** Call it once per `SessionHandle`. To send a second turn, create a new `SessionHandle` with the same `sessionId` and pass `resume: true`. +- **`SessionHandle.cancel()` SIGTERMs the engine.** Five seconds later, if still alive, SIGKILL. The engine is the session leader (`setsid`), so MCP child processes get group-killed too. + +## What the SDK does for you + +| Concern | What the SDK handles | +|---|---| +| Binary discovery | `AMPLIFIER_AGENT_BIN` env, then `which amplifier-agent`, with a clear error if neither resolves. | +| Version compatibility | Probes the engine at spawn time, compares to `PROTOCOL_VERSION_REQUIRED_BY_WRAPPER` (`"0.3.0"`), fails fast on mismatch (overridable). | +| Argv assembly | Builds the canonical argv from typed parameters. No string-bashing on the caller's side. | +| Env hygiene | Allowlist-based env passthrough. Blocks dangerous keys (`PYTHONPATH`, `LD_PRELOAD`, etc.). | +| MCP config spill | Accepts an `mcpServers` object in your params; writes a `0600` tempfile and points the engine at it via `AMPLIFIER_MCP_CONFIG`. Cleans up on cancel. | +| NDJSON parsing | Reads the engine's stderr stream and yields one `notification` event per JSON line. | +| Stdout envelope parsing | Reads the engine's stdout envelope, surfaces success as `result`, errors as `error`. Synthesizes a fallback error event from exit code + stderr tail if the envelope is missing. | +| Activity pings | Yields an `activity` event every 2 seconds while the engine is alive, so your UI knows it hasn't hung. | +| Cancel / dispose | Graceful SIGTERM → SIGKILL on a process group. Tempfile cleanup. | + +## When to use the SDK vs the CLI directly + +| Use the SDK | Use the CLI | +|---|---| +| You're writing a Node/Electron/VS Code app | You're in a shell, CI, or another non-Node language | +| You want typed events and error classifications | You want the simplest possible invocation | +| You need MCP servers configured per-call | You manage MCP via `AMPLIFIER_MCP_CONFIG` yourself | +| You want to drive cancel/timeout from your UI | You can `kill -TERM` yourself | + +Anything the SDK does, you can do by invoking the CLI directly with `child_process.spawn`. The SDK is convenience and consistency, not capability. + +## Read next + +- [Quickstart](quickstart.md) — `npm install` and a hello-world. +- [API reference](api-reference.md) — every public export. +- [Events](events.md) — the `DisplayEvent` union and wire events. +- [Advanced](advanced.md) — approval handling, MCP, env allowlist, custom binaries, models list. diff --git a/docs/typescript/quickstart.md b/docs/typescript/quickstart.md new file mode 100644 index 0000000..8b030a7 --- /dev/null +++ b/docs/typescript/quickstart.md @@ -0,0 +1,146 @@ +# TypeScript SDK quickstart + +Install `amplifier-agent-ts`, spawn the agent, drive a turn — in a Node script. + +## Prerequisites + +- **Node ≥ 20** — the package is ESM-only. +- **The `amplifier-agent` binary on PATH.** Install with `uv tool install --from git+https://github.com/microsoft/amplifier-agent amplifier-agent`. See [Installation](../user/installation.md). +- **A provider API key.** Default provider is Anthropic; set `ANTHROPIC_API_KEY`. + +## 1. Install + +```bash +npm install amplifier-agent-ts +``` + +The SDK depends only on Node's built-in `child_process` and `node:stream` modules. No transitive dependencies. + +## 2. Hello world + +```ts +// hello.mjs +import { + spawnAgent, + DEFAULT_ALLOWLIST, +} from 'amplifier-agent-ts'; + +const handle = await spawnAgent({ + lifecycle: 'one-shot', + sessionId: 'hello-1', + resume: false, + approval: { mode: 'yes' }, + displayMode: 'ndjson', + // DEFAULT_ALLOWLIST = [PATH, HOME, USER, LANG, TERM, TMPDIR] + // + AMPLIFIER_*, LC_* always allowed. + // Provider keys need to be added explicitly: + env: { allowlist: [...DEFAULT_ALLOWLIST, 'ANTHROPIC_API_KEY'] }, +}); + +console.log('Engine:', handle.getEngineInfo()); + +for await (const event of handle.submit('Reply with only the word: pong')) { + if (event.type === 'init') { + console.log('[init] session', event.sessionId); + } else if (event.type === 'notification') { + console.log('[wire]', event.method, event.params); + } else if (event.type === 'result') { + console.log('[reply]', event.text); + } else if (event.type === 'error') { + console.error('[error]', event.code, event.message); + process.exit(1); + } + // 'activity' events fire every 2s while the engine is alive — useful for + // keep-alive UI, skipped here for brevity. +} +``` + +Run it: + +```bash +node hello.mjs +``` + +Expected output: + +``` +Engine: { + binaryPath: '/Users/you/.local/bin/amplifier-agent', + protocolVersion: '0.3.0', + engineVersion: '0.5.2', + bundleDigest: '' +} +[init] session hello-1 +[wire] usage { sessionId: '', turnId: 'turn-1', inputTokens: 4202, ... } +[wire] result/final { sessionId: '', turnId: 'turn-1', text: '' } +[wire] result/delta { sessionId: '', turnId: 'turn-1', text: 'pong' } +[wire] usage { sessionId: '', turnId: 'turn-1', inputTokens: 0, sessionCostTotal: '...' } +[reply] pong +``` + +## 3. Resume the session + +Each `submit()` is one turn. To continue the same conversation, create a new `SessionHandle` with the same `sessionId` and `resume: true`: + +```ts +const turn2 = await spawnAgent({ + lifecycle: 'one-shot', + sessionId: 'hello-1', + resume: true, + approval: { mode: 'yes' }, + displayMode: 'ndjson', + env: { allowlist: [...DEFAULT_ALLOWLIST, 'ANTHROPIC_API_KEY'] }, +}); + +for await (const event of turn2.submit('What was your last reply?')) { + if (event.type === 'result') { + console.log('[reply]', event.text); // "My last reply was 'pong'." + } +} +``` + +## 4. Cancel a running turn + +```ts +const handle = await spawnAgent({ /* ... */ }); + +const iter = handle.submit('Do something long'); +setTimeout(() => handle.cancel(), 2000); // SIGTERM after 2s + +try { + for await (const event of iter) { + // ... + } +} catch (err) { + console.log('Cancelled:', err.message); +} +``` + +`cancel()` sends `SIGTERM` to the engine's process group, waits 5 seconds, then `SIGKILL`s if still alive. The next iteration of the event loop will see an `error` event with `code: "cancelled"` (or similar) and the loop will end. + +## 5. Handle errors + +The SDK surfaces three kinds of failures: + +```ts +import { AaaError } from 'amplifier-agent-ts'; + +try { + const handle = await spawnAgent({ /* bad params */ }); +} catch (err) { + if (err instanceof AaaError) { + console.error(err.code); // e.g. 'env_injection_rejected' + console.error(err.classification); // 'protocol' | 'engine' | 'approval' | ... + console.error(err.message); + console.error(err.remediation); + } +} +``` + +Inside the event loop, an `error` event has the same `code` and `classification` fields. See [Events](events.md#error-event) and [CLI output formats: error codes](../user/output-formats.md#error-codes-reference). + +## Next steps + +- Full API surface: [API reference](api-reference.md). +- Streaming events in detail: [Events](events.md). +- Approval handling, MCP servers, custom binaries: [Advanced](advanced.md). diff --git a/docs/user/README.md b/docs/user/README.md new file mode 100644 index 0000000..66235c4 --- /dev/null +++ b/docs/user/README.md @@ -0,0 +1,37 @@ +# amplifier-agent documentation + +User documentation for the **amplifier-agent** CLI and the **`amplifier-agent-ts`** TypeScript SDK for hosts. + +> All claims in this documentation set were verified against the live binary +> running in an isolated Digital Twin environment, and against the source +> code in this repository. If you find a discrepancy, prefer the actual +> behavior of the binary — those checks were the ground truth. + +Verified against `amplifier-agent 0.5.2` (wire protocol `0.3.0`) and `amplifier-agent-ts 0.6.2`. + +## CLI + +Start here if you want to **run** the agent. + +| Page | What it covers | +|---|---| +| [Overview](overview.md) | What amplifier-agent is, where it fits, what it is not | +| [Quickstart](quickstart.md) | Install, configure, and run your first turn in 5 minutes | +| [Installation](installation.md) | Install via `uv`, update, uninstall, sub-binaries | +| [CLI reference](cli-reference.md) | Every subcommand, every flag, with verified examples | +| [Configuration](configuration.md) | The host config JSON schema, every key, validation rules | +| [Environment variables](environment-variables.md) | All `AMPLIFIER_AGENT_*` and provider env vars | +| [Sessions and storage](sessions-and-storage.md) | Workspaces, session files, resume/fresh, the on-disk layout | +| [Output formats](output-formats.md) | `--output` and `--display`, JSON envelope schema, wire events, exit codes | + +## TypeScript SDK (for hosts) + +Start here if you want to **embed** the agent in your own app or IDE extension. + +| Page | What it covers | +|---|---| +| [Overview](../typescript/overview.md) | What the SDK does, the subprocess-per-turn model | +| [Quickstart](../typescript/quickstart.md) | `npm install`, your first `spawnAgent()` call | +| [API reference](../typescript/api-reference.md) | `spawnAgent`, `SessionHandle`, every public export | +| [Events](../typescript/events.md) | `DisplayEvent` union, wire event shapes | +| [Advanced](../typescript/advanced.md) | Approval handling, MCP config, env allowlist, custom binary paths, models list | diff --git a/docs/user/cli-reference.md b/docs/user/cli-reference.md new file mode 100644 index 0000000..cc4877c --- /dev/null +++ b/docs/user/cli-reference.md @@ -0,0 +1,399 @@ +# CLI reference + +Complete reference for every subcommand and every flag. + +``` +amplifier-agent [--version] COMMAND [ARGS]... + +Commands: + cache Manage the prepared-bundle cache. + config Inspect resolved config. + doctor Run self-diagnostics and report system health. + models Enumerate models available from a provider. + prepare Prime the bundle cache (install-time warm-up). + run Run the agent in single-turn mode (Mode A). + update Check for and install the latest amplifier-agent release. + verify Verify the installation and hook coverage. + version Show engine version and wire protocol version. +``` + +## Global + +| Flag | Description | +|---|---| +| `--version` | Print version and exit (`amplifier-agent, version 0.5.2`). | +| `--help` | Show help for any command. | + +--- + +## `run` + +Run a single turn: submit one prompt, receive one reply. + +``` +amplifier-agent run [OPTIONS] PROMPT +``` + +`PROMPT` is required as a positional argument. Stdin is **not** read for the prompt — passing it via pipe will fail with `[error] prompt_required`. (The `is_stdin_tty()` check is used for the *approval-mode* fallback, not prompt input.) + +### Options + +| Flag | Type | Default | Description | +|---|---|---|---| +| `--session-id TEXT` | string | (anonymous) | Session ID to resume or tag. Omit for an anonymous run that writes no audit record. | +| `--resume` | flag | off | Resume an existing session: replay transcript before this prompt. Mutex with `--fresh`. | +| `--fresh` | flag | off | Force a fresh session — delete any saved state for this session ID before starting. Mutex with `--resume`. | +| `--config PATH` | path | — | Path to a host config JSON file. Overrides `$AMPLIFIER_AGENT_CONFIG`. | +| `--cwd PATH` | path | (process cwd) | Working directory the agent sees and uses for relative file paths. | +| `-v, --verbose` | flag | off | Verbose stderr output. Only meaningful with `--display text`; ignored under `--display ndjson`. | +| `--debug` | flag | off | Maximum stderr verbosity. Only meaningful with `--display text`. | +| `--quiet` | flag | off | Suppress all stderr diagnostics. Mutex with `-v`/`--debug`. | +| `-y, --yes` | flag | off | Auto-approve every tool call. Mutex with `-n`. | +| `-n, --no` | flag | off | Auto-deny every tool call. Mutex with `-y`. | +| `--output [text|json]` | enum | `text` | Stdout format. `text` prints `reply + "\n"`. `json` prints a single-line envelope. | +| `--display [text|ndjson]` | enum | `text` | Stderr format. `text` is human-readable. `ndjson` emits one JSON-RPC-shaped event per line. | +| `--protocol-version TEXT` | string | — | Wrapper's pinned protocol version. Engine fails with `protocol_version_mismatch` if it doesn't match (currently `0.3.0`), unless `allowProtocolSkew` is set. | +| `--workspace TEXT` | string | (auto from cwd) | Workspace slug. Isolates session state by project. Falls back to `$AMPLIFIER_AGENT_WORKSPACE`, then a deterministic slug derived from `--cwd`. | + +### Validation + +The following combinations fail at argv parse time with exit code `2`: + +``` +$ amplifier-agent run "x" -y -n +Error: -y and -n are mutually exclusive + +$ amplifier-agent run "x" --resume --fresh +Error: --resume and --fresh are mutually exclusive + +$ amplifier-agent run "x" -v --quiet +Error: --quiet conflicts with -v/--verbose and --debug; choose one verbosity tier +``` + +Headless runs without an explicit approval policy fail fast: + +``` +$ amplifier-agent run "x" --output json < /dev/null +{"protocolVersion":"0.3.0","sessionId":"","turnId":"","reply":"","error":{ + "code":"approval_unconfigured","classification":"protocol","severity":"error", + "message":"Headless run requires an explicit approval policy. Stdin is not a + TTY, neither -y/--yes nor -n/--no was passed, and host_config does not set + `approval.mode`. ...", + "remediation":"Pass `-y` to auto-approve, `-n` to auto-deny, or set + `{\"approval\": {\"mode\": \"yes\"|\"no\"|\"prompt\"}}` in your --config / + $AMPLIFIER_AGENT_CONFIG file."}, ...} +# exit code 2 +``` + +### Examples + +```bash +# Simplest run — text reply on stdout. +amplifier-agent run "Hello" --session-id s1 -y + +# JSON envelope for scripting. +amplifier-agent run "Hello" --session-id s1 -y --output json + +# Continue a previous session. +amplifier-agent run "Now what?" --session-id s1 --resume -y + +# Wipe and start over for the same session ID. +amplifier-agent run "Fresh start" --session-id s1 --fresh -y + +# Structured event stream on stderr (for hosts). +amplifier-agent run "Hello" --session-id s1 -y \ + --output json --display ndjson 2>events.jsonl +``` + +See [Output formats](output-formats.md) for the JSON envelope and NDJSON event shapes. + +--- + +## `config` + +Inspect host configuration. + +### `config show` + +Print the resolved configuration as JSON with source annotations. + +``` +amplifier-agent config show [--config PATH] +``` + +| Flag | Type | Description | +|---|---|---| +| `--config PATH` | path | Show the configuration that would result from this file. Overrides `$AMPLIFIER_AGENT_CONFIG`. | + +The output reports four blocks: + +```json +{ + "provider": { + "value": "anthropic", + "source": "bundle.default_provider" + }, + "host_config": { + "path": "/path/to/cfg.json", + "source": "--config flag", + "parsed": { ... } + }, + "skills": { + "skills": [ ... bundle defaults + host appended ... ], + "visibility": { ... bundle defaults overlaid by host ... } + }, + "amplifier_agent_home": { + "value": "/root/.amplifier-agent", + "source": "default" + } +} +``` + +Source annotations: + +| Block | Possible sources | +|---|---| +| `provider.source` | `"bundle.default_provider"`, `"host_config.provider.module"` | +| `host_config.source` | `"--config flag"`, `"$AMPLIFIER_AGENT_CONFIG env"`, `"none"` | +| `amplifier_agent_home.source` | `"env:AMPLIFIER_AGENT_HOME"`, `"default"` | + +On parse error, `parsed` is `null` and `parse_error` reports the code and message — `config_unknown_key`, `config_invalid_provider_module`, `config_unreadable`, `config_invalid_type`, etc. See [Configuration](configuration.md). + +--- + +## `doctor` + +Run self-diagnostics. Use it whenever something seems wrong. + +``` +amplifier-agent doctor [--strict] [--quick] [--emit-sha] +``` + +| Flag | Description | +|---|---| +| `--strict` | Exit non-zero on warnings (for CI / image-build gating). Without `--strict`, a missing prepared cache is `[INFO]` only. | +| `--quick` | Run minimal checks: Python version and prepared-cache presence. Skips bundle, MCP, XDG writability, and contract checks. | +| `--emit-sha` | Append a line per bundle module with `sha256_prefix=... module=... source=...`. Currently the SHA is of the source URL string (v1 stub); full content SHA is a future enhancement. | + +### Checks (full mode) + +1. Python version (`>= 3.11`) +2. Bundle declares a `default_provider` string +3. `config` root is writable +4. `cache` root is writable +5. `state` root is writable +6. Bundle modules invariants: `context-simple` mounted, `tool-mcp` mounted, `hooks-logging` *not* mounted +7. `WireApprovalProvider` subclass + all three error codes present +8. `SessionStore` write/read roundtrip in a tempdir succeeds +9. `mcp` module is importable +10. Prepared bundle cache presence (`[INFO]` by default; `[FAIL]` with `--strict`) + +Failure of checks 1–9 → exit code 1. With `--strict`, the cache check also gates exit code. + +### Sample output + +``` +$ amplifier-agent doctor +[ OK ] python: 3.12.3 +[ OK ] bundle default_provider: anthropic +[ OK ] config home: /root/.amplifier-agent/config +[ OK ] cache home: /root/.amplifier-agent/cache +[ OK ] state home: /root/.amplifier-agent/state +[ OK ] bundle modules: context-simple, tool-mcp present; hooks-logging absent +[ OK ] wire_approval_provider: subclass check passed; all three error codes present +[ OK ] session_store: write/read roundtrip in tempdir succeeded +[ OK ] mcp module: importable +[INFO] bundle cache: needs prepare (/root/.amplifier-agent/cache/prepared/0.5.2/da41ba6300040dd9) +``` + +--- + +## `cache` + +Manage the prepared-bundle cache at `~/.amplifier-agent/cache/prepared/`. + +### `cache clear` + +Remove every prepared bundle (all versions, all SHAs). Idempotent — succeeds even if nothing was cached. + +``` +amplifier-agent cache clear +``` + +No flags. The next `run` (or explicit `prepare`) will re-clone and re-install the bundle's modules. + +--- + +## `models` + +Enumerate models from one or more providers. + +### `models list` + +``` +amplifier-agent models list [OPTIONS] +``` + +Two modes: + +- **Single-provider** with `--provider ` — query one provider and emit the single-provider envelope. +- **Aggregate** (no `--provider`) — query every known provider in parallel and emit a per-provider results envelope. + +| Flag | Type | Default | Description | +|---|---|---|---| +| `--provider TEXT` | string | (aggregate) | One of `anthropic`, `openai`, `azure-openai`, `ollama`. Omit for aggregate mode. | +| `--output [auto|json|table]` | enum | `auto` | `auto` → table on TTY, JSON otherwise. | +| `--timeout FLOAT` | number | `15.0` | Request timeout in seconds. | +| `--latest` | flag | off | Return only the latest model per family (provider-default filtering). | + +### Single-provider envelope + +```json +{ + "schema_version": 1, + "provider": "anthropic", + "fetched_at": "2026-06-12T08:22:47.890330+00:00", + "models": [ + { + "id": "claude-haiku-4-5-20251001", + "display_name": "Claude Haiku 4.5", + "context_window": 200000, + "max_output_tokens": 64000, + "capabilities": ["tools", "streaming", "json_mode", "fast", "vision", "thinking"], + "defaults": { "temperature": 0.7, "max_tokens": 64000 } + }, + ... + ] +} +``` + +### Aggregate envelope + +```json +{ + "schema_version": 1, + "fetched_at": "2026-06-12T08:23:46.323889+00:00", + "results": [ + { "provider": "anthropic", "status": "ok", "models": [ ... ] }, + { "provider": "openai", "status": "credentials_missing" }, + { "provider": "azure-openai", "status": "module_not_installed" }, + { "provider": "ollama", "status": "error", "error": "..." } + ] +} +``` + +`status` values: `ok`, `credentials_missing`, `module_not_installed`, `error`. + +Providers without credentials emit a one-line stderr notice and return a `credentials_missing` status — they don't error the whole call: + +``` +$ amplifier-agent models list --provider openai +# openai: OPENAI_API_KEY not set; cannot fetch live model list. Set the env var or choose a different provider. +``` + +--- + +## `prepare` + +Pre-warm the bundle cache. Use in CI images, container builds, or before the first end-user run. + +``` +amplifier-agent prepare +``` + +No flags. Equivalent to running the post-install hook (`amplifier-agent-post-install`) but exits non-zero on failure. The post-install hook swallows errors so a failed prep doesn't break installation. + +What it does: + +1. Read the vendored `bundle.md`. +2. Resolve modules via the foundation source resolver — `git clone` then `pip install` each module into the cache. +3. Mount-time validation (`mount()` for every module). +4. Pickle the prepared bundle to `~/.amplifier-agent/cache/prepared///prepared.pickle`. +5. Write a sibling `manifest.json` for cache-key inspection. + +--- + +## `verify` + +Verify installation invariants. + +``` +amplifier-agent verify [--check-hooks] +``` + +| Flag | Description | +|---|---| +| `--check-hooks` | Verify that the canonical wire-event set is exposed by the streaming hook. | + +Default mode is a no-op (`[ OK ] verify: nothing to check`). The interesting use is `--check-hooks`, which asserts the engine's `CANONICAL_WIRE_EVENTS` covers the minimum: `result/delta`, `result/final`, `tool/started`, `tool/completed`, `usage`. + +``` +$ amplifier-agent verify --check-hooks +[ OK ] hook coverage passes — all minimum-set events present +``` + +--- + +## `version` + +``` +amplifier-agent version [--json] +``` + +| Flag | Description | +|---|---| +| `--json` | Emit a one-line JSON payload. | + +### Output + +``` +$ amplifier-agent version +amplifier-agent 0.5.2 (wire 0.3.0) + +$ amplifier-agent version --json +{"version": "0.5.2", "protocolVersion": "0.3.0"} +``` + +The TypeScript SDK's `probeEngineVersion()` calls this with `--json`. + +--- + +## `update` + +``` +amplifier-agent update [--check] [--tag REF] [--force] [--output {text|json}] +``` + +| Flag | Description | +|---|---| +| `--check` | Show status only; do not install. | +| `--tag TEXT` | Install a specific tag, branch, or SHA. | +| `--force` | Reinstall even when versions match. | +| `--output [text|json]` | Output format. | + +Wraps `uv tool install --reinstall --force git+...@` with install-method detection so editable dev checkouts are not clobbered. + +``` +$ amplifier-agent update --check +Checking latest amplifier-agent release... + Current: 0.5.2 + Latest: 0.5.2 (v0.5.2 from 2026-06-09T04:02:00Z) + Install: editable +``` + +`Install:` values: `uv-tool` (the normal case), `editable`, `other`. + +After a successful install, `update` runs the legacy XDG-to-`~/.amplifier-agent/` migration once. See [Installation: Updating](installation.md#updating). + +--- + +## Exit codes + +| Code | Meaning | +|---|---| +| `0` | Success. | +| `1` | Engine, transport, or unknown error during a `run`. | +| `2` | Protocol error (argv validation, host-config parse, protocol version mismatch, approval unconfigured, missing prompt). | +| `3` | Approval error during a `run` (the model wanted a tool that approval denied). | + +See [Output formats: exit codes](output-formats.md#exit-codes-and-error-classifications) for the full error envelope schema. diff --git a/docs/user/configuration.md b/docs/user/configuration.md new file mode 100644 index 0000000..7cc4842 --- /dev/null +++ b/docs/user/configuration.md @@ -0,0 +1,264 @@ +# Configuration + +amplifier-agent ships a sealed bundle with hard-coded defaults. To override any of them, you provide a **host config file** — a JSON document that parameterizes specific blocks the bundle exposes. + +## Where the config comes from + +amplifier-agent resolves the host config in this order (first match wins): + +1. `--config ` flag. +2. `$AMPLIFIER_AGENT_CONFIG` env var. +3. Neither set → no host config; bundle defaults stand. + +If `$AMPLIFIER_AGENT_CONFIG` is set to a path that doesn't exist, the run fails with `config_unreadable` — it is not silently skipped. + +To see what config will actually be used: + +```bash +amplifier-agent config show --config /path/to/cfg.json +``` + +## The schema + +The config file is JSON. The top level is **closed**: only these five keys are accepted. Any other key produces `config_unknown_key`: + +```json +{ + "provider": { ... }, + "approval": { ... }, + "skills": { ... }, + "mcp": { ... }, + "allowProtocolSkew": false +} +``` + +The schema is intentionally minimal. amplifier-agent only lets you parameterize what `bundle.md` already declares — there is no schema translation, no key renaming, no recursive merging. If the bundle exposes a key, the host can set it; otherwise the bundle default stands. + +--- + +### `provider` + +Select which LLM provider mounts and pass per-provider parameters. + +```json +{ + "provider": { + "module": "anthropic", + "config": { + "default_model": "claude-sonnet-4-5", + "temperature": 0.7, + "max_tokens": 8000, + "thinking_budget_tokens": 1024, + "effort": "medium" + } + } +} +``` + +| Key | Type | Description | +|---|---|---| +| `provider.module` | string | One of `anthropic`, `openai`, `azure-openai`, `ollama`. Anything else → `config_invalid_provider_module`. | +| `provider.config` | object | Pass-through to the provider's mount config. The merger overlays this on top of the bundle's provider defaults per key. | + +The `provider.config` block is **pass-through**. amplifier-agent does not validate the inner keys — they reach the provider's `mount()` unchanged. The keys recognized by current providers include `default_model`, `effort`, `temperature`, `max_tokens`, `thinking_budget_tokens`. Future provider-specific keys flow through automatically. + +The API key for each provider comes from the **environment**, not from this file: + +| Provider | Env var | Legacy alias | +|---|---|---| +| `anthropic` | `ANTHROPIC_API_KEY` | — | +| `openai` | `OPENAI_API_KEY` | — | +| `azure-openai` | `AZURE_OPENAI_API_KEY` | `AZURE_OPENAI_KEY` (deprecated, warned once) | +| `ollama` | `OLLAMA_HOST` | `OLLAMA_BASE_URL` (deprecated) | + +If the named provider's credential env var is unset, the run fails at mount time with a clear stderr message. + +--- + +### `approval` + +Control how tool calls are approved. + +```json +{ + "approval": { + "mode": "yes", + "patterns": [] + } +} +``` + +| Key | Type | Description | +|---|---|---| +| `approval.mode` | `"yes"` \| `"no"` \| `"prompt"` | Headless default. | +| `approval.patterns` | array of strings | Glob patterns for tool-name routing (pass-through to `hooks-approval`). | + +`approval.mode` resolution order (the engine picks the **first defined** value): + +1. `-y` argv flag → `"yes"`. +2. `-n` argv flag → `"no"`. +3. `host_config.approval.mode`. +4. Stdin is a TTY → `"prompt"`. +5. Non-TTY, no explicit policy → **fail fast** with `approval_unconfigured`. + +This means a headless run (CI, container, piped invocation) that does *not* set `-y`, `-n`, or `approval.mode` will refuse to start. Earlier versions silently auto-denied, producing success-shaped no-op runs; the fail-fast was added to prevent that footgun. + +--- + +### `skills` + +Extend the skills the agent can load and tune how skills appear in context. + +```json +{ + "skills": { + "skills": ["~/my-skills", "git+https://github.com/me/extra-skills"], + "visibility": { + "enabled": true, + "inject_role": "user", + "max_skills_visible": 25, + "ephemeral": true, + "priority": 20 + } + } +} +``` + +Two sub-keys with **different merge semantics**: + +| Sub-key | Shape | Merge rule | +|---|---|---| +| `skills.skills` | list of strings | **List-concat**: bundle defaults first, host entries appended. The bundle is the floor; the host can only *extend*, never *strip*. | +| `skills.visibility` | object | **Shallow per-key overlay**: bundle keys come through unless the host overrides them per key. | + +So if the bundle ships with three default skill sources and you add one, the resulting list has four entries — verified with `amplifier-agent config show --config ...`: + +```json +"skills": { + "skills": [ + "git+https://github.com/microsoft/amplifier-bundle-skills@main#subdirectory=skills", + ".amplifier/skills", + "~/.amplifier/skills", + "~/my-skills" + ], + ... +} +``` + +If you declare `skills` in your config but the bundle doesn't have a `tool-skills` mount, the merger refuses with `config_no_matching_module` — it won't silently fabricate a config for a module that won't be mounted. + +--- + +### `mcp` + +Point the agent at an [MCP](https://modelcontextprotocol.io/) server configuration file. + +```json +{ + "mcp": { + "configPath": "/path/to/mcp-servers.json" + } +} +``` + +| Key | Type | Description | +|---|---|---| +| `mcp.configPath` | string | Path to an MCP server config file (the format `tool-mcp` expects). Translated to `AMPLIFIER_MCP_CONFIG` env var. | + +The host config layer is the only way to point `tool-mcp` at a config file from amplifier-agent (the former `--mcp-config-path` argv flag was removed; the host config is the single source of truth). + +--- + +### `allowProtocolSkew` + +```json +{ + "allowProtocolSkew": true +} +``` + +When `true`, the engine boot bypasses the protocol-version-mismatch check. Useful in dev when the TypeScript SDK and the Python engine are out of sync. **Unsafe in production** — version mismatch means the SDK and engine may disagree on wire-event shapes. + +Default: `false`. + +--- + +## Examples + +### Use OpenAI with GPT-4o + +```json +{ + "provider": { + "module": "openai", + "config": { "default_model": "gpt-4o" } + }, + "approval": { "mode": "yes" } +} +``` + +### Run against a local Ollama daemon + +```bash +export OLLAMA_HOST=http://localhost:11434 +``` + +```json +{ + "provider": { + "module": "ollama", + "config": { "default_model": "llama3.1:70b" } + }, + "approval": { "mode": "yes" } +} +``` + +### Headless CI run with Anthropic + +```json +{ + "approval": { "mode": "yes" }, + "provider": { + "module": "anthropic", + "config": { + "default_model": "claude-haiku-4-5", + "max_tokens": 4000 + } + } +} +``` + +### Add custom skills to the default set + +```json +{ + "skills": { + "skills": [ + "git+https://github.com/myorg/my-skills@main", + "./.team-skills" + ] + } +} +``` + +The four bundle-default skill sources are preserved; the two extras are appended. + +--- + +## Validation errors + +All host config parse errors share a uniform `{code, message}` shape. Surface them via: + +```bash +amplifier-agent config show --config /path/to/cfg.json +``` + +| Code | When | +|---|---| +| `config_unreadable` | File not found or unreadable. | +| `config_unknown_key` | Top-level key outside the closed set `{provider, approval, skills, mcp, allowProtocolSkew}`. | +| `config_invalid_type` | Closed-inner-shape violation (currently: a key inside `skills.*` other than `skills` or `visibility`, or `skills`/`skills.visibility` having a non-list/non-dict shape). The other blocks (`provider.config`, `approval`, `mcp`) are pass-through and silently ignored if not a dict. | +| `config_invalid_provider_module` | `provider.module` not in `{anthropic, openai, azure-openai, ollama}`. | +| `config_no_matching_module` | Host declares `skills:` but bundle has no `tool-skills` mount. | + +On parse failure during a `run`, the envelope's `error` field carries the same code with `classification: "protocol"` and the run exits `2`. diff --git a/docs/user/environment-variables.md b/docs/user/environment-variables.md new file mode 100644 index 0000000..330a722 --- /dev/null +++ b/docs/user/environment-variables.md @@ -0,0 +1,103 @@ +# Environment variables + +Every environment variable amplifier-agent reads, sorted by scope. + +## amplifier-agent's own variables + +| Variable | Purpose | +|---|---| +| `AMPLIFIER_AGENT_HOME` | Override the storage root. Default: `~/.amplifier-agent/`. Relocates `cache`, `config`, and `state` together. | +| `AMPLIFIER_AGENT_CONFIG` | Path to the host config JSON file. The `--config` flag overrides this. If set to a non-existent path, the run fails with `config_unreadable`. | +| `AMPLIFIER_AGENT_WORKSPACE` | Workspace slug for session isolation. The `--workspace` flag overrides this. Falls through to a deterministic slug derived from `--cwd` (basename + sha256 prefix) when unset. | +| `AMPLIFIER_AGENT_BIN` | (TypeScript SDK only) Path to the engine binary. The SDK checks this before `which amplifier-agent`. Useful in test harnesses and editable dev checkouts. | +| `AMPLIFIER_AGENT_DEBUG_SIDLOG` | If set (any value), emit `engine-sid-ok pid= sid=` to stderr at engine boot. Diagnostic only — used to confirm `setsid` ran. | +| `AMPLIFIER_AGENT_ALLOW_PROTOCOL_SKEW` | (TypeScript SDK only) Boolean-ish flag; equivalent to `allowProtocolSkew: true` in host config. Mentioned in the protocol-mismatch remediation message. | + +### `AMPLIFIER_AGENT_HOME` + +```bash +export AMPLIFIER_AGENT_HOME=/var/lib/amplifier-agent +amplifier-agent config show | grep -A1 amplifier_agent_home +# "amplifier_agent_home": { +# "value": "/var/lib/amplifier-agent", +# "source": "env:AMPLIFIER_AGENT_HOME" +# } +``` + +Caveat: not every code path expands env vars in paths. The bundle's `hook-context-intelligence` writes its observability events to the literal path `~/.amplifier-agent/state/workspaces/...` (only `~` is expanded). If you relocate via `AMPLIFIER_AGENT_HOME`, transcripts and audits move with it; the context-intelligence events stay at the literal default path. This is an upstream hook limitation, not amplifier-agent behavior — for now, run from `~/.amplifier-agent/` if you need the observability events alongside transcripts. + +### `AMPLIFIER_AGENT_CONFIG` + +```bash +export AMPLIFIER_AGENT_CONFIG=/etc/amplifier-agent/config.json +amplifier-agent config show +# "host_config": { +# "path": "/etc/amplifier-agent/config.json", +# "source": "$AMPLIFIER_AGENT_CONFIG env", +# ... +# } +``` + +`--config` overrides this. To opt out without unsetting the var, pass `--config` with a different path (or use a temporary shell with `env -u AMPLIFIER_AGENT_CONFIG ...`). + +### `AMPLIFIER_AGENT_WORKSPACE` + +```bash +export AMPLIFIER_AGENT_WORKSPACE=my-project +amplifier-agent run "test" -y --session-id s1 +ls ~/.amplifier-agent/state/workspaces/my-project/sessions/s1/ +# transcript.jsonl +# metadata.json +# audits/ +# context-intelligence/ +``` + +Slug grammar: `[a-z0-9][a-z0-9-]{0,63}`. Leading `_` is reserved for internal workspaces (e.g. `_legacy` used by the auto-migration). + +--- + +## Provider credentials + +amplifier-agent never reads provider keys from the host config file — they come from the environment. The default provider is `anthropic`; to use a different provider, set the credential env var **and** add a `provider.module` entry to your host config. + +| Provider | Primary env var | Legacy alias (still honored) | +|---|---|---| +| `anthropic` | `ANTHROPIC_API_KEY` | — | +| `openai` | `OPENAI_API_KEY` | — | +| `azure-openai` | `AZURE_OPENAI_API_KEY` | `AZURE_OPENAI_KEY` (deprecated, one-time stderr warning) | +| `ollama` | `OLLAMA_HOST` | `OLLAMA_BASE_URL` (deprecated) | + +If the configured provider's primary env var is unset, the run fails at mount time: + +``` +No API key found for Anthropic provider +Failed to load module 'provider-anthropic': ... No provider was mounted ... +``` + +Inside the TypeScript SDK, provider env vars are **not** in `DEFAULT_ALLOWLIST` — you must add them to your `env.allowlist` when calling `spawnAgent()`. See [TypeScript advanced: env allowlist](../typescript/advanced.md#environment-allowlist). + +--- + +## Hook-side variables + +These are read by bundle hooks, not by amplifier-agent itself. They are listed here so you know they exist. + +| Variable | Read by | Purpose | +|---|---|---| +| `AMPLIFIER_MCP_CONFIG` | `tool-mcp` | Path to the MCP server config. Set by amplifier-agent when you provide `mcp.configPath` in the host config. You can also set it directly. | +| `AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL` | `hook-context-intelligence` | (Reserved.) If set, the hook would dispatch events to a remote server. Currently no server-config layer in amplifier-agent. | +| `AMPLIFIER_CONTEXT_INTELLIGENCE_API_KEY` | `hook-context-intelligence` | (Reserved.) Companion to the URL above. | + +--- + +## Resolution order summary + +For any setting that has both an env var and a flag, the precedence is **flag → env → bundle default → fail**: + +| Setting | Flag | Env | Default | +|---|---|---|---| +| Host config path | `--config` | `AMPLIFIER_AGENT_CONFIG` | (none — no overlay) | +| Storage root | (none) | `AMPLIFIER_AGENT_HOME` | `~/.amplifier-agent` | +| Workspace | `--workspace` | `AMPLIFIER_AGENT_WORKSPACE` | (derived from cwd) | +| Approval mode | `-y` / `-n` | (none direct) | `host_config.approval.mode` → TTY check → fail | +| Provider | (none) | (none direct) | `host_config.provider.module` → `bundle.default_provider` | diff --git a/docs/user/installation.md b/docs/user/installation.md new file mode 100644 index 0000000..b798f51 --- /dev/null +++ b/docs/user/installation.md @@ -0,0 +1,134 @@ +# Installation + +## Requirements + +- **Python ≥ 3.12** (the package requires 3.12; the engine internals require 3.11 minimum). +- **[uv](https://docs.astral.sh/uv/)** — the recommended installer. +- **Network access** at install time. First `run` downloads the bundle's module dependencies from GitHub. +- **A provider API key** — see [Environment variables](environment-variables.md). + +## Recommended install: `uv tool install` + +```bash +uv tool install --from git+https://github.com/microsoft/amplifier-agent amplifier-agent +``` + +This installs the `amplifier-agent` binary and a sub-binary `amplifier-agent-post-install` on your PATH (typically `~/.local/bin`). Confirm: + +```bash +$ which amplifier-agent +/Users/you/.local/bin/amplifier-agent +$ amplifier-agent --version +amplifier-agent, version 0.5.2 +``` + +### Why uv? + +The package is published on GitHub, not PyPI. `uv tool install` clones the git repo, builds the wheel with `hatchling`, and installs it into an isolated environment with its own Python. You don't need to manage a virtualenv yourself. + +### Editable install (development) + +If you have a local clone: + +```bash +uv tool install --editable /path/to/amplifier-agent +``` + +Changes to source under `/path/to/amplifier-agent/src/` take effect immediately on the next `amplifier-agent` invocation. The `update` command detects the editable install and warns instead of clobbering: + +```bash +$ amplifier-agent update --check +Checking latest amplifier-agent release... + Current: 0.5.2 + Latest: 0.5.2 (v0.5.2 from 2026-06-09T04:02:00Z) + Install: editable +``` + +## What gets installed + +The wheel contains the CLI (`amplifier_agent_cli/`) and the library (`amplifier_agent_lib/`), plus four files vendored as data: + +- The bundle manifest (`amplifier_agent_lib/bundle/bundle.md`). +- The four sub-agent definitions (`explorer.md`, `planner.md`, `coder.md`, `tester.md`). +- The wire protocol spec (`amplifier_agent_lib/protocol/spec.md`). + +Everything else (the orchestrator, provider, hooks, tools — see `bundle.md`) is **not** in the wheel. They are git-cloned and pip-installed lazily into a per-version cache on first invocation. This is why the first `run` is slow (cold cache) and subsequent runs are fast. + +## Post-install warmup + +To pre-warm the bundle cache (e.g., in a CI image or container): + +```bash +amplifier-agent prepare +``` + +Or, equivalently, the entry point installed by the wheel: + +```bash +amplifier-agent-post-install +``` + +Both clone and install the bundle modules into `~/.amplifier-agent/cache/prepared///` and pickle the prepared bundle for reuse. The post-install variant swallows errors so a failed prep doesn't break the install; the `prepare` subcommand exits non-zero on failure so you can gate CI on it. + +## Storage locations + +By default, all per-user state lives under `~/.amplifier-agent/`: + +| Directory | Contents | +|---|---| +| `~/.amplifier-agent/cache/prepared///` | Pickled prepared bundle, manifest | +| `~/.amplifier-agent/config/` | Reserved for future host config | +| `~/.amplifier-agent/state/workspaces//sessions//` | Transcripts, metadata, audits, observability events | + +Override the entire root with `AMPLIFIER_AGENT_HOME`: + +```bash +export AMPLIFIER_AGENT_HOME=/var/lib/amplifier-agent +``` + +This relocates `cache`, `config`, and `state` together. See [Environment variables](environment-variables.md#amplifier_agent_home) for caveats. + +## Updating + +The `update` subcommand wraps `uv tool install --reinstall --force` with version detection: + +```bash +$ amplifier-agent update --check # Show status only, do not install. +$ amplifier-agent update # Install the latest tagged release. +$ amplifier-agent update --tag v0.5.2 # Install a specific tag, branch, or SHA. +$ amplifier-agent update --force # Reinstall even when versions match. +$ amplifier-agent update --output json # Emit JSON for scripts. +``` + +`update` detects how amplifier-agent was installed: + +- `uv-tool` (the normal case) — `uv tool install --reinstall --force git+...@`. +- `editable` — refuses to reinstall (it would clobber your checkout). Pass `--force` if you really mean it. +- `other` — anything `update` can't classify (e.g. pip install from PyPI). Falls back to `uv tool install` if possible. + +After a successful update, `update` invokes the XDG-to-`~/.amplifier-agent/` migration once. Storage that lived under `~/.local/state/amplifier-agent/`, `~/.cache/amplifier-agent/`, and `~/.config/amplifier-agent/` (pre-0.5.x layouts) is moved into `~/.amplifier-agent/state/`, `~/.amplifier-agent/cache/`, and `~/.amplifier-agent/config/`. A sentinel at `~/.amplifier-agent/.migrated_from_xdg` ensures the migration runs only once. + +## Uninstall + +```bash +uv tool uninstall amplifier-agent +``` + +This removes the binary and the tool's virtualenv. It does **not** delete `~/.amplifier-agent/` — transcripts, caches, and audits persist. To remove them: + +```bash +rm -rf ~/.amplifier-agent/ +``` + +If you set `AMPLIFIER_AGENT_HOME`, remove that path instead. + +## Verifying a fresh install + +```bash +amplifier-agent doctor # Cross-check everything the engine needs. +amplifier-agent verify # Verify the installation invariants. +amplifier-agent verify --check-hooks # Confirm wire-event coverage. +amplifier-agent version --json # Machine-readable version pair. +``` + +`doctor` is the one to run if anything seems wrong. It checks Python version, bundle integrity, writability of cache/config/state, MCP module import, and approval-provider contract. See [CLI reference: doctor](cli-reference.md#doctor) for a full breakdown. diff --git a/docs/user/output-formats.md b/docs/user/output-formats.md new file mode 100644 index 0000000..fe189ab --- /dev/null +++ b/docs/user/output-formats.md @@ -0,0 +1,217 @@ +# Output formats + +`amplifier-agent run` writes to two independent streams. `--output` controls stdout. `--display` controls stderr. + +``` + stdout stderr +--output text → "\n" +--output json → {}\n + + --display text → human-readable lines + --display ndjson → one wire event per line +``` + +`--output text --display ndjson` is a valid combination: stdout gets the plain reply, stderr gets the structured event stream. + +--- + +## `--output text` (default) + +The reply is printed to stdout followed by a newline. Nothing else goes to stdout. All progress, status, and diagnostic output is on stderr. + +``` +$ amplifier-agent run "Reply with only: ok" -y --session-id s1 +ok +``` + +If something goes wrong before the model produces a reply, stdout is empty and stderr carries a `[error]` line: + +``` +$ amplifier-agent run -y -n "hello" +Error: -y and -n are mutually exclusive + +$ amplifier-agent run "missing" # no stdin, no -y/-n, no host config +[error] approval_unconfigured: ... # appears on stdout in text mode +``` + +(One quirk: a few engine-level error paths emit the `[error]` to stdout because they happen before output routing is fully configured. Programs that script `amplifier-agent` should prefer `--output json` for unambiguous behavior.) + +--- + +## `--output json` + +A single-line JSON envelope is printed to stdout, regardless of success or failure. Use this in all scripts and host integrations. + +### Success envelope + +```json +{ + "protocolVersion": "0.3.0", + "sessionId": "smoke-1", + "turnId": "turn-1", + "reply": "pong", + "error": null, + "metadata": { + "tokensIn": 0, + "tokensOut": 0, + "durationMs": 21439, + "bundleDigest": "", + "engineVersion": "0.5.2", + "protocolVersion": "0.3.0", + "correlationId": "dd548f41-c9a4-4f08-b45b-203d9fc2b349" + } +} +``` + +| Field | Type | Notes | +|---|---|---| +| `protocolVersion` | string | Wire protocol version, currently `"0.3.0"`. | +| `sessionId` | string | The session ID. Empty string for anonymous runs. | +| `turnId` | string | Always `"turn-1"` in Mode A (each invocation is a fresh subprocess). | +| `reply` | string | The model's text reply. | +| `error` | `null` or error object | See below. | +| `metadata.tokensIn` / `tokensOut` | int | Currently `0` in the envelope; live usage is reported via NDJSON wire events instead. | +| `metadata.durationMs` | int | Engine-side wall-clock duration. | +| `metadata.bundleDigest` | string | Reserved; currently empty. | +| `metadata.engineVersion` | string | The amplifier-agent version. | +| `metadata.protocolVersion` | string | Mirrors top-level for convenience. | +| `metadata.correlationId` | uuid string | Unique per `run` invocation. Appears in audit records and wire events. | + +### Error envelope + +```json +{ + "protocolVersion": "0.3.0", + "sessionId": "", + "turnId": "", + "reply": "", + "error": { + "code": "approval_unconfigured", + "classification": "protocol", + "severity": "error", + "correlationId": "d473ad6f-c72e-46f3-8e4a-593babf513b8", + "message": "Headless run requires an explicit approval policy. ...", + "remediation": "Pass `-y` to auto-approve, `-n` to auto-deny, or set ..." + }, + "metadata": { ... } +} +``` + +Error object fields: + +| Field | Type | Description | +|---|---|---| +| `code` | string | Stable identifier. See [Error codes](#error-codes-reference). | +| `classification` | `engine` \| `protocol` \| `approval` \| `transport` \| `unknown` | Drives exit code mapping. | +| `severity` | `error` (current) | Reserved for future warnings. | +| `correlationId` | uuid | Same as the top-level envelope `correlationId`. | +| `message` | string | Human-readable description. | +| `remediation` | string (optional) | How to fix the error. | +| `stderrTail` | string (optional) | Last N bytes of subprocess stderr (set by the TypeScript SDK; not by the engine itself). | + +--- + +## Exit codes and error classifications + +| Exit code | Classification | When it happens | +|---|---|---| +| `0` | — | Successful run. | +| `1` | `engine`, `transport`, `unknown` | Provider failure, kernel error, internal bug. | +| `2` | `protocol` | Bad argv, host-config parse error, protocol version mismatch, missing prompt, approval unconfigured. | +| `3` | `approval` | The model requested a tool that approval denied during the run. | + +The mapping is fixed in the engine (`_EXIT_CODE_BY_CLASSIFICATION`). The TypeScript SDK applies the same mapping when synthesizing error envelopes from a subprocess that crashed before producing a JSON envelope. + +--- + +## `--display text` (default) + +Stderr emits human-readable summaries: + +``` +[usage] in=4202 out=99 cost=$0.0122376 cache_read=4192 cache_write=2524 dur=3540ms model=claude-sonnet-4-5 provider=anthropic +[result/final] +[result/delta] pong +[usage] in=0 out=0 session_total=$0.0122376 +``` + +`[type]` prefixes correspond to the canonical wire events. Verbosity tiers (in increasing order): + +| Tier | Flag | What stderr shows | +|---|---|---| +| `quiet` | `--quiet` | Nothing. | +| `normal` | (default) | Brief summaries. | +| `verbose` | `-v` / `--verbose` | Adds tool call argument summaries, agent transitions. | +| `debug` | `--debug` | Adds kernel events, internal state. | + +`--quiet` is mutually exclusive with `-v` and `--debug`. + +--- + +## `--display ndjson` + +Stderr emits one JSON object per line, one per wire event. This is what the TypeScript SDK consumes. + +``` +{"method": "usage", "params": {"sessionId": "", "turnId": "turn-1", "inputTokens": 4202, "outputTokens": 115, "llmDurationMs": 5439, "model": "claude-sonnet-4-5", "provider": "anthropic", "cacheReadTokens": 4192, "cacheWriteTokens": 2540, "cost": "0.0125376"}} +{"method": "result/final", "params": {"sessionId": "", "turnId": "turn-1", "text": ""}} +{"method": "result/delta", "params": {"sessionId": "", "turnId": "turn-1", "text": "echo only"}} +{"method": "usage", "params": {"sessionId": "", "turnId": "turn-1", "inputTokens": 0, "outputTokens": 0, "sessionCostTotal": "0.0125376"}} +``` + +Each line has a `method` and a `params` object. The verbosity flags (`-v`, `--debug`, `--quiet`) **do not affect** ndjson output — the full event stream is always emitted. + +### Canonical wire event types + +| Method | Params (typical) | When emitted | +|---|---|---| +| `result/delta` | `{sessionId, turnId, text}` | A chunk of the model's textual reply. May fire many times. | +| `result/final` | `{sessionId, turnId, text}` | End-of-reply marker. `text` is typically empty (the deltas already carried it). | +| `tool/started` | `{sessionId, turnId, tool, ...}` | The model called a tool. | +| `tool/completed` | `{sessionId, turnId, tool, result, ...}` | A tool call returned. | +| `thinking/delta` | `{sessionId, turnId, text}` | Extended-thinking chunk (when the model uses thinking). | +| `thinking/final` | `{sessionId, turnId, text}` | End-of-thinking marker. | +| `usage` | `{inputTokens, outputTokens, cost, model, provider, ...}` | Token accounting. Emitted per LLM call and at session end (`sessionCostTotal`). | + +> **Note on the shape:** events are NDJSON, not full JSON-RPC. They have `method` and `params` but not the `"jsonrpc": "2.0"` field. The TypeScript SDK parses them as a stream of notification-like objects. + +--- + +## Combining `--output` and `--display` + +```bash +# Human-friendly default — text reply, text diagnostics. +amplifier-agent run "..." -y + +# Scripted — JSON envelope, text diagnostics. +amplifier-agent run "..." -y --output json > out.json + +# Quiet — JSON envelope only, nothing on stderr. +amplifier-agent run "..." -y --output json --quiet > out.json + +# Wrapper — JSON envelope on stdout, structured events on stderr. +amplifier-agent run "..." -y --output json --display ndjson \ + > out.json 2> events.ndjson +``` + +The TypeScript SDK uses the last form (`--output json --display ndjson`) under the hood. + +--- + +## Error codes reference + +This is not exhaustive — codes evolve. Surface unknowns by reading the `message` field. + +| Code | Classification | Exit | Where | +|---|---|---|---| +| `approval_unconfigured` | `protocol` | 2 | Headless run with no `-y`/`-n` and no `host_config.approval.mode`. | +| `protocol_version_mismatch` | `protocol` | 2 | `--protocol-version` does not equal the engine's compiled version, and `allowProtocolSkew` is false. | +| `config_unreadable` | `protocol` | 2 | The host config file can't be read. | +| `config_unknown_key` | `protocol` | 2 | Top-level key outside the closed set. | +| `config_invalid_type` | `protocol` | 2 | Closed-inner-shape violation inside `skills:` block. Other blocks (`provider.config`, `approval`, `mcp`) are pass-through. | +| `config_invalid_provider_module` | `protocol` | 2 | `provider.module` is not a known provider. | +| `config_no_matching_module` | `protocol` | 2 | Host declares `skills:` but bundle has no `tool-skills` mount. | +| `prompt_required` | `protocol` | 2 | No prompt passed and stdin is not a TTY. | +| `env_injection_rejected` | `protocol` | 2 | (TypeScript SDK) Caller tried to pass a blocked env key (e.g. `PYTHONPATH`). | + +Approval-class codes (exit 3) arise during a run when the model attempts a tool call that approval denies; they share `classification: "approval"`. diff --git a/docs/user/overview.md b/docs/user/overview.md new file mode 100644 index 0000000..163e3c9 --- /dev/null +++ b/docs/user/overview.md @@ -0,0 +1,78 @@ +# Overview + +**amplifier-agent** is a command-line AI coding agent. It boots an opinionated bundle (a fixed orchestrator, context, hooks, and a set of sub-agents), submits a single prompt to a configured LLM provider, executes any tool calls the model requests (after approval), prints the result, and exits. + +## What it does + +``` +$ amplifier-agent run "List the python files in src/" -y +[result/delta] Here are the python files... + +$ amplifier-agent run "What was your last reply?" --resume --session-id smoke-1 -y +[result/delta] My last reply was "ping". +``` + +Each invocation of `amplifier-agent run` is a **single turn**: one prompt in, one reply out. State is persisted to disk between turns under a *workspace* and a *session id*, so successive runs can carry forward conversation history with `--resume`. + +## Where it fits + +amplifier-agent is one of three layers built on the [amplifier-core](https://github.com/microsoft/amplifier-core) kernel and the [amplifier-foundation](https://github.com/microsoft/amplifier-foundation) library: + +| Layer | What it is | +|---|---| +| amplifier-core | The thin kernel. Defines module contracts (providers, tools, orchestrators, hooks, context) and runs a session. | +| amplifier-foundation | The composition library. Bundles, behaviors, agent files, the `delegate` pattern. | +| **amplifier-agent** | **An opinionated CLI built on the foundation. Vendored bundle, hard-coded sub-agents, single-turn execution model.** | + +The CLI vendors a bundle manifest at install time ([`amplifier-agent-builtin` v1.3.0](../../src/amplifier_agent_lib/bundle/bundle.md)) and four sub-session agents (`explorer`, `planner`, `coder`, `tester`). When the model decides to delegate to a sub-agent (via the `delegate` tool), the CLI spawns it as a child session with its own tool surface. + +## Architecture + +``` +┌─ amplifier-agent (CLI process) ─────────────────────────────────┐ +│ │ +│ click ──> single_turn.run() ──> engine.boot() ──> core loop │ +│ │ │ +│ ├─ provider (LLM) │ +│ ├─ tools (todo, │ +│ │ delegate, mcp, │ +│ │ skills) │ +│ ├─ hooks (status, │ +│ │ redaction, │ +│ │ logging) │ +│ └─ context-simple │ +│ │ +│ stdout: JSON envelope or plain reply (--output) │ +│ stderr: NDJSON wire events or human-readable text (--display) │ +└─────────────────────────────────────────────────────────────────┘ + │ writes + v +~/.amplifier-agent/state/workspaces//sessions// + transcript.jsonl + metadata.json + audits/turn-.json + context-intelligence/events.jsonl +``` + +The engine is launched fresh for every `run`. There is no daemon, no persistent server. Sessions are persisted **between** runs, but each run is a clean subprocess that loads the prepared bundle (from a pickle cache), wires up the configured provider, and exits after one turn. + +## Two consumer surfaces + +1. **The CLI** — typed by humans, scripts, and CI. Documented in [CLI reference](cli-reference.md). + +2. **The TypeScript SDK** (`amplifier-agent-ts`) — used by hosts (IDE extensions, chat UIs, agents-of-agents) to spawn the CLI per turn and consume its structured event stream. Documented in [TypeScript SDK](../typescript/overview.md). + +Both surfaces talk to the *same* `amplifier-agent` binary. The SDK is a thin process driver; it does not contain LLM logic. Anything you can do via the SDK, you can do by invoking the CLI directly. + +## What it is not + +- **Not a server.** No daemon, no socket, no long-lived process. One run = one subprocess. +- **Not a multi-turn REPL.** The CLI is single-turn. Multi-turn conversation is achieved by re-invoking with `--resume` and the same `--session-id`. Hosts wanting an interactive feel drive this loop themselves. +- **Not a bundle host.** The bundle is vendored and sealed per release. You cannot swap providers, tools, or orchestrators at runtime. You *can* parameterize what the bundle exposes — see [Configuration](configuration.md). +- **Not a generic Amplifier shell.** For exploratory multi-bundle development, use the broader amplifier ecosystem (`amplifier-foundation`, your own CLI built on top). amplifier-agent is the opinionated, sealed user-facing distribution. + +## Next steps + +- New here? → [Quickstart](quickstart.md) +- Building a host? → [TypeScript SDK overview](../typescript/overview.md) +- Want every flag? → [CLI reference](cli-reference.md) diff --git a/docs/user/quickstart.md b/docs/user/quickstart.md new file mode 100644 index 0000000..cd6d079 --- /dev/null +++ b/docs/user/quickstart.md @@ -0,0 +1,121 @@ +# Quickstart + +Install amplifier-agent, configure a provider, and run your first turn — in five minutes. + +## Prerequisites + +- **Python 3.12 or newer**. (3.11 is the floor inside the engine, but the package itself requires 3.12.) +- **[uv](https://docs.astral.sh/uv/)** for installation. If you don't have it: `curl -LsSf https://astral.sh/uv/install.sh | sh` +- **An API key** for at least one supported provider: Anthropic, OpenAI, Azure OpenAI, or Ollama. + +## 1. Install + +```bash +uv tool install --from git+https://github.com/microsoft/amplifier-agent amplifier-agent +``` + +This installs the `amplifier-agent` binary on your PATH. Verify: + +```bash +$ amplifier-agent --version +amplifier-agent, version 0.5.2 +``` + +## 2. Set a provider API key + +Export the env var for the provider you want to use: + +```bash +export ANTHROPIC_API_KEY=sk-ant-... # for Anthropic +# or +export OPENAI_API_KEY=sk-... # for OpenAI +# or +export AZURE_OPENAI_API_KEY=... # for Azure OpenAI +# or +export OLLAMA_HOST=http://localhost:11434 # for a local Ollama daemon +``` + +The default provider is **Anthropic**. To use a different provider, you need a host config file — see [step 5](#5-configure-a-different-provider-optional). + +## 3. Check your install + +```bash +$ amplifier-agent doctor +[ OK ] python: 3.12.3 +[ OK ] bundle default_provider: anthropic +[ OK ] config home: /Users/you/.amplifier-agent/config +[ OK ] cache home: /Users/you/.amplifier-agent/cache +[ OK ] state home: /Users/you/.amplifier-agent/state +[ OK ] bundle modules: context-simple, tool-mcp present; hooks-logging absent +[ OK ] wire_approval_provider: subclass check passed; all three error codes present +[ OK ] session_store: write/read roundtrip in tempdir succeeded +[ OK ] mcp module: importable +[INFO] bundle cache: needs prepare (/Users/you/.amplifier-agent/cache/prepared/0.5.2/da41ba6300040dd9) +``` + +The `[INFO] bundle cache: needs prepare` is normal on a fresh install — the bundle's modules get installed lazily on first `run`. + +## 4. Run your first turn + +```bash +$ amplifier-agent run "Reply with only the word: pong" --session-id smoke-1 -y +pong +``` + +What happened: + +- `--session-id smoke-1` names the session so you can resume it. +- `-y` auto-approves any tool the model wants to call. (Required in non-interactive contexts; without it the run will refuse to start. See [approval](configuration.md#approval).) +- The default `--output text` prints the reply to stdout. Use `--output json` to get the full envelope (see [output formats](output-formats.md)). + +### Resume the session + +```bash +$ amplifier-agent run "What was your last reply?" --session-id smoke-1 --resume -y +My last reply was "pong". +``` + +The conversation history persisted between invocations under `~/.amplifier-agent/state/workspaces//sessions/smoke-1/`. See [Sessions and storage](sessions-and-storage.md) for the layout. + +## 5. Configure a different provider (optional) + +Create a host config file: + +```bash +mkdir -p ~/.config/amplifier-agent +cat > ~/.config/amplifier-agent/config.json <<'EOF' +{ + "provider": { + "module": "openai", + "config": { + "default_model": "gpt-4o" + } + }, + "approval": { + "mode": "yes" + } +} +EOF +``` + +Then pass it on each run: + +```bash +amplifier-agent run "Hello" --config ~/.config/amplifier-agent/config.json --session-id s1 +``` + +Or set the env var once for your shell: + +```bash +export AMPLIFIER_AGENT_CONFIG=~/.config/amplifier-agent/config.json +amplifier-agent run "Hello" --session-id s1 +``` + +The full schema is documented in [Configuration](configuration.md). + +## What's next + +- Explore the full CLI: [CLI reference](cli-reference.md) +- Understand session and workspace layout: [Sessions and storage](sessions-and-storage.md) +- Pick a model: [`amplifier-agent models list`](cli-reference.md#models-list) +- Embed amplifier-agent in your own app: [TypeScript SDK](../typescript/overview.md) diff --git a/docs/user/sessions-and-storage.md b/docs/user/sessions-and-storage.md new file mode 100644 index 0000000..1d790cf --- /dev/null +++ b/docs/user/sessions-and-storage.md @@ -0,0 +1,203 @@ +# Sessions and storage + +amplifier-agent is a single-turn CLI, but conversations span multiple turns. State is persisted to disk between invocations under a *workspace* and a *session id*. + +## The single-turn model + +Every `amplifier-agent run` call is one turn: + +1. Boot the engine (deserialize the prepared bundle, wire up the configured provider, install hooks). +2. Load transcript from disk if `--resume`. Wipe transcript if `--fresh`. +3. Submit the prompt and stream the result through the orchestrator (`loop-streaming`). +4. Persist the new turn to disk. +5. Exit. + +Multi-turn conversation is achieved by re-running with the same `--session-id` and `--resume`. There is no daemon; nothing persists in memory between turns. + +```bash +amplifier-agent run "What's 2+2?" --session-id math -y +# 4 + +amplifier-agent run "Add 3" --session-id math --resume -y +# 2+2+3 = 7 +``` + +## Workspaces + +A *workspace* isolates sessions belonging to one project. Resolution order: + +1. `--workspace ` flag. +2. `AMPLIFIER_AGENT_WORKSPACE` env var. +3. **Auto-derived** from `--cwd` (or process cwd if unset): `-`. + +Auto-derivation makes the slug deterministic per-directory and disambiguates same-name directories in different paths. Example for `/home/alice/work/my-project`: + +``` +my-project-7a3b8c9d +``` + +The base name is lowercased and slug-sanitized; the suffix is 8 hex chars of the sha256 of the absolute path. + +### Slug grammar + +``` +[a-z0-9][a-z0-9-]{0,63} +``` + +A leading underscore is reserved for internal workspaces: + +- `_legacy` — the target of the one-time migration of pre-workspace sessions (flat `state/sessions//` → `state/workspaces/_legacy/sessions//`). + +## The on-disk layout + +``` +~/.amplifier-agent/ ← $AMPLIFIER_AGENT_HOME, default +├── cache/ +│ └── prepared/ +│ └── 0.5.2/ ← +│ └── da41ba6300040dd9/ ← +│ ├── prepared.pickle ← Pickled PreparedBundle +│ └── manifest.json ← {"aaa_version", "bundle_sha256_prefix"} +├── config/ ← Reserved for future use +└── state/ + ├── .migrated_from_xdg ← Sentinel; XDG migration ran + └── workspaces/ + ├── _legacy/ ← Pre-workspace sessions migrated here + │ └── sessions/... + └── my-project-7a3b8c9d/ ← Auto-derived workspace slug + └── sessions/ + └── smoke-1/ ← + ├── transcript.jsonl ← One JSON message per line + ├── metadata.json ← Session metadata + ├── audits/ + │ └── turn-turn-1.json ← Per-turn audit + └── context-intelligence/ + ├── events.jsonl ← Kernel/delegate lifecycle events + └── metadata.json +``` + +### `transcript.jsonl` + +One JSON object per line. Each line is a kernel-side message: + +```jsonl +{"role": "user", "content": "Reply with just: pong", "metadata": {"timestamp": "..."}} +{"role": "assistant", "content": [{"type": "thinking", "thinking": "...", "signature": "..."}, {"type": "text", "text": "pong"}]} +``` + +The transcript is the source of truth for resume. `--resume` replays this file into the orchestrator's context before the new prompt is appended. + +### `metadata.json` + +Minimal — currently just a turn status marker: + +```json +{ "last_turn": "complete" } +``` + +### `audits/turn-.json` + +One file per `run` invocation. Only written when `--session-id` is provided (anonymous runs do not write audits). + +```json +{ + "argvDigest": "sha256:<64 hex>", + "envDigest": "sha256:<64 hex>", + "protocolVersion": "0.3.0", + "exitCode": 0, + "correlationId": "dd548f41-c9a4-4f08-b45b-203d9fc2b349", + "startedAt": "2026-06-12T08:22:02.255700+00:00", + "endedAt": "2026-06-12T08:22:23.695709+00:00" +} +``` + +Notes: + +- `argvDigest` is `sha256(' '.join(sys.argv))`. +- `envDigest` is currently a placeholder (`sha256({"extra": {}})`) — env allowlisting moved to the SDK side; the engine no longer captures the inbound env. +- `correlationId` matches the `correlationId` in the JSON envelope `error` and `metadata` fields, and in the NDJSON wire events. +- In Mode A, each `run` is "turn-1" — multiple runs against the same session ID **overwrite** the same audit file. The transcript and context-intelligence event log preserve the full history. + +### `context-intelligence/` + +Written by the `hook-context-intelligence` bundle. Captures kernel and `delegate`-tool lifecycle events: + +```jsonl +{"event": "session/start", "timestamp": "...", ...} +{"event": "delegate:agent_spawned", "agent": "explorer", ...} +{"event": "delegate:agent_completed", "result": "...", ...} +``` + +These events are local-only — no remote dispatch unless `AMPLIFIER_CONTEXT_INTELLIGENCE_SERVER_URL` is set (currently no support for setting it inside amplifier-agent). + +## Resume, fresh, and anonymous runs + +| Mode | Behavior | +|---|---| +| `--session-id ` only | New session. Creates `/sessions//` if missing. Writes transcript, metadata, audit. If the dir already exists, raises (use `--fresh` or `--resume`). | +| `--session-id --resume` | Loads existing transcript, replays it, then runs the new prompt. Falls back to *any other workspace* under `workspaces_root()` if not found in the current workspace (cross-workspace resume). | +| `--session-id --fresh` | **Deletes** `/sessions//` before booting. Use to wipe a corrupt session and start over. | +| (no `--session-id`) | **Anonymous run**. No audit is written. No session directory is created. Transcript still exists in-memory but is not persisted. | + +`--resume` includes transcript repair: if a previous run was interrupted leaving an orphaned tool call (a `tool_use` block with no matching `tool_result`), the loader injects a synthetic cancellation result so the orchestrator can proceed. This is what makes session resume robust across SIGTERM, crashes, and timeouts. + +## Listing and inspecting sessions + +There is no `sessions list` subcommand. Use the filesystem: + +```bash +# List all sessions in a workspace +ls ~/.amplifier-agent/state/workspaces//sessions/ + +# Find a session anywhere +find ~/.amplifier-agent/state -type d -name '' + +# Inspect a transcript +jq -c . < ~/.amplifier-agent/state/workspaces/.../sessions/.../transcript.jsonl | head + +# Inspect audits +cat ~/.amplifier-agent/state/workspaces/.../sessions/.../audits/*.json +``` + +## Migrations + +Two one-time migrations run automatically: + +### XDG → unified storage + +Pre-0.5.x amplifier-agent followed XDG conventions: `~/.local/state/amplifier-agent/`, `~/.cache/amplifier-agent/`, `~/.config/amplifier-agent/`. The current layout unifies everything under `~/.amplifier-agent/`. The migration: + +- Runs once after a successful `amplifier-agent update`. +- Moves `state/`, `cache/`, `config/` into the new root. +- Writes a sentinel at `~/.amplifier-agent/.migrated_from_xdg` so it never runs twice. + +### Flat sessions → workspace tree + +Sessions created under the previous flat layout (`state/sessions//`) are moved to `state/workspaces/_legacy/sessions//` on first runtime. This runs at most once per process and is idempotent. + +## Cleaning up + +There is no automatic GC. Clean up by hand: + +```bash +# Remove one session +rm -rf ~/.amplifier-agent/state/workspaces//sessions// + +# Remove one workspace +rm -rf ~/.amplifier-agent/state/workspaces// + +# Remove all sessions (preserves bundle cache) +rm -rf ~/.amplifier-agent/state/ + +# Nuke everything (sessions, cache, sentinel) +rm -rf ~/.amplifier-agent/ +``` + +The bundle cache is independent of sessions: + +```bash +amplifier-agent cache clear +# removes ~/.amplifier-agent/cache/prepared/* across all versions +``` + +Next `run` re-clones and re-installs the bundle modules.