# llms-full.txt
# amplitude-ai 1.26.0 — Detailed API Reference for LLM Agents
# Use this file for instrumentation guidance. See llms.txt for discovery.

## Core API

### AmplitudeAI(amplitude)
Initialize the SDK. Required entry point.
```
import os
from amplitude import Amplitude
from amplitude_ai import AmplitudeAI
amplitude = Amplitude(os.environ["AMPLITUDE_AI_API_KEY"])
ai = AmplitudeAI(amplitude=amplitude, content_mode="full")
```

### patch(amplitude)
Zero-code instrumentation. Monkey-patches all detected provider SDKs.
```
from amplitude_ai import patch, unpatch
patch(amplitude=amplitude)
unpatch()
```

### wrap(client, amplitude)
Convert an existing provider client into an instrumented wrapper. Supports both sync and async clients.
```
from amplitude_ai import wrap
instrumented = wrap(openai_client, amplitude=amplitude)  # works with openai.OpenAI and openai.AsyncOpenAI
```

### ai.agent(agent_id, user_id)
Create a bound agent for user/session lineage.
```
agent = ai.agent("my-agent", user_id="u1")
child = agent.child("sub-agent")
with agent.session(session_id="s1") as session:
    ...  # LLM calls tracked automatically
    with session.run_as(child) as cs:
        ...  # delegate to child agent
```

### @tool / @observe
Decorators for automatic function tracking.
```
from amplitude_ai import tool, observe

@tool
def search_products(query: str) -> dict:
    return {'items': []}

@observe
async def handle_request(request):
    ...  # Emits [Agent] Span events with session lifecycle
```

