Wyrd docs v0.2.0 Reference GitHub

Status: Runnable

Verify your first agent

You will run support-desk, a small customer-support API, against the local server from Set up Wyrd. Each question it answers is one Wyrd Run, and three Verifiers judge the answer:

VerifierBound toChecksHow it runs
no-refund-promiseThe ServiceThe answer never contains guaranteed refundYour code calls it and waits; no model
ticket-completeThe agentThe ticket has an answer and an order IDYour code calls it and waits; sandboxed Python, no model
follows-policyThe agentA refund decision letter the agent writes follows the refund policyYour code queues it; the server runs a model judge in the background

At the end you have a verdict for every check, each tied to the exact Card versions that ran, and one SQL query that joins it to the ticket, the trace, and the model call.

The code is the same app in Python, Rust, and TypeScript. This page runs the Python one; the tabs show the matching Rust and TypeScript code.

Prerequisites

  • A local server running from Set up Wyrd, in another terminal. The app reads the endpoint and credential that setup saved, so you export nothing.
  • An OpenAI API key with access to gpt-4o (the agent) and gpt-6-luna (the policy judge).
  • Python 3.11+ and git.

1. Get the example

bash
git clone https://github.com/bohmian-ai/wyrd.git
cd wyrd/examples/support-desk-app/python
pip install "wyrd-ai[agents,gateway,otel]" fastapi uvicorn

The agents extra brings the OpenAI Agents SDK, gateway the HTTP client the gateway uses, and otel the trace exporter. The Cards the app runs live next door, in examples/support-desk-app/service/.

2. Connect the model

Wyrd calls OpenAI for you, so your code never holds the OpenAI key. If you accepted the OpenAI key during setup, with the proposed deployment name openai, this step is already done.

Otherwise, with OPENAI_API_KEY exported, apply one document. It names the variable instead of holding the key; the CLI reads the value and sends it once, and the server encrypts it, so no read ever returns it:

bash
wyrd gateway deployment put --file - <<'JSON'
{
  "credentials": [{"name": "openai-key", "provider": "openai", "source": {"local_env": "OPENAI_API_KEY"}}],
  "deployments": [{"name": "openai", "adapter": "openai", "auth": {"bearer": {"credential": "openai-key"}}}]
}
JSON

The server creates the credential openai-key and the deployment openai together, or neither, and prints both with the secret redacted. Agents now reach OpenAI models as openai/<model>, and both Agent Cards in the example ask for exactly that with execution_route: {kind: wyrd_gateway, deployment: openai}. Set up the LLM gateway covers other providers and the separate credential and deployment commands.

3. Read what is declared

The example is seven Cards in service/. The Service names the others by relative path, such as ref: ./support-agent.yaml, so switch tabs to follow each reference:

service/support-desk.yaml yaml
apiVersion: wyrd/v1
kind: Service
metadata:
  name: support-desk
  version: 1.0.0
  space: default
spec:
  service_type: agent
  # The Service's own Verifier checks every answer the desk gives.
  verified_by:
    - verifier: ./no-refund-promise.yaml
  tables:
    - name: tickets
      schema:
        type: object
        properties:
          question: {type: string}
          answer: {type: string}
        required: [question, answer]
  components:
    - alias: agent
      ref: ./support-agent.yaml
      verified_by:
        - verifier: ./ticket-complete.yaml
        - verifier: ./follows-policy.yaml
    - alias: prompt
      ref: ./support-prompt.yaml
CardRole
support-desk.yamlThe Service: its components, its tickets table, and which Verifier checks what
support-agent.yamlThe agent. It runs support-prompt through the gateway’s openai deployment
support-prompt.yamlThe prompt and model the agent runs
no-refund-promise.yamlBound to the Service. One assertion, no model
ticket-complete.yamlBound to the agent. A sandboxed Python check, no model
follows-policy.yamlBound to the agent. Extracts the text of the agent’s letter, then asks policy-judge
policy-judge.yamlA tool-less Agent with the refund policy in its prompt. Returns a verdict citing lines of the letter

verified_by on the Service binds no-refund-promise to every Run of the Service. verified_by on the agent component binds the other two to the agent alone. Declaring the Service declares every Card it references, so the next step declares all seven.

