Metadata-Version: 2.4
Name: rippletide-python
Version: 0.3.0
Summary: Monitor-first Python SDK for Rippletide-connected agents
License: UNLICENSED
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: opentelemetry-sdk==1.40.0
Requires-Dist: opentelemetry-exporter-otlp-proto-common==1.40.0

# Rippletide Python

`rippletide-python` connects a code-owned Python agent to Rippletide without
taking ownership of its model traffic. It has no provider dependencies: wrap a
hand-written provider or internal HTTP call at the existing model seam.

## Install and connect

The current release is `rippletide-python==0.3.0`. It requires
the platform's OTLP ingestion/readers and does not provide V1 trace compatibility.
Until it is published, validate it by installing a locally built wheel; do not
assume the unversioned registry install below contains candidate changes.

Requires Python 3.10 or later. From the existing agent repository, use its Python
environment, or create one if it has none:

```bash
python3 -m venv .venv
source .venv/bin/activate
python -m pip install rippletide-python
```

On Windows, activate with `.venv\Scripts\Activate.ps1` in PowerShell instead.
Keep the dependency in the project's requirements or package-manager manifest.

In Rippletide, choose **Connect an agent → Agent (Python)**. Save the generated
credentials in the repository's git-ignored `.env`, then apply the generated
prompt with your coding agent in that repository. The CLI also supports
`rippletide connect --kind agent --language python --print-prompt`.

Run a real turn that uses a tool and delivers a response. Confirm **Runtime
verified (5/5)** in Fleet and the run's tool calls and **Delivered** outcome in
Activity. Copying a prompt or storing credentials alone does not prove a
connection.

## Instrument existing call sites

The following fragment belongs inside the existing request handler; reuse the
agent's own provider, messages, tools, context, and response sink.

```python
from rippletide import Rippletide, load_rippletide_env

# Keep these values aligned with the agent's existing provider call.
PROVIDER_NAME = "existing-provider"
PROVIDER_API_KEY_ENV = "EXISTING_PROVIDER_API_KEY"
MODEL_OPERATION = "existing-provider.request"

load_rippletide_env("/path/to/connected-repo/.env")
rippletide = Rippletide(name="Support agent")

try:
    rippletide.register_prompts({"system": {"fallback": SYSTEM_PROMPT}})  # prompt text is withheld by default
    rippletide.tools("core", TOOLS)
    rippletide.connection(PROVIDER_NAME, env={"api_key": PROVIDER_API_KEY_ENV})
    with rippletide.run(name="inbound_request", input=request, conversation_id=conversation_id):
        with rippletide.agent(name="orchestrator"):
            call_model = rippletide.traced(raw_model_call, name=MODEL_OPERATION, kind="model")
            candidate = call_model(messages)
            return rippletide.deliver_response(
                request=request, response=candidate, context=trusted_context,
                deliver=send_to_caller,
            )
finally:
    rippletide.flush()
```

The CLI writes an agent-scoped `RIPPLETIDE=rippletide://…` connection string
into the connected repository's `.env`. `load_rippletide_env()` loads it without
overriding the process environment. The SDK also accepts the legacy
`RIPPLETIDE_API_KEY`, `RIPPLETIDE_AGENT_ID` (or `RIPPLETIDE_APP`), and optional
`RIPPLETIDE_BASE_URL` generated by the Connection page. A valid `RIPPLETIDE`
connection takes precedence over those legacy values; explicit constructor
arguments take precedence over either format. Never use a Platform key in the
agent or commit the `.env`.

## Async and streaming agents

The same client serves synchronous and asyncio code. `guard_tool_call()`,
`guard_tool_result()` and `deliver_response()` accept `async def` callbacks and
then return a coroutine to await; the policy round trip and receipts run in a
worker thread, so the event loop is never blocked. `run()`, `agent()` and
`span()` are plain `with` blocks in both cases. `traced()` wraps sync
functions, coroutine functions, and calls that return a provider stream: the
stream is passed through untouched and its span ends once the stream is
consumed, closed, or abandoned, in which case the span is marked
`stream_not_consumed`. Policy decides on the complete response: pass the text
the agent has assembled from the stream to `deliver_response()` before the
existing sink delivers it. When the tokens must reach the caller as they arrive,
pass the stream itself as `response`: chunks flow through to `deliver` untouched
and the decision is recorded on the assembled text once the stream ends, with no
suppression possible, so a BLOCK is marked as decided after delivery.
Use `await rippletide.aflush()` and
`await rippletide.ashutdown()` from asyncio code.

