Run-loop WebSocket

The run-loop endpoint moves the agent loop server-side. Instead of you calling the model, parsing tool calls, and re-prompting, the server runs the model↔tool iteration and streams you frames. Your client’s job is to execute tool calls and send results back — a tool-executor.
wss://aius.co/api/v1/runs/ws
This is a bespoke JSON-over-WebSocket protocol, not OpenAI-wire. Each frame is a single JSON object with a type field.

Lifecycle

1

Connect & authenticate

Open the socket, then send an auth frame with your aius_… token. You have ~5 seconds; an invalid token closes the socket (code 1008).
2

Start a run

Send a start frame with the agent, model, and seed messages.
3

React to ready

The server replies with ready, carrying the session/run identifiers.
4

Execute tools

For each tool_call frame, run the tool locally and reply with a tool_result frame. Render any assistant_message frames as they arrive.
5

Finish

The server sends run_completed when the loop ends, then closes the socket.

Client → server frames

auth (required, first)

{ "type": "auth", "token": "aius_xxxxxxxx..." }

start (required, second)

{
  "type": "start",
  "agent": "build",
  "model": "anthropic/claude-sonnet-4",
  "messages": [
    { "role": "user", "content": "List the files in the project." }
  ],
  "generate_title": true,
  "session_id": "optional-resume-id",
  "run_id": "optional",
  "step_run_id": "optional"
}
FieldTypeRequiredNotes
typestringYesMust be "start".
agentstringNoAgent profile to run (e.g. build). Influences server-side config.
modelstringNoAnthropic Claude slug. Defaults server-side if omitted.
messagesarrayYesOpenAI-wire seed messages for the conversation.
generate_titlebooleanNoIf true, the server emits a one-off title frame for the session.
session_id / run_id / step_run_idstringNoProvide to resume / correlate; otherwise the server generates them.

tool_result (in response to each tool_call)

{
  "type": "tool_result",
  "tool_call_id": "tc_abc123",
  "status": "ok",
  "result": "file1.txt\nfile2.txt"
}
FieldTypeNotes
tool_call_idstringMust match the tool_call frame’s tool_call_id.
statusstring"ok" on success; anything else is treated as a failure. Defaults to "ok".
resultanyThe tool output. Strings are passed through; other JSON values are stringified before being fed back to the model.
If you don’t reply to a tool_call within the server’s tool timeout, the run treats that call as a timeout and the model is told the tool didn’t complete. Always send a tool_result for every tool_call.

Server → client frames

ready

Sent once after a valid start. Capture these IDs for correlation/resume.
{
  "type": "ready",
  "session_id": "sess_...",
  "run_id": "run_...",
  "step_run_id": "step_..."
}

title (optional)

Only when generate_title: true was sent — a generated, human-readable session title. Best-effort; may not arrive.
{ "type": "title", "title": "List project files" }

assistant_message

The agent’s prose for an iteration. Emitted mid-run alongside tool calls, and again at the end for the final message. Use id to key/dedupe blocks so successive messages render as distinct turns.
{
  "type": "assistant_message",
  "id": "llm_...",
  "message": { "role": "assistant", "content": "I'll list the files now." }
}

tool_call

The server wants your client to execute a tool. Run it, then reply with a matching tool_result.
{
  "type": "tool_call",
  "tool_call_id": "tc_abc123",
  "name": "list_files",
  "arguments": "{\"path\": \".\"}"
}
FieldNotes
tool_call_idEcho this back in your tool_result.
nameTool/function name to execute.
argumentsThe model’s arguments. Typically a JSON string (OpenAI function-call style) — parse before use.

run_completed

Terminal frame: the loop finished. The socket closes shortly after.
{
  "type": "run_completed",
  "status": "completed",
  "session_id": "sess_...",
  "run_id": "run_..."
}
status is "completed" when the model stopped requesting tools, or "max_iterations" if the loop hit its iteration cap.

error

Something went wrong during the run.
{ "type": "error", "detail": "run failed" }

Close codes

CodeMeaning
1008Policy violation — run loop disabled, auth failed/timed out, or a malformed auth/start frame.

End-to-end example

A minimal tool-executor that handles one tool and runs to completion.
import asyncio, json, websockets

TOKEN = "aius_xxxxxxxx..."
URL = "wss://aius.co/api/v1/runs/ws"

async def run():
    async with websockets.connect(URL) as ws:
        await ws.send(json.dumps({"type": "auth", "token": TOKEN}))
        await ws.send(json.dumps({
            "type": "start",
            "agent": "build",
            "model": "anthropic/claude-sonnet-4",
            "messages": [{"role": "user", "content": "List the files, then summarize."}],
            "generate_title": True,
        }))

        async for raw in ws:
            frame = json.loads(raw)
            t = frame.get("type")
            if t == "ready":
                print("run:", frame["run_id"])
            elif t == "title":
                print("title:", frame["title"])
            elif t == "assistant_message":
                print("assistant:", frame["message"].get("content"))
            elif t == "tool_call":
                args = json.loads(frame.get("arguments") or "{}")
                result = execute_tool(frame["name"], args)   # your code
                await ws.send(json.dumps({
                    "type": "tool_result",
                    "tool_call_id": frame["tool_call_id"],
                    "status": "ok",
                    "result": result,
                }))
            elif t == "run_completed":
                print("done:", frame["status"])
                break
            elif t == "error":
                print("error:", frame.get("detail"))
                break

def execute_tool(name, args):
    if name == "list_files":
        import os
        return "\n".join(os.listdir(args.get("path", ".")))
    return f"unknown tool: {name}"

asyncio.run(run())