4. Deploy the Cards

Check the declaration first. It validates all seven Cards and contacts no server:

bash
wyrd declare ../service --check --format json

Then deploy:

bash
python deploy.py

This declares all seven Cards, which creates the vala.datasets.tickets table. It then downloads the registered versions into .wyrd-bundle/, so the app runs exactly what the server holds, and turns on gateway metadata capture, so each model call is recorded with its Run:

deploy.py python
cards = Cards(client)
receipt = cards.declare_from_path(SERVICE / "support-desk.yaml")
cards.pull(receipt.root, BUNDLE)
Gateway(client).put_capture_policy({"mode": "metadata", "payload_fields": []})
print(f"declared {receipt.root} into {BUNDLE.name}")

It ends with declared <card-uid> into .wyrd-bundle. Declaring is idempotent: running it again with unchanged Cards changes nothing. Declaring needs the editor role; a viewer credential is refused with WYRD_PERMISSION_403_DENIED_RBAC.

5. Start the app

The app connects once at startup. WyrdState.from_path loads the pulled bundle, start_bifrost starts the buffered writer for the tickets table, and telemetry exports spans, including the Agents SDK’s own traces, to Wyrd:

desk.py python
self.client = client or WyrdClient()
self.state = WyrdState.from_path(BUNDLE, self.client)
self.state.start_bifrost()
# Spans export to Wyrd through the SDK's OTLP exporter; the Agents SDK's
# own traces become spans too, under the request's Run.
self.state.start_telemetry()
set_trace_processors([WyrdAgentsTraceProcessor()])

Rows and spans are buffered, so they reach Wyrd in batches, not on every call. On exit the app flushes them. In Python and TypeScript, state.shutdown() flushes the spans of the provider start_telemetry installed, then drains the writer. In Rust, start_telemetry(&state) returns a Telemetry handle that the server keeps while it serves; it flushes that handle before draining the state:

app.py python
app.state.desk = Desk()
yield
# Flush buffered tickets and spans to Wyrd before exiting.
app.state.desk.state.shutdown()

A process killed before shutdown() loses whatever was still buffered.

Start the API, and leave it running:

bash
uvicorn app:app

6. Ask a question

In another terminal, ask a question:

bash
curl -s localhost:8000/ask -H 'content-type: application/json' 
  -d '{"question": "Where is my order?", "order_id": "A-1001"}'

The handler answers in a new Run, records the ticket under it, then asks for both model-free verdicts and waits for them:

desk.py python
run = self.state.run()
with run, tracer.start_as_current_span("support-desk.ask"):
    # These headers tie the gateway's record of the call to this Run.
    headers = {
        "wyrd-run-id": run.run_id,
        "wyrd-card-uid": str(self.state.card_ref("agent").uid),
    }
    model = OpenAIChatCompletionsModel(
        self.model, self.gateway.with_options(default_headers=headers)
    )
    agent = Agent(name="support-agent", model=model)
    result = await Runner.run(agent, self.template.replace("{{question}}", question))
    answer = str(result.final_output)
    run.observe.record(TICKETS, {"question": question, "answer": answer})
desk.py python
return {
    "no-refund-promise": run.observe.verify("no-refund-promise", answer=ticket["answer"]),
    "ticket-complete": run.for_card("agent").observe.verify(
        "ticket-complete", ticket=ticket
    ),
}

run.observe.verify checks against the Service’s Verifiers; run.for_card("agent").observe.verify checks against the agent’s. Both carry the same run_id.

Both calls are synchronous: verify returns once the verdict exists. The reply holds the Run ID, the answer, and each Verifier’s verdict with one entry per requirement. This reply was captured from the Python SDK’s integration run of this app, trimmed of each requirement’s check and provenance; there a local model fixture wrote the answer, so your answer text will differ:

