Metadata-Version: 2.4
Name: mirrors-sdk
Version: 0.1.3
Summary: Background trace capture and bounded optional context for Python agents
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Description-Content-Type: text/markdown
Requires-Dist: httpx<0.29,>=0.28.1

# Mirrors Python SDK

Capture original conversation history in the background and retrieve optional
context before assembling a model request. Your application keeps control of its
system prompt, model calls, tools, streaming, errors and cancellation.

Python 3.11 or later. Install or upgrade to the latest stable Mirrors SDK release with
your application's dependency manager; for pip:

```sh
python -m pip install --upgrade mirrors-sdk
python -c "from importlib.metadata import version; print(version('mirrors-sdk'))"
```

## Installation skill

This source includes an agent installation skill for synchronous and async Python
applications. From this checkout, read
[`install-mirrors-python-sdk/SKILL.md`](src/mirrors_sdk/skills/install-mirrors-python-sdk/SKILL.md).
Packages built from this source include the same skill. Read it without guessing
a virtual environment path (the initial PyPI release predates this resource):

```sh
python -c "from importlib.resources import files; print(files('mirrors_sdk').joinpath('skills/install-mirrors-python-sdk/SKILL.md').read_text())"
```

It verifies Python publication before selecting a current package-index release,
with a reviewed source checkout fallback when no official release is available.

## Synchronous integration

```python
import os
from uuid import uuid4

from mirrors_sdk import Mirrors

mirrors = Mirrors(
    api_url=os.environ["MIRRORS_API_URL"],
    api_key=os.environ["MIRRORS_API_KEY"],
    agent_id=os.environ["MIRRORS_AGENT_ID"],
)
execution_id = str(uuid4())

# Map the application's complete original history to canonical messages.
trace = {
    "id": execution_id,
    "messages": [{"role": "user", "content": "Help with my account"}],
}
mirrors.capture_trace(trace)  # Returns immediately; do not wait before a model call.

selected = mirrors.get_context(trace, timeout_ms=1000)
system = original_system_prompt
if selected.context:
    system += "\n\n" + selected.context

# The application's existing model call and error/stream handling stay here.
response = existing_model_call(system=system, messages=original_messages)
```

Appending the returned text is synchronous. `get_context` is also a synchronous
Python method, but it may wait for optional network context for **at most 1,000 ms
from call entry**, including snapshot copying and scheduling. Use a shorter
`timeout_ms` from 1–1000 to reduce that wait. Timing is bounded best effort, not a
hard real-time guarantee while Python or the operating system cannot schedule work.
When a wait resumes, the SDK discards context whose deadline or freshness expired;
`elapsed_ms` includes that wait through the public return check.
It does not wait for capture delivery. No user event loop or async framework is
required. Async applications may use `await mirrors.aget_context(trace)` to avoid
blocking their event loop.

The SDK starts its mode heartbeat in the background. Until a valid mode lease is
available, or after it expires, retrieval immediately returns empty local shadow
fallback. Confirmed shadow returns empty while evaluating in the background; off
does not retrieve context. Capture continues in every server mode. A successful
response contains `context`, `mode`, `delivery`, `reason`, `elapsed_ms` and nullable
`freshness`; `delivery == "accepted"` means receipt, not application or model success.
Use text promptly and check its `freshness` expiry if dispatch is delayed.

Copy the URL and agent ID from the agent's SDK connection settings. Keep keys in
server secrets with runtime read/write permissions. The SDK refuses redirects and
requires HTTPS, except for literal loopback development addresses. It never logs
keys, requests or conversation contents.

## Original evidence and final capture

Canonical messages preserve original roles (`system`, `user`, `assistant`, `tool`),
text, reasoning, block order and tool call/result IDs. Preserve original history;
do not include SDK context in evidence or invent missing associations. Capture
copies accepted input so later caller mutations cannot change an admitted request.
Oversized, cyclic, non-JSON or custom-object inputs are rejected without truncation.

After the execution and its response stream have ended, capture the full final
history:

```python
ticket = mirrors.capture_trace(
    completed_original_trace,
    final=True,
    terminal="completed",  # Or "failed" / "canceled"; not a task-success label.
)
# Inspect outside the model path. wait() takes seconds and never waits indefinitely.
receipt = ticket.wait(timeout=2)
```

`ticket.receipt.status` distinguishes `pending`, `accepted`, `dropped` and
`unconfirmed`. A timeout or connection loss may follow a server commit, so
`unconfirmed` does not mean rejected. Accepted ingestion with `status == "queued"`
does not mean analysis has finished. Retries retain the same immutable request
identity; there is no infinite retry loop or durable crash queue.

Use a fresh execution ID after restart, finalization or `release_trace(id)`. Release
only performs local cleanup; it does not claim server completion. For a conversation
spanning executions, supply both canonical fields `conversationId` and
`conversationRevision`, with a durable, atomically allocated increasing revision.
Keep that pair and source metadata fixed throughout one execution. Optional
`user: {"id": ..., "segment": ...}` uses opaque internal IDs, never email addresses.
These canonical JSON field names match the TypeScript SDK and wire protocol.

