meander-agent 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,2 @@
1
+ # SCM syntax highlighting & preventing 3-way merges
2
+ pixi.lock merge=binary linguist-language=YAML linguist-generated=true -diff
@@ -0,0 +1,25 @@
1
+ name: meander-agent tests
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches:
7
+ - main
8
+ workflow_call:
9
+
10
+ jobs:
11
+ test:
12
+ name: Test
13
+ runs-on: ubuntu-latest
14
+ steps:
15
+ - uses: actions/checkout@v6
16
+ with:
17
+ persist-credentials: false
18
+
19
+ - uses: prefix-dev/setup-pixi@v0.9.5
20
+ with:
21
+ manifest-path: pyproject.toml
22
+ cache: true
23
+
24
+ - name: Run tests
25
+ run: pixi run test
@@ -0,0 +1,69 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - "v[0-9]+.[0-9]+.[0-9]+"
7
+
8
+ jobs:
9
+ test:
10
+ uses: ./.github/workflows/meander-agent-tests.yml
11
+
12
+ build:
13
+ name: Build dist
14
+ needs: test
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v6
18
+ with:
19
+ persist-credentials: false
20
+
21
+ - uses: prefix-dev/setup-pixi@v0.9.5
22
+ with:
23
+ manifest-path: pyproject.toml
24
+ cache: true
25
+
26
+ - name: Verify tag matches pyproject version
27
+ run: |
28
+ python - <<'PY'
29
+ import os
30
+ import tomllib
31
+ from pathlib import Path
32
+
33
+ tag = os.environ["GITHUB_REF_NAME"]
34
+ version = tomllib.loads(Path("pyproject.toml").read_text())["project"]["version"]
35
+ expected = f"v{version}"
36
+
37
+ if tag != expected:
38
+ raise SystemExit(f"tag {tag!r} does not match pyproject version {version!r}; expected {expected!r}")
39
+ PY
40
+
41
+ - name: Build distributions
42
+ run: |
43
+ rm -rf dist
44
+ pixi run build
45
+ pixi run check-dist
46
+
47
+ - uses: actions/upload-artifact@v6
48
+ with:
49
+ name: python-package-distributions
50
+ path: dist/
51
+
52
+ publish:
53
+ name: Publish to PyPI
54
+ needs: build
55
+ runs-on: ubuntu-latest
56
+ environment:
57
+ name: pypi
58
+ url: https://pypi.org/p/meander-agent
59
+ permissions:
60
+ id-token: write
61
+ steps:
62
+ - name: Download all the dists
63
+ uses: actions/download-artifact@v6
64
+ with:
65
+ name: python-package-distributions
66
+ path: dist/
67
+
68
+ - name: Publish dist to PyPI
69
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,12 @@
1
+ # pixi environments
2
+ .pixi/*
3
+ !.pixi/config.toml
4
+
5
+ .venv/
6
+ __pycache__/
7
+ *.pyc
8
+ .pytest_cache/
9
+ .DS_Store
10
+ *.egg-info/
11
+ dist/
12
+ build/
@@ -0,0 +1,229 @@
1
+ Metadata-Version: 2.4
2
+ Name: meander-agent
3
+ Version: 0.1.0
4
+ 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.
5
+ Author-email: Raphael Feikert <r.feikert@symbolic-intelligence.de>
6
+ Requires-Python: >=3.11
7
+ Requires-Dist: opentelemetry-exporter-otlp-proto-http<2,>=1.30
8
+ Requires-Dist: opentelemetry-sdk<2,>=1.30
9
+ Provides-Extra: claude
10
+ Requires-Dist: claude-agent-sdk<0.3,>=0.2; extra == 'claude'
11
+ Provides-Extra: openai
12
+ Requires-Dist: openai-agents<0.18,>=0.17; extra == 'openai'
13
+ Provides-Extra: test
14
+ Requires-Dist: pytest-randomly>=3; extra == 'test'
15
+ Requires-Dist: pytest>=8; extra == 'test'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # meander-agent
19
+
20
+ Kundenseitiges SDK für die **Plan-Sende-Seite** von meander. Dein Agent hält
21
+ fest, was er getan hat, als nachvollziehbare Behauptung mit Herkunft, und stellt
22
+ GENAU EINE Frage an eine Regel deiner Ontologie. Das Paket baut daraus ein
23
+ `meander.plan`, prüft dessen FORM und hängt es an einen echten OpenTelemetry-Span,
24
+ den dein OTLP-Export zu meander trägt. Kein meander-Import, keine Ontologie im
25
+ Client.
26
+
27
+ ## Das Modell in einem Absatz
28
+
29
+ Ein `meander.plan` ist eine **Behauptung plus eine Frage**, keine Entscheidung:
30
+
31
+ - **facts** — was der Agent beobachtet/eingeschätzt hat. Jeder Fakt trägt SEINE
32
+ Herkunft (`origin.kind`: `tool` | `api` | `human` | `agent`). Ein Tool-Ergebnis
33
+ ist `tool`, die eigene Einschätzung des Agenten ist `agent`.
34
+ - **relations** — behauptete Beziehungen zwischen Entities (optional).
35
+ - **question** — GENAU EINE Frage an eine bestehende Derivation (Regel) deiner
36
+ Welt, z.B. „gilt hier `requires_review`?". Der Agent **entscheidet nicht**; ob
37
+ die Regel greift, bestimmt meander serverseitig, und ein Mensch prüft.
38
+
39
+ Der Client prüft nur die **FORM** (Pflichtfelder, Typen, genau eine Frage,
40
+ `origin`-Vokabular, `plan_version == 1`). Ob die Entities/Properties/Derivations
41
+ wirklich existieren, weiß nur der Server (er kennt deine Ontologie). Darum
42
+ importiert dieses Paket meander NICHT und lädt keine Ontologie.
43
+
44
+ **Garantien (nie stilles Nichtstun):** ein Fehlerlauf hängt NIE ein Attribut an;
45
+ ein nicht mehr beschreibbarer Span verweigert das Attribut laut
46
+ (`AttachResult(ok=False, span_not_recording)`); ein formfehlerhafter Plan wird mit
47
+ Feldpfad gemeldet, statt halb geschrieben zu werden.
48
+
49
+ ## Installation
50
+
51
+ ```
52
+ pip install meander-agent # Transport + Emitter (schlank, ohne LLM-SDK)
53
+ pip install "meander-agent[claude]" # + Claude-Agent-SDK-Binding
54
+ pip install "meander-agent[openai]" # + OpenAI-Agents-SDK-Binding
55
+ ```
56
+
57
+ ## 1. Dein Vokabular deklarieren
58
+
59
+ Du gibst dem Agenten die Namen deiner Welt (er soll nichts erfinden). Je Entity
60
+ ihre identity-Felder und Properties, je Relation ihre Endpunkte, plus die
61
+ Derivation-IDs, die er befragen darf:
62
+
63
+ ```python
64
+ vocabulary = {
65
+ "entities": {
66
+ "Order": {"identity": ["order_id"], "properties": ["amount", "risk"]},
67
+ "Case": {"identity": ["case_id"], "properties": []},
68
+ "Outcome": {"identity": ["outcome_id"], "properties": []},
69
+ },
70
+ "relations": {
71
+ "concerns": {"from": "Case", "to": "Order"},
72
+ },
73
+ "derivation_ids": ["requires_review.high_value"],
74
+ }
75
+ ```
76
+
77
+ Daraus erzeugt das Paket den Prompt-Baustein (nennt dem Modell nur diese Namen)
78
+ und das JSON-Schema der strukturierten Ausgabe (erlaubt nur diese Namen).
79
+
80
+ ## 2. Transport einrichten
81
+
82
+ Ohne eigenes OTel genügt ein Aufruf. `endpoint` ist die volle OTLP-Traces-URL
83
+ deiner Source, `source_key` das Bearer-Token dieser Source:
84
+
85
+ ```python
86
+ from meander_agent import init_meander
87
+
88
+ client = init_meander(
89
+ endpoint="https://<host>/api/sources/<source_id>/v1/traces",
90
+ source_key="<bearer-token>",
91
+ )
92
+ # client.tracer -> der verdrahtete OTel-Tracer
93
+ # client.shutdown() / client.force_flush() -> Export abschließen
94
+ ```
95
+
96
+ `init_meander` setzt KEINEN globalen Provider; der Client hält seinen eigenen.
97
+ Hast du bereits ein OTel-Setup, lass `init_meander` weg und übergib deinen Tracer
98
+ direkt (siehe Abschnitt 5).
99
+
100
+ ## 3. Einen Agenten-Run fahren (Claude)
101
+
102
+ ```python
103
+ from meander_agent.claude import run_with_plan
104
+
105
+ result = run_with_plan(
106
+ "Bearbeite Bestellung ORD-42. Rufe die üblichen Tools auf, behaupte die "
107
+ "gewonnenen Fakten mit ihrer Herkunft und stelle GENAU EINE Frage an die "
108
+ "Derivation requires_review.high_value. Triff KEINE Entscheidung.",
109
+ vocabulary=vocabulary,
110
+ tracer=client.tracer,
111
+ )
112
+ print("angehängt" if result.attached else f"kein Plan gesetzt: {result.error_state}")
113
+ client.shutdown() # Span exportieren
114
+ ```
115
+
116
+ Das Binding öffnet den Root-Span, fährt das Modell mit strukturierter Ausgabe,
117
+ sperrt bei Fehler/Abbruch und hängt bei Erfolg den geprüften Plan an. Es gibt ein
118
+ `RunResult` zurück (siehe Abschnitt 4). Braucht das `[claude]`-Extra und einen
119
+ `ANTHROPIC_API_KEY`.
120
+
121
+ ## 3b. Derselbe Run über OpenAI
122
+
123
+ Identischer Aufruf, anderes Binding (Extra `[openai]`, `OPENAI_API_KEY`):
124
+
125
+ ```python
126
+ from meander_agent.openai import run_with_plan
127
+
128
+ result = run_with_plan(task, vocabulary=vocabulary, tracer=client.tracer)
129
+ ```
130
+
131
+ Beide Bindings gibt es auch async (`run_with_plan_async`).
132
+
133
+ ## 4. Was du zurückbekommst
134
+
135
+ `RunResult`:
136
+
137
+ - `attached: bool` — wurde `meander.plan` an den Span gehängt?
138
+ - `plan: dict | None` — der angehängte Plan (bei Erfolg).
139
+ - `shape_errors: list[ShapeError]` — Formfehler mit `path` / `code` / `message`,
140
+ falls die Ausgabe die FORM-Prüfung nicht bestand.
141
+ - `error_state` — `None` bei Erfolg; sonst der Fehlerausgang des Laufs
142
+ (SDK-Fehler, Abbruch, Typ-Mismatch). Ist er gesetzt, wird NIE angehängt.
143
+
144
+ Kein `meander.plan` heißt also immer: entweder ein Fehlerlauf (`error_state`) oder
145
+ eine formfehlerhafte Ausgabe (`shape_errors`) — beides steht im Ergebnis, nichts
146
+ verschwindet still.
147
+
148
+ ## 5. Eigenes Tracing / eigenes SDK (der Kern)
149
+
150
+ Willst du dein eigenes SDK anbinden (oder deinen eigenen Tracer nutzen), fährst du
151
+ den SDK-neutralen Kontextmanager selbst. Er liefert dir den Baustein und das
152
+ Schema und übernimmt Parsen, Formprüfung, Sperren und Anhängen:
153
+
154
+ ```python
155
+ from meander_agent import meander_run
156
+
157
+ with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
158
+ # run.prompt_fragment : Format + erlaubtes Vokabular -> als Instruktion ans SDK
159
+ # run.output_schema : JSON-Schema, falls dein SDK strukturierte Ausgabe kann
160
+ output, error = call_your_llm(task, instructions=run.prompt_fragment,
161
+ schema=run.output_schema)
162
+ # output: das strukturierte Ergebnis (dict) ODER ein reiner JSON-String.
163
+ # error_state: None bei Erfolg, sonst ein beliebiges Detail (=> Sperre).
164
+ result = run.finalize(output, error_state=error)
165
+ ```
166
+
167
+ Der Kern übergibt nie selbst Prompts ans SDK und liest nie SDK-Resultate — das ist
168
+ Sache deines Bindings. Genauso arbeiten die mitgelieferten Claude-/OpenAI-Bindings.
169
+
170
+ ## 6. Deterministischer Plan ohne LLM
171
+
172
+ Baust du den Plan selbst (Tests, regelbasierte Agenten, auth-loser Pfad), nutzt du
173
+ den Emitter direkt:
174
+
175
+ ```python
176
+ from meander_agent import attach_plan, validate_plan_shape
177
+
178
+ plan = {
179
+ "plan_version": 1,
180
+ "facts": [
181
+ {"entity": "Order", "identity": {"order_id": "ORD-42"},
182
+ "property": "amount", "value": 900,
183
+ "origin": {"kind": "tool", "ref": "lookup_order"}},
184
+ ],
185
+ "relations": [
186
+ {"relation": "concerns",
187
+ "from": {"entity": "Case", "identity": {"case_id": "C-1"}},
188
+ "to": {"entity": "Order", "identity": {"order_id": "ORD-42"}},
189
+ "origin": {"kind": "agent"}},
190
+ ],
191
+ "question": {
192
+ "derivation_id": "requires_review.high_value",
193
+ "subject": {"entity": "Case", "identity": {"case_id": "C-1"}},
194
+ },
195
+ }
196
+
197
+ errors = validate_plan_shape(plan) # reine FORM-Prüfung (leer = ok)
198
+ with client.tracer.start_as_current_span("agent.run") as span:
199
+ res = attach_plan(span, plan) # hängt bei Formgültigkeit an
200
+ if not res.ok:
201
+ print("nicht angehängt:", [e.as_dict() for e in res.errors])
202
+ client.shutdown()
203
+ ```
204
+
205
+ ## Öffentliche Fläche
206
+
207
+ | Symbol | Zweck |
208
+ |---|---|
209
+ | `init_meander(endpoint, source_key) -> MeanderClient` | Transport (OTel + OTLP) einrichten |
210
+ | `MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown()` | Tracer, Kern-Zucker, Export |
211
+ | `meander_agent.claude.run_with_plan[_async](task, *, vocabulary, tracer)` | Claude-Binding |
212
+ | `meander_agent.openai.run_with_plan[_async](task, *, vocabulary, tracer)` | OpenAI-Binding |
213
+ | `meander_run(vocabulary, tracer) -> Run` | SDK-neutraler Kern-Kontextmanager |
214
+ | `Run.prompt_fragment / .output_schema / .finalize(output, error_state)` | Bausteine + Verarbeitung |
215
+ | `attach_plan(span, plan) -> AttachResult` | FORM-Prüfung + Anhang |
216
+ | `validate_plan_shape(plan) -> list[ShapeError]` | reine FORM-Prüfung |
217
+ | `RunResult`, `AttachResult`, `ShapeError` | Ergebnis-/Fehlertypen |
218
+ | `PLAN_ATTRIBUTE_KEY`, `ORIGIN_KINDS`, `PLAN_VERSION` | Client-Konstanten |
219
+
220
+ ## Entwicklung
221
+
222
+ ```
223
+ pixi run test # zufällige Reihenfolge (pytest-randomly)
224
+ pixi run -- pytest -p no:randomly # feste Reihenfolge
225
+ ```
226
+
227
+ Die credential-gated Live-LLM-Tests (Claude/OpenAI) laufen dort, wo die Keys
228
+ gesetzt sind, und werden sonst übersprungen; die deterministische Suite läuft
229
+ immer und ohne Mocks.
@@ -0,0 +1,212 @@
1
+ # meander-agent
2
+
3
+ Kundenseitiges SDK für die **Plan-Sende-Seite** von meander. Dein Agent hält
4
+ fest, was er getan hat, als nachvollziehbare Behauptung mit Herkunft, und stellt
5
+ GENAU EINE Frage an eine Regel deiner Ontologie. Das Paket baut daraus ein
6
+ `meander.plan`, prüft dessen FORM und hängt es an einen echten OpenTelemetry-Span,
7
+ den dein OTLP-Export zu meander trägt. Kein meander-Import, keine Ontologie im
8
+ Client.
9
+
10
+ ## Das Modell in einem Absatz
11
+
12
+ Ein `meander.plan` ist eine **Behauptung plus eine Frage**, keine Entscheidung:
13
+
14
+ - **facts** — was der Agent beobachtet/eingeschätzt hat. Jeder Fakt trägt SEINE
15
+ Herkunft (`origin.kind`: `tool` | `api` | `human` | `agent`). Ein Tool-Ergebnis
16
+ ist `tool`, die eigene Einschätzung des Agenten ist `agent`.
17
+ - **relations** — behauptete Beziehungen zwischen Entities (optional).
18
+ - **question** — GENAU EINE Frage an eine bestehende Derivation (Regel) deiner
19
+ Welt, z.B. „gilt hier `requires_review`?". Der Agent **entscheidet nicht**; ob
20
+ die Regel greift, bestimmt meander serverseitig, und ein Mensch prüft.
21
+
22
+ Der Client prüft nur die **FORM** (Pflichtfelder, Typen, genau eine Frage,
23
+ `origin`-Vokabular, `plan_version == 1`). Ob die Entities/Properties/Derivations
24
+ wirklich existieren, weiß nur der Server (er kennt deine Ontologie). Darum
25
+ importiert dieses Paket meander NICHT und lädt keine Ontologie.
26
+
27
+ **Garantien (nie stilles Nichtstun):** ein Fehlerlauf hängt NIE ein Attribut an;
28
+ ein nicht mehr beschreibbarer Span verweigert das Attribut laut
29
+ (`AttachResult(ok=False, span_not_recording)`); ein formfehlerhafter Plan wird mit
30
+ Feldpfad gemeldet, statt halb geschrieben zu werden.
31
+
32
+ ## Installation
33
+
34
+ ```
35
+ pip install meander-agent # Transport + Emitter (schlank, ohne LLM-SDK)
36
+ pip install "meander-agent[claude]" # + Claude-Agent-SDK-Binding
37
+ pip install "meander-agent[openai]" # + OpenAI-Agents-SDK-Binding
38
+ ```
39
+
40
+ ## 1. Dein Vokabular deklarieren
41
+
42
+ Du gibst dem Agenten die Namen deiner Welt (er soll nichts erfinden). Je Entity
43
+ ihre identity-Felder und Properties, je Relation ihre Endpunkte, plus die
44
+ Derivation-IDs, die er befragen darf:
45
+
46
+ ```python
47
+ vocabulary = {
48
+ "entities": {
49
+ "Order": {"identity": ["order_id"], "properties": ["amount", "risk"]},
50
+ "Case": {"identity": ["case_id"], "properties": []},
51
+ "Outcome": {"identity": ["outcome_id"], "properties": []},
52
+ },
53
+ "relations": {
54
+ "concerns": {"from": "Case", "to": "Order"},
55
+ },
56
+ "derivation_ids": ["requires_review.high_value"],
57
+ }
58
+ ```
59
+
60
+ Daraus erzeugt das Paket den Prompt-Baustein (nennt dem Modell nur diese Namen)
61
+ und das JSON-Schema der strukturierten Ausgabe (erlaubt nur diese Namen).
62
+
63
+ ## 2. Transport einrichten
64
+
65
+ Ohne eigenes OTel genügt ein Aufruf. `endpoint` ist die volle OTLP-Traces-URL
66
+ deiner Source, `source_key` das Bearer-Token dieser Source:
67
+
68
+ ```python
69
+ from meander_agent import init_meander
70
+
71
+ client = init_meander(
72
+ endpoint="https://<host>/api/sources/<source_id>/v1/traces",
73
+ source_key="<bearer-token>",
74
+ )
75
+ # client.tracer -> der verdrahtete OTel-Tracer
76
+ # client.shutdown() / client.force_flush() -> Export abschließen
77
+ ```
78
+
79
+ `init_meander` setzt KEINEN globalen Provider; der Client hält seinen eigenen.
80
+ Hast du bereits ein OTel-Setup, lass `init_meander` weg und übergib deinen Tracer
81
+ direkt (siehe Abschnitt 5).
82
+
83
+ ## 3. Einen Agenten-Run fahren (Claude)
84
+
85
+ ```python
86
+ from meander_agent.claude import run_with_plan
87
+
88
+ result = run_with_plan(
89
+ "Bearbeite Bestellung ORD-42. Rufe die üblichen Tools auf, behaupte die "
90
+ "gewonnenen Fakten mit ihrer Herkunft und stelle GENAU EINE Frage an die "
91
+ "Derivation requires_review.high_value. Triff KEINE Entscheidung.",
92
+ vocabulary=vocabulary,
93
+ tracer=client.tracer,
94
+ )
95
+ print("angehängt" if result.attached else f"kein Plan gesetzt: {result.error_state}")
96
+ client.shutdown() # Span exportieren
97
+ ```
98
+
99
+ Das Binding öffnet den Root-Span, fährt das Modell mit strukturierter Ausgabe,
100
+ sperrt bei Fehler/Abbruch und hängt bei Erfolg den geprüften Plan an. Es gibt ein
101
+ `RunResult` zurück (siehe Abschnitt 4). Braucht das `[claude]`-Extra und einen
102
+ `ANTHROPIC_API_KEY`.
103
+
104
+ ## 3b. Derselbe Run über OpenAI
105
+
106
+ Identischer Aufruf, anderes Binding (Extra `[openai]`, `OPENAI_API_KEY`):
107
+
108
+ ```python
109
+ from meander_agent.openai import run_with_plan
110
+
111
+ result = run_with_plan(task, vocabulary=vocabulary, tracer=client.tracer)
112
+ ```
113
+
114
+ Beide Bindings gibt es auch async (`run_with_plan_async`).
115
+
116
+ ## 4. Was du zurückbekommst
117
+
118
+ `RunResult`:
119
+
120
+ - `attached: bool` — wurde `meander.plan` an den Span gehängt?
121
+ - `plan: dict | None` — der angehängte Plan (bei Erfolg).
122
+ - `shape_errors: list[ShapeError]` — Formfehler mit `path` / `code` / `message`,
123
+ falls die Ausgabe die FORM-Prüfung nicht bestand.
124
+ - `error_state` — `None` bei Erfolg; sonst der Fehlerausgang des Laufs
125
+ (SDK-Fehler, Abbruch, Typ-Mismatch). Ist er gesetzt, wird NIE angehängt.
126
+
127
+ Kein `meander.plan` heißt also immer: entweder ein Fehlerlauf (`error_state`) oder
128
+ eine formfehlerhafte Ausgabe (`shape_errors`) — beides steht im Ergebnis, nichts
129
+ verschwindet still.
130
+
131
+ ## 5. Eigenes Tracing / eigenes SDK (der Kern)
132
+
133
+ Willst du dein eigenes SDK anbinden (oder deinen eigenen Tracer nutzen), fährst du
134
+ den SDK-neutralen Kontextmanager selbst. Er liefert dir den Baustein und das
135
+ Schema und übernimmt Parsen, Formprüfung, Sperren und Anhängen:
136
+
137
+ ```python
138
+ from meander_agent import meander_run
139
+
140
+ with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
141
+ # run.prompt_fragment : Format + erlaubtes Vokabular -> als Instruktion ans SDK
142
+ # run.output_schema : JSON-Schema, falls dein SDK strukturierte Ausgabe kann
143
+ output, error = call_your_llm(task, instructions=run.prompt_fragment,
144
+ schema=run.output_schema)
145
+ # output: das strukturierte Ergebnis (dict) ODER ein reiner JSON-String.
146
+ # error_state: None bei Erfolg, sonst ein beliebiges Detail (=> Sperre).
147
+ result = run.finalize(output, error_state=error)
148
+ ```
149
+
150
+ Der Kern übergibt nie selbst Prompts ans SDK und liest nie SDK-Resultate — das ist
151
+ Sache deines Bindings. Genauso arbeiten die mitgelieferten Claude-/OpenAI-Bindings.
152
+
153
+ ## 6. Deterministischer Plan ohne LLM
154
+
155
+ Baust du den Plan selbst (Tests, regelbasierte Agenten, auth-loser Pfad), nutzt du
156
+ den Emitter direkt:
157
+
158
+ ```python
159
+ from meander_agent import attach_plan, validate_plan_shape
160
+
161
+ plan = {
162
+ "plan_version": 1,
163
+ "facts": [
164
+ {"entity": "Order", "identity": {"order_id": "ORD-42"},
165
+ "property": "amount", "value": 900,
166
+ "origin": {"kind": "tool", "ref": "lookup_order"}},
167
+ ],
168
+ "relations": [
169
+ {"relation": "concerns",
170
+ "from": {"entity": "Case", "identity": {"case_id": "C-1"}},
171
+ "to": {"entity": "Order", "identity": {"order_id": "ORD-42"}},
172
+ "origin": {"kind": "agent"}},
173
+ ],
174
+ "question": {
175
+ "derivation_id": "requires_review.high_value",
176
+ "subject": {"entity": "Case", "identity": {"case_id": "C-1"}},
177
+ },
178
+ }
179
+
180
+ errors = validate_plan_shape(plan) # reine FORM-Prüfung (leer = ok)
181
+ with client.tracer.start_as_current_span("agent.run") as span:
182
+ res = attach_plan(span, plan) # hängt bei Formgültigkeit an
183
+ if not res.ok:
184
+ print("nicht angehängt:", [e.as_dict() for e in res.errors])
185
+ client.shutdown()
186
+ ```
187
+
188
+ ## Öffentliche Fläche
189
+
190
+ | Symbol | Zweck |
191
+ |---|---|
192
+ | `init_meander(endpoint, source_key) -> MeanderClient` | Transport (OTel + OTLP) einrichten |
193
+ | `MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown()` | Tracer, Kern-Zucker, Export |
194
+ | `meander_agent.claude.run_with_plan[_async](task, *, vocabulary, tracer)` | Claude-Binding |
195
+ | `meander_agent.openai.run_with_plan[_async](task, *, vocabulary, tracer)` | OpenAI-Binding |
196
+ | `meander_run(vocabulary, tracer) -> Run` | SDK-neutraler Kern-Kontextmanager |
197
+ | `Run.prompt_fragment / .output_schema / .finalize(output, error_state)` | Bausteine + Verarbeitung |
198
+ | `attach_plan(span, plan) -> AttachResult` | FORM-Prüfung + Anhang |
199
+ | `validate_plan_shape(plan) -> list[ShapeError]` | reine FORM-Prüfung |
200
+ | `RunResult`, `AttachResult`, `ShapeError` | Ergebnis-/Fehlertypen |
201
+ | `PLAN_ATTRIBUTE_KEY`, `ORIGIN_KINDS`, `PLAN_VERSION` | Client-Konstanten |
202
+
203
+ ## Entwicklung
204
+
205
+ ```
206
+ pixi run test # zufällige Reihenfolge (pytest-randomly)
207
+ pixi run -- pytest -p no:randomly # feste Reihenfolge
208
+ ```
209
+
210
+ Die credential-gated Live-LLM-Tests (Claude/OpenAI) laufen dort, wo die Keys
211
+ gesetzt sind, und werden sonst übersprungen; die deterministische Suite läuft
212
+ immer und ohne Mocks.