json
{
  "run_id": "01a1512a-1238-7ea5-b9bb-a4f0ad0dab64",
  "answer": "Your order A-1001 ships tomorrow.",
  "checks": {
    "no-refund-promise": {
      "verdict": "passed",
      "requirements": [{
        "id": "no-refund-promise",
        "statement": "The answer never promises a guaranteed refund",
        "outcome": "passed",
        "reason": "the comparison held",
        "actual": "Your order A-1001 ships tomorrow."
      }]
    },
    "ticket-complete": {
      "verdict": "passed",
      "requirements": [{
        "id": "ticket-complete",
        "statement": "The ticket has an answer and an order id",
        "outcome": "passed",
        "reason": "all fields present",
        "citations": [{"slot": "ticket", "locator": {"json_pointer": ""}}]
      }]
    }
  }
}

Now drop the order ID and ask again:

bash
curl -s localhost:8000/ask -H 'content-type: application/json' 
  -d '{"question": "Where is my order?"}'

ticket-complete fails with the reason missing order_id. That is a verdict, not an error: the request succeeds, and your code decides what to do with a failed check. Keep the run_id from either reply for step 8.

A verify call that names a Verifier not bound to the Run’s Card fails with WYRD_SDK_404_UNKNOWN_VERIFIER; evidence for a slot the Verifier doesn’t declare fails with WYRD_VERIFICATION_400_INPUT_INVALID. Those are errors, not verdicts.

7. Queue a policy review

Some answers are documents. POST /reviews asks the agent to write a refund decision letter in Markdown, uploads the letter as the evidence for follows-policy, and replies at once with an execution ID. The judge calls a model, so the customer doesn’t wait for it. submit only queues the check: a returned execution ID means the work is staged, not that a verdict exists:

bash
curl -s localhost:8000/reviews -H 'content-type: application/json' 
  -d '{"question": "Can I get a refund on a damaged item?"}'
desk.py python
execution = run.for_card("agent").observe.submit(
    "follows-policy", letter=verify.File(letter.encode(), filename="letter.md")
)
assert isinstance(execution, verify.Execution)  # Run evidence always queues one

The server runs it: an extract step turns the letter into text, then the tool-less policy-judge Agent, whose prompt holds the refund policy, decides whether the letter follows it and cites the lines it relied on. The policy never travels with the request; changing it means a new judge version, so every verdict names the policy it applied. Read the result back by its ID:

bash
curl -s localhost:8000/reviews/<execution-id>
app.py python
# Any process can pick a check up from its execution ID alone.
state = verify.Execution.connect(execution_id, request.app.state.desk.client).status()

The first read can arrive before the queued execution is visible, and answers 404:

json
{"code": "WYRD_VERIFICATION_404_EXECUTION_NOT_FOUND", "message": "no verification execution with this ID in the caller's tenant"}

Read again. status then moves from pending to running to completed. A completed review, captured from the same integration run and trimmed of check and provenance:

json
{
  "execution_id": "d4be3e01-88fd-7d99-a6e7-5fbf4b775bef",
  "status": "completed",
  "verdict": "passed",
  "requirements": [{
    "id": "follows-policy",
    "statement": "The letter's refund decision follows the refund policy",
    "outcome": "passed",
    "reason": "a 40 dollar refund within 30 days is approved",
    "citations": [{"slot": "letter", "locator": {"lines": {"start": 4, "end": 5}}}]
  }]
}

The citation points at lines 4 and 5 of the letter. Its provenance names the exact judge Agent, Prompt version, and gateway route that decided. errored, timed_out, and cancelled are terminal too: they carry an error with a stable code instead of a verdict, so stop polling on any of them. retrying means read again. Run a verification and inspect its evidence covers every status, and what to do if an execution stays not found.

Nothing about the ID is tied to the process that queued it. Any process with the credential can read it.

8. Query the evidence

Every piece of the Run landed in Bifrost, Wyrd’s warehouse, keyed by the same run_id. One query joins the ticket, the request span, the gateway call, and every verdict:

evidence.py python
SQL = """
SELECT t.run_id, t.question, t.answer, s.trace_id, g.call_id, r.execution_id, r.verdict
FROM vala.datasets.tickets t
JOIN vala.traces.spans s ON s.run_id = t.run_id AND s.name = 'support-desk.ask'
JOIN vala.gateway.calls g ON g.run_id = t.run_id
JOIN vala.verification.results r ON r.run_id = t.run_id
WHERE t.run_id = $1
"""
bash
python evidence.py <run-id>

