• Joined on 2024-05-22

worldtree-sdk (1.0.0)

Published 2026-07-18 23:08:26 -07:00 by vh

Installation

pip install --index-url https://gitea.phasefinal.com/api/packages/vh/pypi/simple/ --extra-index-url https://pypi.org/simple worldtree-sdk

About this package

Official async Python consumer SDK for the Worldtree Conversation API

worldtree-sdk (Python)

Official async Python consumer SDK for the Worldtree Conversation API — the Python sibling of the shipped TypeScript v1.0.0, built off the same behavioral spec and the same fixture corpus (../docs/behavioral-spec.md, ../fixtures/), re-idiomatized to async Python per the contract at ../docs/contracts/worldtree-sdk-python-v0.contract.md.

  • Async-first. async with WorldtreeClient(...), async for ev in client.sessions.stream_turn(...); cancellation via native asyncio.
  • Resilient streaming. stream_turn auto-resumes across connection drops with jittered backoff, replaying only unseen events — one continuous event sequence.
  • One runtime dependency: httpx. Stdlib dataclasses events; no Pydantic.
  • Wire-verbatim snake_case. No field-mapping layer; reads are open-world, so additive server fields are never dropped.
  • Python ≥ 3.11, mypy --strict-clean, ships py.typed.

Install

uv pip install worldtree-sdk        # from the internal Gitea package index

Quickstart

import asyncio
from worldtree_sdk import WorldtreeClient

async def main() -> None:
    async with WorldtreeClient("https://api.worldtree.example", auth="wt-...") as client:
        async for ev in client.sessions.stream_turn(session_id, "Hello there"):
            if ev.type == "text":
                print(ev.content, end="", flush=True)
            elif ev.type == "done":
                print(f"\n[{ev.phase}] {ev.usage}")

asyncio.run(main())

Streaming a turn

client.sessions.stream_turn(session_id, content) is the default resilient stream (auto-resume on drops). stream_turn_raw(...) is the single-attempt primitive if you want to drive reconnection yourself.

Events are frozen dataclasses in a discriminated union (TurnEvent) — switch on ev.type. Every event carries sse_id, turn_id, and raw (the full parsed envelope, so unknown/additive fields are always reachable):

ev.type dataclass key fields
text / thinking TextEvent / ThinkingEvent content
text_boundary TextBoundaryEvent kind, char_offset
tool_start / tool_result ToolStartEvent / ToolResultEvent name, arguments / result
affect_update AffectUpdateEvent status, snapshot
worker_phase / awaiting_llm_first_token phase / elapsed_ms_since_building_prompt
done / error / cancelled terminal response/usage · message/error_code · reason

Cancel a running turn (independent of the stream, which keeps yielding until its terminal):

await client.sessions.cancel_turn(session_id, turn_id, persist_partial=True)

Abandoning a stream: breaking the async for (or the surrounding task being cancelled) closes the connection but does not cancel the server turn by default (abandon="detach"). Pass abandon="cancel" for break-out-means-cancel UX:

async for ev in client.sessions.stream_turn(sid, content, abandon="cancel"):
    if ev.type == "text" and stop_requested:
        break   # closes the connection AND issues a best-effort cancel

REST surface

Every REST call is await-ed and returns the parsed body (open-world dict, or a list for the array endpoints); 204 responses return None.

# sessions
page   = await client.sessions.list(limit=25, include_archived=True)
info   = await client.sessions.get(session_id)
msgs   = await client.sessions.messages(session_id, limit=50, cursor=page["next_cursor"])
new    = await client.sessions.create({"agent_id": "a1", "config": {"system_prompt": "…"}})
await client.sessions.update(session_id, {"name": "Renamed", "archived": True})
await client.sessions.delete(session_id)

# persona state (W-7) — the canonical PAD wrapper; a non-finite axis is rejected pre-HTTP
from worldtree_sdk import PadState
await client.sessions.set_persona_state(session_id, PadState(0.4, -0.1, 0.2))

