Metadata-Version: 2.5
Name: schift-observer
Version: 0.2.0
Summary: Thin native client for hosted Schift Observer
License-Expression: Apache-2.0
License-File: LICENSE
Requires-Python: >=3.11
Description-Content-Type: text/markdown

# Schift Observer Python

A thin, Apache-2.0 instrumentation SDK for the private hosted Schift Observer service.
Python 3.11+, no runtime dependencies. No local Context State Management engine,
selection algorithm, Agent loop, replay store, evaluation engine, or subprocess.

```python
from schift_observer import ObserverClient, ReActAdapter

client = ObserverClient()  # reads SCHIFT_OBSERVER_API_KEY
run = client.start_run(agentId="support", sessionId="session-1", model="model", provider="custom")
run.record_candidate(srn="srn:document:policy", contextManagementCallId="call-1")
run.record_selection(selected=True, srn="srn:document:policy", contextManagementCallId="call-1")
run.record_assembly(contextManagementCallId="call-1", tokenUsage=120)
ReActAdapter(run).connector("drive.search", "connector-run-1", modelTurnId="turn-1")
run.end_run(status="completed")
client.close(timeout=5)
```

Configuration is `{"schema":"schift.observer.config.v1","observer":{...}}`.
`api_key_ref` accepts `env://UPPERCASE_NAME`; `project_id`, `sampling`, `retention`,
`redaction` and bounded batching/retry controls follow the shared JSON schema.
`endpoint` defaults to the gateway, `https://gw.schift.io/v1/observer/events`, and also
accepts `https://api.schift.io/v1/observer/events` so existing 0.1.x configurations keep
working (exact match; nothing else). No custom transport or local-mode override is
supported. Missing key creates no worker and stores no events. Any HTTP 401 or 403 (for
example a revoked key, or an organization that is not enabled) stops delivery for the life
of the process; `health()` then reports `entitlement` `"revoked"` and counts `unauthorized`
drops. The server owns entitlements, usage, retention and evaluation decisions.
Allowances, retention and overage are set by your account's plan and shown in the console
and on the pricing page. When ingestion is not allowed, the server answers HTTP 402 and
that batch is dropped as `quota` without a retry.
`last_receipt()` returns a defensive copy of the last successfully enabled server receipt;
disabled or revoked receipts stop delivery without being reported as accepted delivery.

Recording is synchronous in-memory work and never waits for HTTP. One daemon
worker batches bounded events. Call `flush(timeout=...)` or `close(timeout=...)`
for delivery at shutdown; they return false if the caller's wait expires. Python
async applications use `await client.aflush()` / `await client.aclose()`; these
stdlib wrappers offload the wait and do not block the event loop. HTTP timeouts
bound socket operations and a monotonic deadline bounds streamed body reads (a
blocking socket operation may overrun that deadline by one socket timeout); caller flush deadlines bound shutdown waiting. Delivery
is best effort, with stable IDs across retries; hosted ingestion must deduplicate.
Events accepted by the in-memory queue can be lost at abrupt process exit.

Metadata-only is the default and removes payload/reason. `redaction="redacted"`
requires a synchronous `sanitizer=` callback; exceptions or invalid output drop
that event. `redaction="full_opt_in"` explicitly consents to payload capture.
All modes strip credential-shaped fields and embedded secrets before queueing.
Sanitizers execute on the recording thread and must be cheap; network work belongs
in the customer's Agent, not in a sanitizer. `health()` matches the TypeScript fields:
`hosted`, `degraded`, `entitlement`, `queuedCount`, `deliveredCount`, `dropReasons`,
and `closed`, plus the Python missing-reference `diagnostic`. It never exposes raw
errors or credentials.

`ReActAdapter`, `CustomAgentAdapter`, `OpenAICompatibleAdapter`, `LangChainAdapter`
and `LangGraphAdapter` expose hooks only. Framework callbacks keep nested run IDs
but do not treat arbitrary chain execution as Context assembly. Use explicit
`record_assembly` for actual assembly. The LangChain callback facade has no
LangChain dependency; it accepts callback-manager flags and standard run kwargs.

`LangChainAdapter` records into the run you pass in, and your code owns that run's
lifecycle, so one run can also carry your own ReAct, connector and cache records. Close
it on every path: `run.end_run(status="completed")` on success and
`run.end_run(status="failed")` when the chain raises; an unclosed run keeps its session
`running`.

Feedback pruning (gateway endpoint, the default): a `context_feedback_recorded` event makes
the server's judge decide whether each mutable item selected in the same session and run
can be dropped; other block kinds are never judged, and items without captured text
(`payload.text`/`payload.content`, or the feedback's `payload.contexts`) are recorded as
`no_content` without judging. `client.get_revisions(session_id)` (or
`await client.aget_revisions(...)`) returns the applied discards; leave those srns out of
the next assembly and record `context_excluded` with `transition="discard"`.
Failures raise `RevisionsError` with a transport `reason` (`disabled` without a key).

Model callbacks project only standard
`response.generations[].message.usage_metadata.total_tokens` into `tokenUsage`.
All generations must provide valid nonnegative safe integers; missing or malformed
usage is omitted. Completed callbacks are counted once. Tool callbacks use their
tool run ID as `correlationId` and do not invent a `modelTurnId`. Retriever
callbacks use `searchOperationId`. Message and tool content is never copied by
these framework hooks, including when payload capture is explicitly enabled.