For whole-turn latency, call `turn = mirrors.start_turn()` at user-message acceptance
and `turn.finish()` after the final response, including tools, streaming and retries.
The first finish freezes monotonic milliseconds. Supply it as `turnLatencyMs` on the
final trace only; omit unknown duration. Timing does not imply success.

## Optional system prompt per user turn

Use one execution ID per user turn. When the installed SDK and deployed API/worker
readers support it, supply that turn's original system prompt once on the trace:

```python
from uuid import uuid4
from mirrors_sdk import AgentContext, CanonicalTrace

agent_context: AgentContext = {"systemPrompt": original_system_prompt}
turn_trace: CanonicalTrace = {
    "id": str(uuid4()),
    "messages": original_messages,
    "agentContext": agent_context,
}
# Optional retrieval or intermediate captures can use the same trace.
# After this turn's tools and response stream finish:
mirrors.capture_trace(
    {**turn_trace, "messages": completed_original_messages},
    final=True,
    terminal="completed",
)
```

The server places the supplied prompt once before the trace corpus in Luna's
analysis input. Do not attach it to each user message or synthesize extra system
messages. Preserve system messages already present in the original history; the
analysis formatter replaces exact repeated copies with references while retaining
the original stored evidence. The SDK itself does not alter messages or call Luna.

Capture the application's prompt before adding Mirrors-returned context. Include
`agentContext` on the final snapshot to retain it: omission does not inherit an
earlier prompt or another turn's value. Omit the field when unknown; an empty
`systemPrompt` explicitly means no prompt. The prompt is not client-wide state,
and this field does not discover or load skills.

Python currently accepts only `systemPrompt` inside `agentContext`. Its limit is
100,000 UTF-16 code units, matching the service's character limit, and 262,144 UTF-8
JSON bytes for the context object. It also counts toward the existing snapshot
limit. Invalid or oversized input is rejected without truncation. Enable this
field only after compatible API and worker readers are deployed; upgrading the
package alone does not deploy them.

## Shutdown and emergency bypass

```python
# At shutdown, outside the agent's model path:
summary = mirrors.flush(timeout_ms=2000)
mirrors.dispose()
```

`flush` waits only for capture tickets pending when called and reports accepted,
dropped, unconfirmed and pending counts. Its `complete` flag is true only when every
selected ticket was accepted. `close(timeout_ms=2000)` combines a bounded flush
and disposal. `dispose()` immediately disables optional work without waiting for
network completion. Background threads do not keep the Python process alive, so
process exit may abandon captures; flush explicitly when confirmation matters.

`set_enabled(False)` cancels optional work and clears active state. Re-enabling
requires fresh execution IDs. Invalid configuration stays inert and yields explicit
fallback receipts. SDK failures do not invoke, retry or replace application model
calls. Cancellation passed as `threading.Event` to `get_context(..., cancel=event)`
returns a distinct canceled result; preserve the application's own cancellation
check before invoking its provider. Async task cancellation propagates normally.

## Limits and design tradeoffs

| Limit | Value |
| --- | --- |
| Encoded snapshot | 1,000,000 bytes; 10,000 messages; 50,000 JSON nodes; depth 32 |
| Optional system prompt | 100,000 UTF-16 code units; context object at most 262,144 UTF-8 JSON bytes |
| Pending captures | 16 prefixes and 16 finals, separately |
| Active execution state / recent final receipts | 64 / 128 |
| Pending context retrievals | 16, independent of capture |
| Capture timeout defaults | 1 second prefix; 6 seconds final |
| Explicit capture `timeout_ms` | 1–30,000 ms |
| Context / response bytes | 32,768 / 65,536 |
| Circuit default | 3 failures, 30-second cooldown, one recovery probe per endpoint |
| Mode heartbeat / lease | 1 second with jitter / at most 3 seconds |

One daemon event-loop thread and one HTTPX client own network work; at most two
daemon DNS lookups isolate stalled system resolution. The benefit is cancellable,
pooled HTTP for synchronous callers without a thread per upload. The cost is the
HTTPX dependency and best-effort background delivery. Endpoint circuits
are independent, so a successful capture cannot hide context failures. Uncertain
captures reconcile before newer ordinals; final retries share their original budget.

The package mirrors the current TypeScript `captureTrace` / `getContext` boundary.
It does not port the legacy session adapters or ready-only injection helpers, and
it contains no private policy engine or provider integration. Tests cover local
protocol behavior and package installation; they do not establish PyPI publication,
hosted service compatibility, production latency or model-quality improvement.

## Development

```sh
python -m pip install ./packages/python-sdk build
python -m unittest discover -s packages/python-sdk/tests -v
python -m build packages/python-sdk
```

CI runs behavioral and real loopback HTTP tests on Python 3.11, 3.13 and 3.14, then
builds and installs the wheel in a clean environment. No test calls a real provider
or a hosted Mirrors service.
