Metadata-Version: 2.4
Name: meander-agent
Version: 0.1.0
Summary: Client-side meander.plan SDK: transport setup + a form-checked plan onto a real OTel span, with thin Claude/OpenAI bindings. Imports meander NOT.
Author-email: Raphael Feikert <r.feikert@symbolic-intelligence.de>
Requires-Python: >=3.11
Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30
Requires-Dist: opentelemetry-sdk<2,>=1.30
Provides-Extra: claude
Requires-Dist: claude-agent-sdk<0.3,>=0.2; extra == 'claude'
Provides-Extra: openai
Requires-Dist: openai-agents<0.18,>=0.17; extra == 'openai'
Provides-Extra: test
Requires-Dist: pytest-randomly>=3; extra == 'test'
Requires-Dist: pytest>=8; extra == 'test'
Description-Content-Type: text/markdown

# meander-agent

Kundenseitiges SDK für die **Plan-Sende-Seite** von meander. Dein Agent hält
fest, was er getan hat, als nachvollziehbare Behauptung mit Herkunft, und stellt
GENAU EINE Frage an eine Regel deiner Ontologie. Das Paket baut daraus ein
`meander.plan`, prüft dessen FORM und hängt es an einen echten OpenTelemetry-Span,
den dein OTLP-Export zu meander trägt. Kein meander-Import, keine Ontologie im
Client.

## Das Modell in einem Absatz

Ein `meander.plan` ist eine **Behauptung plus eine Frage**, keine Entscheidung:

- **facts** — was der Agent beobachtet/eingeschätzt hat. Jeder Fakt trägt SEINE
  Herkunft (`origin.kind`: `tool` | `api` | `human` | `agent`). Ein Tool-Ergebnis
  ist `tool`, die eigene Einschätzung des Agenten ist `agent`.
- **relations** — behauptete Beziehungen zwischen Entities (optional).
- **question** — GENAU EINE Frage an eine bestehende Derivation (Regel) deiner
  Welt, z.B. „gilt hier `requires_review`?". Der Agent **entscheidet nicht**; ob
  die Regel greift, bestimmt meander serverseitig, und ein Mensch prüft.

Der Client prüft nur die **FORM** (Pflichtfelder, Typen, genau eine Frage,
`origin`-Vokabular, `plan_version == 1`). Ob die Entities/Properties/Derivations
wirklich existieren, weiß nur der Server (er kennt deine Ontologie). Darum
importiert dieses Paket meander NICHT und lädt keine Ontologie.

**Garantien (nie stilles Nichtstun):** ein Fehlerlauf hängt NIE ein Attribut an;
ein nicht mehr beschreibbarer Span verweigert das Attribut laut
(`AttachResult(ok=False, span_not_recording)`); ein formfehlerhafter Plan wird mit
Feldpfad gemeldet, statt halb geschrieben zu werden.

## Installation

```
pip install meander-agent            # Transport + Emitter (schlank, ohne LLM-SDK)
pip install "meander-agent[claude]"  # + Claude-Agent-SDK-Binding
pip install "meander-agent[openai]"  # + OpenAI-Agents-SDK-Binding
```

## 1. Dein Vokabular deklarieren

Du gibst dem Agenten die Namen deiner Welt (er soll nichts erfinden). Je Entity
ihre identity-Felder und Properties, je Relation ihre Endpunkte, plus die
Derivation-IDs, die er befragen darf:

```python
vocabulary = {
    "entities": {
        "Order":   {"identity": ["order_id"], "properties": ["amount", "risk"]},
        "Case":    {"identity": ["case_id"],  "properties": []},
        "Outcome": {"identity": ["outcome_id"], "properties": []},
    },
    "relations": {
        "concerns": {"from": "Case", "to": "Order"},
    },
    "derivation_ids": ["requires_review.high_value"],
}
```

Daraus erzeugt das Paket den Prompt-Baustein (nennt dem Modell nur diese Namen)
und das JSON-Schema der strukturierten Ausgabe (erlaubt nur diese Namen).

## 2. Transport einrichten

Ohne eigenes OTel genügt ein Aufruf. `endpoint` ist die volle OTLP-Traces-URL
deiner Source, `source_key` das Bearer-Token dieser Source:

```python
from meander_agent import init_meander

client = init_meander(
    endpoint="https://<host>/api/sources/<source_id>/v1/traces",
    source_key="<bearer-token>",
)
# client.tracer  -> der verdrahtete OTel-Tracer
# client.shutdown() / client.force_flush()  -> Export abschließen
```

`init_meander` setzt KEINEN globalen Provider; der Client hält seinen eigenen.
Hast du bereits ein OTel-Setup, lass `init_meander` weg und übergib deinen Tracer
direkt (siehe Abschnitt 5).

## 3. Einen Agenten-Run fahren (Claude)

```python
from meander_agent.claude import run_with_plan

result = run_with_plan(
    "Bearbeite Bestellung ORD-42. Rufe die üblichen Tools auf, behaupte die "
    "gewonnenen Fakten mit ihrer Herkunft und stelle GENAU EINE Frage an die "
    "Derivation requires_review.high_value. Triff KEINE Entscheidung.",
    vocabulary=vocabulary,
    tracer=client.tracer,
)
print("angehängt" if result.attached else f"kein Plan gesetzt: {result.error_state}")
client.shutdown()   # Span exportieren
```