# authored history (#347) — idempotency_key is REQUIRED and never auto-generated;
# a replay with the same (session, key) returns the original ack.
turn = await client.sessions.write_history(
    session_id,
    {"author": "assistant", "content": "…", "idempotency_key": "unique-key-1"},
)

# identity / catalogs
me    = await client.me.get()
usage = await client.me.usage(window="7d", breakdown=True)
caps  = await client.capabilities.get()
models = await client.models.available_for_characters()

# characters
char  = await client.characters.create({"character": {"name": "Nyx"}})
state = await client.characters.state(character_id)

# consumer agents (Tier-3)
agents = await client.agents.list()
agent  = await client.agents.define(
    {"agent_name": "Nyx", "role": "gpt-echo", "system_prompt": "be nyx"}
)

Admin

Admin calls need a separate admin credential and use it per-request — they never fall back to the default bearer. A client built without admin_auth raises ConfigurationError before any network I/O.

async with WorldtreeClient(base, auth="user-key", admin_auth="admin-key") as client:
    keys = await client.admin.keys.list(user_prefix="acme/")
    await client.admin.keys.issue({"label": "ci", "user_id": "u1", "tier": "pro"})
    await client.admin.keys.bulk_rotate({"user_prefix": "acme/", "grace_seconds": 300})
    await client.admin.persona.archive({"end_user_id": "u1"})
    await client.admin.sessions.retire(session_id)
    await client.admin.users.change_tier("u1", {"tier": "pro"})
    rollup = await client.admin.usage(window="30d", breakdown=True)
    async for ev in client.admin.stream_events():     # D2 admin lifecycle stream
        ...

Auth & transport

  • auth is a static key or a provider callable (sync or async) returning a short-lived token — resolved per request:

    WorldtreeClient(base, auth=lambda: mint_heimdall_token())          # sync provider
    WorldtreeClient(base, auth=async_token_provider)                   # async provider
    
  • Bifrost-bound sessions.create requires a per-request consumer_key (checked pre-HTTP): await client.sessions.create(body, consumer_key="ck-…").

  • Transport ownership. By default the client owns a private httpx.AsyncClient (closed by aclose() / async with). Inject your own to share a pool — it stays caller-owned and is never closed by the SDK:

    async with httpx.AsyncClient() as pool:
        client = WorldtreeClient(base, auth="…", transport=pool)   # pool NOT closed by aclose()
    

Errors

One base, three branches (WorldtreeError): stream/connection failures (ConnectFailed + AgentNotAvailable / TurnLaunchUnavailable / SessionRetired, ConnectionDropped, ProtocolErrors, ResumeErrors), CancelErrors, and the REST ApiError floor. Client-misuse is a plain ValueError/TypeError raised pre-HTTP.

from worldtree_sdk import ApiError

try:
    await client.sessions.get(session_id)
except ApiError as e:
    print(e.status, e.error_code, e.message)   # e.g. 404 "session_not_found"

error_code is an open string — a novel server code is carried verbatim, never rejected. Hide-existence 404s (feature-absent / ungranted / session-absent) are surfaced undiscriminated on the ApiError floor by design (no NotFoundError). Bearer tokens are redacted from all error strings.

Development

uv venv
uv pip install -e '.[dev]'
uv run pytest          # conformance (shared ../fixtures corpus) + unit tests
uv run mypy            # strict typing gate
uv run ruff check .    # lint gate

The conformance runner exercises the shared ../fixtures/*.json corpus — the executable acceptance floor. The corpus is the floor, not the ceiling: every B-* behavioral rule binds whether or not a fixture covers it.

Requirements

Requires Python: >=3.11
Details
PyPI
2026-07-18 23:08:26 -07:00
104
Proprietary
99 KiB
Assets (2)
Versions (5) View all
1.2.0 2026-08-02
1.1.2 2026-08-01
1.1.1 2026-08-01
1.1.0 2026-07-30
1.0.0 2026-07-18