Skip to content

Python client and history store

The package includes a small A2A client for applications (astralvessel.client) and an application-side conversation store (astralvessel.history). Both are optional; any A2A client works.

VesselClient

import asyncio
from astralvessel.client import VesselClient, transcript_of

async def main():
    client = VesselClient("http://vessel:8700", "app-key")
    events = [e async for e in client.stream("field-guide", "alice", "Which birds hunt fish?")]
    print(next(e["text"] for e in events if e["type"] == "answer"))
    messages, turn = transcript_of(events)   # store these as history
    await client.close()

asyncio.run(main())

VesselClient(base_url, app_key, *, transport=None) sends Authorization: Bearer <app_key> on every request. A signed token from a configured issuer works in place of the app key. transport is an optional httpx.AsyncBaseTransport, useful in tests.

Method Returns
stream(agent, user, text, *, history=None, conversation_id=None, images=None) An async iterator of event dicts for one turn
cancel(agent, user, task_id) The JSON-RPC response to CancelTask
rpc(agent, user, method, params) Any JSON-RPC call, as the decoded response
agents(user) The caller's agents from /v1/agents, or [] on an error status
manifest(agent, user) The agent's /manifest
card(agent) The agent card
close() Closes the HTTP client

stream sends the history as base64 JSON bytes, images as (media type, bytes) pairs, and asks for markdown, the transcript, primitives and all four file types. Reads time out after 900 seconds.

Events from stream

Each item is a dict with a type. Every item except error from a refused request also has raw, the decoded A2A frame.

type Keys
task task_id, conversation_id, state
An event type such as text.delta or turn.finished The event's fields, plus task_id, conversation_id and state
status state, text, task_id: a status message that is not an event, such as a rejection reason
transcript messages, turn, last
file name, media_type, data (bytes), call_id
answer text, components, schema_version
error status and detail for a refused HTTP request; detail and raw for a JSON-RPC error

transcript_of(events) returns the turn's messages in order and its closing summary (or None). decode(frame) turns one A2A JSON frame into these items.

HistoryStore

A conversation store in one SQLite file, keyed by owner. Every method takes the owner, and a conversation is visible only to the owner who claimed it.

from astralvessel.history import HistoryStore, new_id

store = HistoryStore("conversations.sqlite")     # or ":memory:"
conversation = new_id()
await store.claim("alice", conversation, "Fish-eating birds")
history = await store.history("alice", conversation)
# ... run the turn with history=history, then:
await store.record_turn("alice", conversation, task_id, question, turn, messages, events=[])
Method Does
claim(owner, conversation_id, title) Creates the conversation, or touches it if this owner has it. Returns False if another owner has it.
history(owner, conversation_id) Every stored message, in order; [] if not this owner's
record_turn(owner, conversation_id, turn_id, user_text, result, messages, events) Appends a turn. result must have a status; result and events are stored as JSON, so drop raw and file bytes first.
list(owner, limit=50) The owner's conversations, most recently updated first, with turn counts
get(owner, conversation_id) A conversation with its turns, or None
delete(owner, conversation_id) Deletes it and its turns; returns whether it existed
sweep(idle_seconds) Deletes conversations idle longer than this; returns (owner, id) pairs

new_id() returns a random 32-character hex id, which is valid as a contextId.