Connecting an application¶
Your application reaches the vessel over the deployment's internal front network, and the vessel reaches your
application the same way for service tools. This page shows how to attach an application and what it must do on
each turn.
Attach to the front network¶
The generated Compose file publishes no ports. Attach your application's container to the front network, which
Compose names <project>_front (astralvessel_front by default):
# your application's compose.yml
services:
app:
image: my-app:latest
environment:
VESSEL_URL: http://vessel:8700
networks: [default, vessel]
networks:
vessel:
external: true
name: astralvessel_front
The vessel listens at front_address on port 8700, and containers on front reach it by name as
http://vessel:8700. The front network is internal, so it gives no route out;
keep your application on its own network too for anything else it needs. A service grant's url names your
application as the vessel would reach it on this network.
Set [vessel] public_url to the base URL your application uses, so agent cards and /v1/agents show reachable
addresses.
What your application does on each turn¶
- Authenticate with its app key and name the user in
X-Vessel-User(or send the user's token). - Send the history. The vessel keeps no conversations. Send the messages stored from earlier turns as a
transcript part, and a stable conversation id as the message's
contextId. - Ask for the transcript. List
application/vnd.astralvessel.transcript+jsoninacceptedOutputModesto get the turn's new messages back. - Store the new messages and send them with the next turn.
- Store files from
fileartifacts if you want to keep them; the vessel does not.
The bundled Python client does steps 1 to 3, and its history store does step 4:
from astralvessel.client import VesselClient, transcript_of
from astralvessel.history import HistoryStore, new_id
client = VesselClient("http://vessel:8700", app_key)
store = HistoryStore("conversations.sqlite")
async def ask(user: str, conversation_id: str, question: str) -> str:
await store.claim(user, conversation_id, question[:60])
history = await store.history(user, conversation_id)
events = [e async for e in client.stream("field-guide", user, question, history=history,
conversation_id=conversation_id)]
messages, turn = transcript_of(events)
task_id = next(e["task_id"] for e in events if e["type"] == "task")
await store.record_turn(user, conversation_id, task_id, question, turn or {"status": "unknown"}, messages, [])
return next((e["text"] for e in events if e["type"] == "answer"), "")
Start a conversation with conversation_id = new_id(). To call agents from another language, see
HTTP and A2A protocol.
Calling from an orchestrator¶
An orchestrator that wants only the answer can send a non-streaming SendMessage with a structured skill call, the
shape Astral uses for external agents:
{"jsonrpc": "2.0", "id": "1", "method": "SendMessage", "params": {"message": {
"messageId": "m1", "role": "ROLE_USER",
"parts": [{"data": {"method": "tools/call", "name": "field-guide",
"arguments": {"question": "Which birds hunt fish?"}}}]}}}
The question is read from the first of question, message, query, text or prompt in arguments. The
completed task's answer artifact carries the Markdown answer and any components under _ui_components. Without
the transcript media type in acceptedOutputModes, no transcript is streamed.
Readiness¶
GET /health (no credentials) returns whether each agent's sandbox is ready:
Calls to an agent whose sandbox is not ready get 503. The vessel checks sandboxes every 5 seconds until all are ready, then every 60 seconds.