It prints one row per gateway call and verdict for that Run. For the first /ask Run above, after the app had shut down and flushed, the integration run printed two rows, one per verdict:

text
{'run_id': '01a1512a-1238-7ea5-b9bb-a4f0ad0dab64', 'question': 'Where is my order?', 'answer': 'Your order A-1001 ships tomorrow.', 'trace_id': b'eø÷+AS7›>=KÊ?|I', 'call_id': '01a1512a-1322-7780-8ea1-475e61e2de0d', 'execution_id': '01a1512a-1348-7223-9310-e27a7b1f114e', 'verdict': 'passed'}
{'run_id': '01a1512a-1238-7ea5-b9bb-a4f0ad0dab64', 'question': 'Where is my order?', 'answer': 'Your order A-1001 ships tomorrow.', 'trace_id': b'eø÷+AS7›>=KÊ?|I', 'call_id': '01a1512a-1322-7780-8ea1-475e61e2de0d', 'execution_id': '01a1512a-1341-7a51-ac0a-b2e5d3c5c3cc', 'verdict': 'passed'}

Evidence is not instant. Tickets and spans are buffered in the app, and verdicts are written in batches by the server, so a query right after a request can return fewer rows. Query again, or stop the app first so it flushes. The same SQL runs from the CLI with wyrd query --sql.

If it fails

You seeCauseFix
A gateway error naming openai/gpt-4o or openai/gpt-6-lunaNo openai deployment, or the key lacks that modelRun step 2 and check the key’s model access
WYRD_CLIENT_401_NO_CREDENTIALSThe app found no saved credentialRun it with the same WYRD_CONFIG_HOME as the server from setup
WYRD_VALA_409_BIFROST_FINGERPRINT_MISMATCHA vala.datasets.tickets table with different columns already exists in this tenantUse a fresh tenant in its own config directory, such as WYRD_CONFIG_HOME=~/.config/wyrd-quickstart wyrd server dev --tenant quickstart
The app can’t find .wyrd-bundleThe Cards were never deployedRun python deploy.py from the python/ directory
A review stays pendingThe server is not running background verificationCheck the server terminal for errors
WYRD_VERIFICATION_404_EXECUTION_NOT_FOUND right after /reviewsThe queued execution is not visible yetRead it again
The evidence query returns no rowsRows are still buffered in the app or the serverQuery again, or stop the app so it flushes
WYRD_CLI_409_SERVER_VERSION_INCOMPATIBLEThe CLI and server are different minor versionsUse a CLI and server from the same release

Adapt it to your own app

The same three moves apply to any application you own:

  1. Declare. Write a Service Card whose components pin each Agent, Prompt, or other part by exact version, whose tables hold the rows you record, and whose verified_by binds Verifiers. A hosted model your Prompt names, such as openai/gpt-4o, is not a Model Card; declare a Model Card only for a model artifact you own and version. Run wyrd declare <dir> --check --format json, then wyrd declare <dir> --format json, then wyrd pull the root into a bundle. Define a Service covers each field.
  2. Observe. Load the bundle with WyrdState.from_path, start Bifrost and telemetry once at startup, wrap each unit of work in state.run(), record rows with run.observe.record, and call state.shutdown() on exit. Record a Run and observations shows each call.
  3. Verify. Call verify where your code must act on the verdict, and submit where it must not wait, then read the execution by ID until it is terminal. Run a verification and inspect its evidence covers both.

A coding agent connected through MCP can follow the same path: wyrd.guide {"topic": "start"} returns it step by step.

Clean up

Stop the API with Ctrl-C, then remove the deployment and the stored key:

bash
wyrd gateway deployment delete openai
wyrd gateway credential delete openai-key

Your Cards and verdicts stay in the database and .wyrd/. Delete those as described in Set up Wyrd to start over.

Next

  • Set up the LLM gateway: route other providers and see what each call records.
  • The Rust and TypeScript tabs are reference copies, compiled against the public SDKs by this repository’s check:examples and ts:typecheck lanes. The runnable apps, with manifests and tests, are in the wyrd-example-py, wyrd-example-rs, and wyrd-example-ts repositories.

Last updated