Skip to main content

Typed Python client for integrating agents with meander.

Project description

meander-agent

Typed Python client for the plan-sending side of meander.

Your agent records what it did as a traceable claim with provenance, and asks exactly one question of a rule in your ontology. This package turns that into a meander.plan, checks its shape, and attaches it to a real OpenTelemetry span. Your OTLP export carries the span to meander. The client never imports meander and holds no ontology.

The model in one paragraph

A meander.plan is a claim plus one question, not a decision:

  • facts: what the agent observed or judged. Each fact carries its own provenance (origin.kind: tool | api | human | agent). A tool result is tool; the agent's own judgement is agent.
  • relations: claimed relationships between entities (optional).
  • question: exactly one question for a rule (derivation) that already exists in your world, for example "does requires_review hold here?". The agent does not decide. Whether the rule fires is decided by meander on the server, and a human reviews it.

The client checks only the shape: required fields, types, exactly one question, the origin vocabulary, and plan_version == 1. Whether the entities, properties, and derivations really exist is known only to the server, because only the server has your ontology. That is why this package does not import meander and loads no ontology.

Guarantees (never a silent no-op): a failed run never attaches an attribute; a span that can no longer be written refuses the attribute out loud (AttachResult(ok=False, span_not_recording)); a plan with a bad shape is reported with a field path instead of being written half-way.

Installation

pip install meander-agent            # transport + emitter (slim, no LLM SDK)
pip install "meander-agent[claude]"  # + Claude Agent SDK binding
pip install "meander-agent[openai]"  # + OpenAI Agents SDK binding

1. Declare your vocabulary

You give the agent the names of your world, so it invents nothing. For each entity its identity fields and properties, for each relation its endpoints, plus the derivation IDs it may ask about:

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"],
}

From this the package builds the prompt fragment (it names only these names to the model) and the JSON schema for the structured output (it allows only these names).

2. Set up transport

If you have no OTel of your own, one call is enough. endpoint is the full OTLP traces URL of your source, source_key is that source's bearer token:

from meander_agent import init_meander

client = init_meander(
    endpoint="https://<host>/api/sources/<source_id>/v1/traces",
    source_key="<bearer-token>",
)
# client.tracer  -> the wired OTel tracer
# client.shutdown() / client.force_flush()  -> finish the export

init_meander sets no global provider; the client keeps its own. If you already have an OTel setup, skip init_meander and pass your tracer directly (see section 5).

3. Run an agent (Claude)

from meander_agent.claude import run_with_plan

result = run_with_plan(
    "Handle order ORD-42. Call the usual tools, claim the facts you gathered "
    "with their provenance, and ask exactly one question of the derivation "
    "requires_review.high_value. Do NOT make a decision.",
    vocabulary=vocabulary,
    tracer=client.tracer,
)
print("attached" if result.attached else f"no plan set: {result.error_state}")
client.shutdown()   # export the span

The binding opens the root span, runs the model with structured output, locks on error or abort, and on success attaches the checked plan. It returns a RunResult (see section 4). Needs the [claude] extra and an ANTHROPIC_API_KEY.

3b. The same run over OpenAI

Same call, different binding (extra [openai], OPENAI_API_KEY):

from meander_agent.openai import run_with_plan

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

Both bindings also come async (run_with_plan_async).

4. What you get back

RunResult:

  • attached: bool tells whether meander.plan was attached to the span.
  • plan: dict | None is the attached plan (on success).
  • shape_errors: list[ShapeError] lists shape errors with path / code / message, when the output failed the shape check.
  • error_state is None on success; otherwise it is the run's failure exit (SDK error, abort, type mismatch). When it is set, nothing is ever attached.

So no meander.plan always means one of two things: a failed run (error_state) or an output with a bad shape (shape_errors). Both are in the result; nothing disappears silently.

5. Your own tracing / your own SDK (the core)

If you want to wire your own SDK (or use your own tracer), you drive the SDK-neutral context manager yourself. It hands you the fragment and the schema, and takes care of parsing, the shape check, locking, and attaching:

from meander_agent import meander_run

with meander_run(vocabulary=vocabulary, tracer=my_tracer) as run:
    # run.prompt_fragment : shape + allowed vocabulary -> instruction to your SDK
    # run.output_schema   : JSON schema, if your SDK can do structured output
    output, error = call_your_llm(task, instructions=run.prompt_fragment,
                                  schema=run.output_schema)
    # output: the structured result (dict) OR a plain JSON string.
    # error_state: None on success, otherwise any detail (=> lock).
    result = run.finalize(output, error_state=error)

The core never passes prompts to the SDK itself and never reads SDK results; that is your binding's job. The bundled Claude and OpenAI bindings work the same way.

6. A deterministic plan without an LLM

If you build the plan yourself (tests, rule-based agents, an auth-free path), you use the emitter directly:

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)            # pure shape check (empty = ok)
with client.tracer.start_as_current_span("agent.run") as span:
    res = attach_plan(span, plan)             # attaches if the shape is valid
    if not res.ok:
        print("not attached:", [e.as_dict() for e in res.errors])
client.shutdown()

Public surface

Symbol Purpose
init_meander(endpoint, source_key) -> MeanderClient set up transport (OTel + OTLP)
MeanderClient.tracer / .run(vocabulary=…) / .force_flush() / .shutdown() tracer, core shortcut, 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-neutral core context manager
Run.prompt_fragment / .output_schema / .finalize(output, error_state) building blocks + processing
attach_plan(span, plan) -> AttachResult shape check + attach
validate_plan_shape(plan) -> list[ShapeError] pure shape check
RunResult, AttachResult, ShapeError result / error types
PLAN_ATTRIBUTE_KEY, ORIGIN_KINDS, PLAN_VERSION client constants

Development

pixi run test          # random order (pytest-randomly)
pixi run -- pytest -p no:randomly    # fixed order

The credential-gated live LLM tests (Claude/OpenAI) run where the keys are set, and are skipped otherwise. The deterministic suite always runs, with no mocks.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

meander_agent-0.1.1.tar.gz (47.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

meander_agent-0.1.1-py3-none-any.whl (20.1 kB view details)

Uploaded Python 3

File details

Details for the file meander_agent-0.1.1.tar.gz.

File metadata

  • Download URL: meander_agent-0.1.1.tar.gz
  • Upload date:
  • Size: 47.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for meander_agent-0.1.1.tar.gz
Algorithm Hash digest
SHA256 c7454a4a2b12bfadb931a29896eea876cbbf6908ef62cfa34a244665c4c2497b
MD5 cea900b0c3c71b60490e1649d09eebdb
BLAKE2b-256 82c1feac80ea03cc3807514aea2135d40775d6e335886b7bad4ab532dd8a06d3

See more details on using hashes here.

Provenance

The following attestation bundles were made for meander_agent-0.1.1.tar.gz:

Publisher: publish-pypi.yml on Symbolic-Intelligence-Org/meander-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file meander_agent-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: meander_agent-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 20.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for meander_agent-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 c98d72ec8e11bea61113f416e23f4f4c3ee195637d9e57cc10a7e9210a0411ff
MD5 19b9b7f421185642dceb169ef27bfd20
BLAKE2b-256 19f1bb252748413ed4215e3a2943bda3418e31ea6c6246f1ec33b0d0e5c7e562

See more details on using hashes here.

Provenance

The following attestation bundles were made for meander_agent-0.1.1-py3-none-any.whl:

Publisher: publish-pypi.yml on Symbolic-Intelligence-Org/meander-agent

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page