### Experiments
Record A/B test or feature flag assignment with `experiments`, not `context`:
```
agent = ai.agent("support-bot", experiments=[{"key": "router", "variant": "fast"}])
with agent.session(user_id="u1", experiments=[{"key": "prompt", "variant": "v2", "provider": "statsig"}]) as s:
    s.track_user_message("Hi", experiments=[{"key": "retry", "variant": "on", "unit": "turn"}])
```
Every event carries `[Agent] Experiments` (an array of `"key:variant"` strings; split charts by it) and, when exactly one experiment applies, also `[Agent] Experiment Key` / `Variant` / `Unit` / `Provider`. Call entries override session entries, which override agent entries, by key; `child()` and `run_as()` inherit them. `unit` is `agent_session` (default), `turn`, or `span`. Key, variant, and provider are up to 256 characters, keys without `:`; at most 32 per event; invalid entries are dropped with a debug log.
Take the assignment from the flag tool in use: `experiments=experiments_from_amplitude(variants, keys=[...])` for an Amplitude Experiment `fetch_v2()` / `evaluate_v2()` / `all()` result (`provider: "amplitude"`, fallback variants skipped; `keys` names the experiment flags, otherwise every flag is recorded and more than 32 warns); `openfeature.api.add_hooks([amplitude_ai.openfeature.AmplitudeAIHook(flags=[...])])` for any OpenFeature provider (each listed flag evaluated inside a session is recorded with the provider's name; `variant_of=lambda key, value: ...` names the arm for object-valued flags such as Statsig experiments); `session.add_experiments([...])` for an assignment learned after the session was entered. Neither sends an exposure event.

### MockAmplitudeAI
Deterministic testing helper. Drop-in replacement.
```
from amplitude_ai import MockAmplitudeAI
mock = MockAmplitudeAI()
mock.get_events("[Agent] AI Response")  # → list of events
mock.flush()  # → list of events
print(mock.summary())  # fill-rate report with 8 verification gates
```

### OTEL Span-First Mode
Optional OpenTelemetry integration. OTEL spans become the core primitive,
mapped to [Agent] events by the built-in SpanEventMapper.
```
pip install 'amplitude-ai[otel]'

ai = AmplitudeAI(amplitude=amplitude)
ai.enable_otel()  # registers AmplitudeEventSpanProcessor

# @observe(type=...) creates OTEL spans when enabled
@observe(type="tool")
def search(query: str) -> dict:
    return {'results': []}

# Attach attributes to current span
with ai.using_attributes({"customer_tier": "enterprise"}):
    response = client.chat.completions.create(...)

# Set attributes on current span/trace
ai.update_current_span({"key": "value"})
ai.update_current_trace({"deploy_env": "prod"})
```

### Session Tags
Attach custom tags to all events within a session.
```
with agent.session(session_id="s1", tags=["beta", "experiment-a"]) as s:
    ...  # all events get [Agent] Tags = ["beta", "experiment-a"]
```

### Git Metadata
Auto-captured from env vars (AMPLITUDE_GIT_SHA, GIT_SHA) or git CLI.
Appears as [Agent] Git SHA, [Agent] Git Ref, [Agent] Git Repo on events.

### Coding agents
`amplitude-ai coding-agents init` instruments the AI coding agents that work on a repo. It writes `.githooks/prepare-commit-msg` (adds `Agent-Session-Id` / `Agent-Name` trailers to agent commits) and `.github/workflows/agent-outcomes.yml` (Commit Merged / Commit Reverted / Pull Request Merged / Deploy Succeeded / Deploy Failed, each with `agent_setup_version`, a hash of the agent setup files listed in `AMPLITUDE_AGENT_SETUP_PATHS`; re-running init upgrades an untouched earlier version), merges Claude Code OTLP settings into `.claude/settings.json` the Codex Stop hook (`amplitude-ai hook codex`) into `.codex/hooks.json`, Cursor's agent hooks (`amplitude-ai hook cursor`) into `.cursor/hooks.json` and Gemini CLI telemetry into `.gemini/settings.json`, prints goose's and Copilot's OTLP env, and sets `core.hooksPath`. The API key only goes to gitignored `.claude/settings.local.json` and `.amplitude-ai/local.env`.
Set `AMPLITUDE_SESSION_TRIGGER` (`interactive`, `headless`, `ci`, `scheduled`, `event`) where the harness can't tell how a session started; it becomes `[Agent] Session Trigger` and the commit's `Agent-Trigger` trailer (CI runs are `ci` without it). The workflow stamps `agent_trigger`, and `Pull Request Merged` adds `agent_triggers`, `hours_open`, `hours_to_first_review` and `review_rounds`. Codex and Cursor events carry `[Agent] Root Agent Name` and `[Agent] Agent Chain Depth`.
Tool Calls that create a commit carry `commit_sha` (Codex hook, `CodexSDKTracker`, Claude Agent SDK tracker).
```
from amplitude_ai.integrations.codex_sdk import CodexSDKTracker
tracker = CodexSDKTracker(model="gpt-5-codex")
with agent.session(user_id="u1") as s:
    tracker.track_input(s, prompt)
    for event in thread.run_streamed(prompt):
        tracker.process(s, event)
    tracker.end_session(s)
```

### Cross-process agent context (A2A / ACP / HTTP)
```
from amplitude_ai.propagation import declare_a2a_extension, extract_a2a_metadata, inject_a2a_metadata, inject_a2a_response_metadata
# AMPLITUDE_AI_CONTEXT_KEYS='{"k1": "<32+ byte secret>"}' on both sides signs and verifies the context
message.metadata.update(inject_a2a_metadata())            # caller, inside a session; identity="none" by default
ctx = extract_a2a_metadata(request.message.metadata)      # receiver: pass the whole result
with agent.session(context=ctx) as s:                     # verified: continue the caller; otherwise: fresh session + [Agent] Peer *
    reply.metadata = inject_a2a_response_metadata(reply.metadata)
s.track_tool_call(tool_name="ask_peer", latency_ms=ms, success=True, peer=extract_a2a_metadata(reply.metadata))
card = declare_a2a_extension(card)                         # AgentCard capabilities.extensions (required: false)
# ACP: inject_acp_meta() / extract_acp_meta(); HTTP: inject_context() / extract_context(), or AmplitudeAIMiddleware(accept_context=True)
# Business events: event_properties={**ai.context_properties()} (never a user ID)
```

## Provider Wrappers

| Provider     | Import                                   | Streaming | Tool Calls | TTFB | Cache Tokens |
|--------------|------------------------------------------|-----------|------------|------|--------------|
| OpenAI       | `from amplitude_ai import OpenAI`        | Yes       | Yes        | Yes  | Yes          |
| AsyncOpenAI  | `from amplitude_ai import AsyncOpenAI`   | Yes       | Yes        | Yes  | Yes          |
| Anthropic    | `from amplitude_ai import Anthropic`     | Yes       | Yes        | Yes  | Yes          |
| Gemini       | `from amplitude_ai import Gemini`        | Yes       | No         | No   | No           |
| AzureOpenAI  | `from amplitude_ai import AzureOpenAI`   | Yes       | Yes        | Yes  | No           |
| Bedrock      | `from amplitude_ai import Bedrock`       | Yes       | Yes        | No   | No           |
| Mistral      | `from amplitude_ai import Mistral`       | Yes       | No         | No   | No           |

## Canonical Patterns

### 1. Zero-code (patch)
```python
import os
from amplitude import Amplitude
from amplitude_ai import AmplitudeAI, patch
amplitude = Amplitude(os.environ["AMPLITUDE_AI_API_KEY"])
ai = AmplitudeAI(amplitude=amplitude)
patch(amplitude=amplitude)
```

### 2. Wrap existing client
```python
from amplitude_ai import wrap
instrumented = wrap(existing_client, amplitude=amplitude)
```

### 3. Provider wrapper
```python
import os
from amplitude_ai import AmplitudeAI, OpenAI
client = OpenAI(amplitude=ai, api_key=os.environ["OPENAI_API_KEY"])
response = client.chat.completions.create(model="gpt-4o", messages=[...], amplitude_user_id="u1")
```

### 4. Bound agent + session
```python
agent = ai.agent("assistant", user_id="u1")
with agent.session(session_id="s1") as session:
    response = client.chat.completions.create(...)
```

### 5. FastAPI middleware
```python
from amplitude_ai.middleware import AmplitudeAIMiddleware
app.add_middleware(
    AmplitudeAIMiddleware,
    amplitude_ai=ai,
    user_id_resolver=lambda request: request.state.user.id,  # authenticated user, not a request header
)
```

### 6. Multi-agent orchestration with session.run_as()
```python
orchestrator = ai.agent("orchestrator", user_id="u1")
researcher = orchestrator.child("researcher")
with orchestrator.session(session_id="s1") as s:
    # Provider calls inside run_as are automatically tagged with the child's agent_id
    with s.run_as(researcher) as cs:
        response = client.chat.completions.create(model="gpt-4o", messages=[...])
# run_as shares session_id, trace_id, turn counter; does NOT emit Session End
```

## MCP Tools

- `get_event_schema(event_type?)` — Return event schema and property definitions
- `get_integration_pattern(pattern_id?)` — Return canonical instrumentation patterns
- `validate_setup()` — Check required environment variables
- `suggest_instrumentation(framework?, provider?, content_tier?)` — Value-first setup guidance with content-tier and privacy defaults
- `validate_file(source, language?)` — Detect uninstrumented LLM call sites
- `search_docs(query, max_results?)` — Search README, llms-full.txt, and amplitude-ai.md by keyword

## CLI

- `amplitude-ai` — Print setup prompt for AI coding agents; `--print-guide` outputs full guide
- `amplitude-ai mcp` — Start the MCP server for AI coding agents
- `amplitude-ai doctor [--json] [--no-mock-check]` — Validate environment, event pipeline and agent context keys (`agent_context.keys`, `agent_context.unsigned_http`, `agent_context.pairwise_salts`; secrets never printed)
- `amplitude-ai doctor --verify '<envelope>' [--transport a2a|acp|http] [--json]` — Verify one A2A metadata / ACP `_meta` / HTTP header envelope against `AMPLITUDE_AI_CONTEXT_KEYS` and explain the result (kid, iat/exp, clock delta); exits non-zero unless `ok`
- `amplitude-ai status [--json]` — Show SDK version, installed providers, and env config
- `amplitude-ai coding-agents init [--harness claude-code|codex|goose|cursor|gemini|copilot]... [--privacy full|metadata] [--region us|eu] [--dry-run] [--json]` — Commit hook, outcomes workflow, Claude Code / Codex / Cursor / Gemini CLI settings, goose and Copilot env guidance
- `amplitude-ai coding-agents doctor [--harness ...] [--json]` — Check hooksPath, hook, workflow, harness settings and API key presence (never printed)
- `amplitude-ai hook codex` — Codex Stop hook (JSON on stdin); forwards new rollout lines as [Agent] events; always exits 0
- `amplitude-ai-register-catalog` — Push event catalog to Amplitude Data

## Common Errors

- "No events captured" → Ensure session context wraps your LLM calls
- "patch() drops events silently" → patch() requires active SessionContext
- "flush() timeout" → Call ai.flush() before process exit in serverless
- Import errors → Install optional deps: pip install 'amplitude-ai[openai]'

---

## Content shaping (excerpt from amplitude-ai.md)

Full guide: `amplitude-ai.md` or MCP resource `amplitude-ai://instrument-guide`.

**User message text vs structured pipeline data (critical for Agent Analytics UI):**

- `s.track_user_message(...)` **`content`** becomes **`$llm_message.text`** on `[Agent] User Message`. That string is what **session lists, segmentation, and enrichment** treat as “what the user said.”
- **Do not** pass large JSON blobs, RAG context packs, or internal pipeline state as the **only** user message body — the product will show that JSON as the session title and break down charts by raw JSON.
- **Do** pass a **short natural-language** line (the real end-user prompt, or a canonical summary for headless jobs, e.g. `"Summarize the attached design doc and list open questions"`).
- Put structured segmentation dimensions in **`context=`** (dict → `[Agent] Context` JSON, always queryable in charts). Avoid `event_properties` for custom dimensions on `[Agent]` events — they arrive, but they are outside the cataloged `[Agent]` schema, and projects that block unplanned properties will reject them.

```python
# GOOD: readable user line + structured state in context
s.track_user_message(
    "Summarize the attached design doc and list open questions",
    context={"structured_payload": payload_dict},
)

# BAD: entire pipeline state as the "user message" (shows up as session label / $llm_message.text)
s.track_user_message(json.dumps(payload_dict))
```

**Provider wrapper auto-tracking caution:** Wrapped providers automatically emit `[Agent] User Message` for every `role: "user"` message in the LLM request. **Do not call `s.track_user_message()` for the same user input you are also passing to a provider wrapper** — you will get two `[Agent] User Message` events for a single user turn. Choose one: either call `s.track_user_message()` manually and suppress auto-tracking, or rely on the provider wrapper and omit the manual call. If internal delegation calls include structured prompts as `role: "user"`, those will appear as duplicate user turns unless you use `run_as()` / `arun_as()` (which suppress auto user-message tracking). See **Step 3g-b** for the canonical fan-out pattern.

Structured content channels, including tool input/output, attachments, and span state, recognize complete JSON object/array strings up to 100,000 characters, within the runtime parser limits. Built-in PII redaction applies to decoded string values and object keys. Numeric values under common phone, mobile, card/PAN, and SSN field names become quoted redaction placeholders, including values in nested containers and serialized JSON strings. Field matching ignores case and punctuation (for example, `phone_number`, `creditCardNumber`, and `social-security-number`). Other numeric tokens, including timestamps and large integers, retain their exact spelling. If built-in redaction would merge distinct object keys, the serialized content becomes `[content redacted]` instead of losing a field. This correction applies to serialized JSON; native object keys and numeric values retain their existing behavior. Unlabelled numbers and unrecognized field names cannot reliably identify PII: sanitize sensitive source fields or use a custom redactor. Custom patterns and callbacks retain their original string boundary and run once after built-in redaction, followed by base64 redaction. If these later steps make an originally valid JSON document invalid, the SDK emits `[content redacted]`. Native parser limits or excessive nested JSON-string depth also fail closed. Numeric-looking strings, including timestamps embedded in HTML source, remain ordinary redaction. Malformed or oversized JSON follows the ordinary text-redaction and truncation path; existing final size limits can still truncate output.

**Experiments.** If the agent runs under an A/B test or feature flag (a prompt, model, router, or tool variant), pass the assignment as `experiments`. Do not put it in `context` or `event_properties`:

```python
with agent.session(user_id=user_id, experiments=[
    {"key": "planner", "variant": flags.get_variant("planner"), "provider": "statsig"},
]) as s:
    ...
```

Rules (follow these, do not guess):

- **Use the assignment the app already computes.** Find the flag client call (Amplitude Experiment, Statsig, LaunchDarkly, PostHog, GrowthBook, Optimizely, or in-house) and pass its key and variant. If there is no experiment, add nothing.
- **Where to set it:** `ai.agent(..., experiments=...)` when the whole agent is under the variant, `agent.session(..., experiments=...)` when it's assigned per session, `experiments=` on one `agent.track_*` / `s.track_*` call for a single turn. A session entry overrides an agent entry with the same key; a call entry overrides both. Child agents (`agent.child()`, `s.run_as()`) inherit them.
- **`unit`** is what was randomized: `"agent_session"` (the default), `"turn"`, or `"span"`. **`provider`** is free text; use lowercase `amplitude`, `statsig`, `launchdarkly`, `posthog`, `growthbook`, `optimizely`, or `custom`.
- **Limits:** key, variant, and provider are non-blank strings of up to 256 characters; keys can't contain `:`. At most 32 experiments per event. Invalid entries are dropped with a debug log, never raised.
- **Sent as** `[Agent] Experiments` (`["planner:v2"]`, an array with one `key:variant` string per experiment) on every event and, when exactly one experiment applies, also `[Agent] Experiment Key`, `Variant`, `Unit`, and `Provider`. Split any Agent Analytics chart by `[Agent] Experiments` to compare arms; `[Agent] Experiment Variant` works when one experiment applies.
- **Existing `context={"experiment_variant": ...}` code keeps working**; when you touch it, move the value to `experiments` instead of writing it in both places.
- **Prefer the helper for the flag tool in use.** Amplitude Experiment: `experiments=experiments_from_amplitude(variants, keys=[...])` where `variants` is the `fetch_v2()` / `evaluate_v2()` / `all()` result and `keys` names the experiment flags (sets `provider: "amplitude"`, skips fallback variants; without `keys` every flag is recorded and more than 32 warns). Any OpenFeature provider (LaunchDarkly, Statsig, GrowthBook, Flagsmith, Unleash, ...): register `amplitude_ai.openfeature.AmplitudeAIHook(flags=[...])` once with `openfeature.api.add_hooks([...])` and every listed flag evaluated inside a session is recorded with the provider's name lowercased (without `flags` every evaluation is recorded, configuration flags included); add `variant_of=lambda key, value: ...` when the provider returns an object (Statsig experiments and dynamic configs) so the arm name comes out of it. Other SDKs: build the entry from the evaluation and call `session.add_experiments([...])` (or the module-level `amplitude_ai.add_experiments`) right after the flag read; events tracked from then on carry it.

### HTML artifacts

HTML artifact envelopes are ordinary structured content in tool input/output, AI/user attachments, and span input/output. Use the installed SDK version's tracking APIs; no artifact-specific preparation API is required. In `full` mode, existing string redaction and custom patterns/callbacks apply; `metadata_only` and `customer_enriched` withhold these content fields while retaining the underlying events.

The producer owns HTML privacy and fidelity. Select source data approved for analytics capture before constructing HTML. SDK redaction is best effort over source strings: it does not parse HTML, decode entities, interpret CSS, or guarantee removal of sensitive text assembled by rendering. HTML is not rewritten or omitted merely because of its markup. The viewer's HTML security controls remain a separate boundary; SDK capture does not make HTML safe to render.

Preflight the complete serialized field, including its envelope, attachments, and siblings, against SDK and destination limits. Python's existing serializer caps these fields at 100,000 characters and can append `...[truncated]`, which can break JSON or HTML; source strings also have existing redaction limits. Budget for escaping and redaction output, and inspect a representative emitted event. Use a compact representation only if it preserves all meaningful content. If the artifact still cannot fit intact, report the oversize condition and omit the optional artifact at the producer while preserving the underlying event; do not silently remove meaningful content to fit. The SDK does not perform an artifact-specific size rewrite.