```python
async def handle(request):
    with rippletide.run(name="inbound_request", input=request):
        stream = rippletide.traced(open_model_stream, name=MODEL_OPERATION, kind="model")(messages)
        candidate = "".join([chunk.text async for chunk in stream])
        await rippletide.guard_tool_call(toolset="core", tool="lookup", params=args, execute=lookup_async)
        return await rippletide.deliver_response(request=request, response=candidate, deliver=send_async)
```

## Policy failures and capture

The client starts disabled when no Connection key is present. For
`guard_tool_call()` and `deliver_response()`:

- Network failures, 5xx responses, rate limits, and malformed or interrupted HTTP
  responses fail open: the protected callback can still run.
- An enforce-mode `BLOCK` raises `RippletidePolicyBlockedError` before the
  protected callback runs. An observe-only `WOULD_BLOCK` permits execution.
- HTTP `400`, `401`, `403`, or `404` from the policy decision endpoint raises
  `RippletidePolicyDecisionError` before the callback runs. These request,
  authentication, authorization, or routing failures are not swallowed as
  telemetry outages.

Result filtering has its own fallback rules, described below. Application input
validation and exceptions from the agent's own callbacks can also propagate.

Runtime instrumentation uses native OpenTelemetry spans and start logs, exported as
OTLP/HTTP JSON to `/v1/traces` and `/v1/logs`. Providers are local to the client;
the SDK does not replace application providers. The only profile is `full`, but
content is omitted unless `Rippletide(capture_content=True)` is explicit. Captured
content is bounded and credential-redacted, including nested Python mappings
such as `UserDict` and `MappingProxyType`, not generally PII-filtered. The old
capture modes are unsupported. `runtime_traces=False` disables evidence export
without disabling policy checks. Always call `flush()` before a short-lived
worker exits, and `shutdown()` when the client is no longer needed.

Tool and response policy spans include the invocation ID; once a decision arrives, their end
events and the protected action spans include its decision ID, including in
the default content-withheld posture. These IDs correlate the action with its policy receipt.

Prompt inventory is recorded with its content withheld by default. Pass
`allow_content_export=True` to `register_prompts(...)` only when the prompt
owner has explicitly approved storing the prompt text in Rippletide.

## Filter a completed tool result (explicit opt-in)

For an eligible collection returned by a tool, declare its result contract and
pass `allow_result_export=True`. This evaluates the complete result transiently
at Rippletide; do this only with the owner's explicit approval. It is false by
default. Transient policy failures and malformed or mismatched plans forward
every item unchanged. A missing result endpoint (`404` with no error code or
`NOT_FOUND`) also uses this fallback. Other deterministic request or permission
rejections raise `RippletidePolicyDecisionError` before `apply` runs.

```python
rippletide.tools("catalog", TOOLS, results={
    "list_products": {
        "resultSchema": {"type": "object", "properties": {"products": {"type": "array"}}},
        "resultCollection": {"itemsPath": ["products"], "itemIdPath": ["id"]},
    },
})

result = list_products(params)
return rippletide.guard_tool_result(
    toolset="catalog", tool="list_products", params=params,
    result=result, result_items=result["products"], context=trusted_context,
    allow_result_export=True,
    apply=lambda guarded: {**guarded["result"], "products": guarded["deliveredItems"]},
)
```

`ALLOW` and observe-only `WOULD_BLOCK` items are delivered; only enforce-mode
`BLOCK` items are excluded. The callback receives the original result snapshot,
the delivered and excluded items, and the decision. It is responsible for the
native response shape, and the SDK records the applied outcome as a receipt.
Result fingerprints use the server's ECMAScript JSON encoding, including its
number formatting and UTF-16 object-key ordering.

Python MCP servers remain unsupported. A response that is streamed to the
caller before it is complete cannot be protected by `deliver_response()`; the
SDK does not represent such a path as complete response-delivery protection.