Das Binding öffnet den Root-Span, fährt das Modell mit strukturierter Ausgabe,
sperrt bei Fehler/Abbruch und hängt bei Erfolg den geprüften Plan an. Es gibt ein
`RunResult` zurück (siehe Abschnitt 4). Braucht das `[claude]`-Extra und einen
`ANTHROPIC_API_KEY`.

## 3b. Derselbe Run über OpenAI

Identischer Aufruf, anderes Binding (Extra `[openai]`, `OPENAI_API_KEY`):

```python
from meander_agent.openai import run_with_plan

result = run_with_plan(task, vocabulary=vocabulary, tracer=client.tracer)
```

Beide Bindings gibt es auch async (`run_with_plan_async`).

## 4. Was du zurückbekommst

`RunResult`:

- `attached: bool` — wurde `meander.plan` an den Span gehängt?
- `plan: dict | None` — der angehängte Plan (bei Erfolg).
- `shape_errors: list[ShapeError]` — Formfehler mit `path` / `code` / `message`,
  falls die Ausgabe die FORM-Prüfung nicht bestand.
- `error_state` — `None` bei Erfolg; sonst der Fehlerausgang des Laufs
  (SDK-Fehler, Abbruch, Typ-Mismatch). Ist er gesetzt, wird NIE angehängt.

Kein `meander.plan` heißt also immer: entweder ein Fehlerlauf (`error_state`) oder
eine formfehlerhafte Ausgabe (`shape_errors`) — beides steht im Ergebnis, nichts
verschwindet still.

## 5. Eigenes Tracing / eigenes SDK (der Kern)

Willst du dein eigenes SDK anbinden (oder deinen eigenen Tracer nutzen), fährst du
den SDK-neutralen Kontextmanager selbst. Er liefert dir den Baustein und das
Schema und übernimmt Parsen, Formprüfung, Sperren und Anhängen:

```python
from meander_agent import meander_run

with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
    # run.prompt_fragment : Format + erlaubtes Vokabular -> als Instruktion ans SDK
    # run.output_schema   : JSON-Schema, falls dein SDK strukturierte Ausgabe kann
    output, error = call_your_llm(task, instructions=run.prompt_fragment,
                                  schema=run.output_schema)
    # output: das strukturierte Ergebnis (dict) ODER ein reiner JSON-String.
    # error_state: None bei Erfolg, sonst ein beliebiges Detail (=> Sperre).
    result = run.finalize(output, error_state=error)
```

Der Kern übergibt nie selbst Prompts ans SDK und liest nie SDK-Resultate — das ist
Sache deines Bindings. Genauso arbeiten die mitgelieferten Claude-/OpenAI-Bindings.

## 6. Deterministischer Plan ohne LLM

Baust du den Plan selbst (Tests, regelbasierte Agenten, auth-loser Pfad), nutzt du
den Emitter direkt:

```python
from meander_agent import attach_plan, validate_plan_shape

plan = {
    "plan_version": 1,
    "facts": [
        {"entity": "Order", "identity": {"order_id": "ORD-42"},
         "property": "amount", "value": 900,
         "origin": {"kind": "tool", "ref": "lookup_order"}},
    ],
    "relations": [
        {"relation": "concerns",
         "from": {"entity": "Case", "identity": {"case_id": "C-1"}},
         "to":   {"entity": "Order", "identity": {"order_id": "ORD-42"}},
         "origin": {"kind": "agent"}},
    ],
    "question": {
        "derivation_id": "requires_review.high_value",
        "subject": {"entity": "Case", "identity": {"case_id": "C-1"}},
    },
}

errors = validate_plan_shape(plan)            # reine FORM-Prüfung (leer = ok)
with client.tracer.start_as_current_span("agent.run") as span:
    res = attach_plan(span, plan)             # hängt bei Formgültigkeit an
    if not res.ok:
        print("nicht angehängt:", [e.as_dict() for e in res.errors])
client.shutdown()
```

## Öffentliche Fläche

| Symbol | Zweck |
|---|---|
| `init_meander(endpoint, source_key) -> MeanderClient` | Transport (OTel + OTLP) einrichten |
| `MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown()` | Tracer, Kern-Zucker, Export |
| `meander_agent.claude.run_with_plan[_async](task, *, vocabulary, tracer)` | Claude-Binding |
| `meander_agent.openai.run_with_plan[_async](task, *, vocabulary, tracer)` | OpenAI-Binding |
| `meander_run(vocabulary, tracer) -> Run` | SDK-neutraler Kern-Kontextmanager |
| `Run.prompt_fragment / .output_schema / .finalize(output, error_state)` | Bausteine + Verarbeitung |
| `attach_plan(span, plan) -> AttachResult` | FORM-Prüfung + Anhang |
| `validate_plan_shape(plan) -> list[ShapeError]` | reine FORM-Prüfung |
| `RunResult`, `AttachResult`, `ShapeError` | Ergebnis-/Fehlertypen |
| `PLAN_ATTRIBUTE_KEY`, `ORIGIN_KINDS`, `PLAN_VERSION` | Client-Konstanten |

## Entwicklung

```
pixi run test          # zufällige Reihenfolge (pytest-randomly)
pixi run -- pytest -p no:randomly    # feste Reihenfolge
```

Die credential-gated Live-LLM-Tests (Claude/OpenAI) laufen dort, wo die Keys
gesetzt sind, und werden sonst übersprungen; die deterministische Suite läuft
immer und ohne Mocks.
