Chat with a project
You drive a project by chatting with it. Aura reads files, edits inputs, runs the solver, and replies. One message plus everything it triggers is a turn.
Send a message and wait
Section titled “Send a message and wait”const reply = await aura.chat(project.id, "Solve this and summarize the result.");console.log(reply.content);reply = await aura.chat(project.id, "Solve this and summarize the result.")print(reply.content)# one message in, one reply out, then exitaura chat --message "Solve this and summarize the result."
# or the interactive terminal UIaura chatHow the wait works
Section titled “How the wait works”chat() connects to the project WebSocket, dispatches the message, and returns the completed
reply. In an environment that blocks WebSockets it falls back to polling /chat/sync — same
result, later. It gives up after fifteen minutes; the timeout and the fallback’s poll interval are
both adjustable. That bound is total duration, not inactivity, so a long solve can exceed it while
perfectly healthy — raise it rather than re-sending the message.
Two details matter on a project several callers share:
- You get your own reply. The match is on the dispatched message’s sequence number, not on a message count, so another caller’s turn is never handed back to you.
- The wait is pinned to one conversation — the one your dispatch landed in. Activating or creating another conversation mid-turn does not redirect it.
A timeout means the turn is genuinely still running. A socket that dies announces itself, but
one that goes half-open just stops delivering — and a turn’s terminal event with it. So the wait
never concludes anything from silence: whenever the stream has been quiet for quietProbeMs /
quiet_probe (30 seconds by default, floored at 1 second), and again before the deadline expires,
the turn’s state is read back from the server. A turn that finished is returned however its terminal
event was lost, so a timeout is never a finished turn you missed.
const reply = await aura.chat(project.id, "Re-solve with a 30 minute budget.", { conversationId, // omit to use the project's active conversation pollIntervalMs: 2000, timeoutMs: 15 * 60_000, // long solves need a longer wait});reply = await aura.chat( project.id, "Re-solve with a 30 minute budget.", conversation_id=conversation_id, # omit for the active conversation poll_interval=2.0, timeout=15 * 60,)Watching a turn as it happens
Section titled “Watching a turn as it happens”onDelta / on_delta receives assistant text as it is generated, and onToolActivity /
on_tool_activity fires as the agent starts and finishes tools. Both are no-ops on the poll
fallback, so treat them as progress reporting rather than a delivery guarantee — chat() still
returns the completed reply.
await aura.chat(project.id, "Solve this and summarize the result.", { onDelta: (text) => process.stdout.write(text), onToolActivity: (activity) => console.error(`[${activity.status}] ${activity.tool}`),});import sysfrom aura_sdk import ToolActivityData
def on_tool_activity(activity: ToolActivityData) -> None: print(f"[{activity.status}] {activity.tool}", file=sys.stderr)
await aura.chat( project.id, "Solve this and summarize the result.", on_delta=lambda text: print(text, end="", flush=True), on_tool_activity=on_tool_activity,)Both callbacks are plain functions called on the event loop, so keep them quick — hand off to a queue or a task if they need to await. A client that could not open the socket once polls for the rest of its life.
Choosing a conversation
Section titled “Choosing a conversation”A project can hold several conversations. Without one named, chat uses the project’s active conversation.
const conversation = await aura.createConversation(project.id, "budget sweep");
await aura.chat(project.id, "Try a 20% larger fleet.", { conversationId: conversation.id });
const all = await aura.listConversations(project.id);conversation = await aura.create_conversation(project.id, "budget sweep")
await aura.chat(project.id, "Try a 20% larger fleet.", conversation_id=conversation.id)
all_conversations = await aura.list_conversations(project.id)aura conversation listaura chat --conversation <conversation-id>
# start a fresh threadaura chat --new-conversation --title "budget sweep"A session pins one conversation id and sends it with every turn, so the transcript stays in sync even if the project’s active conversation changes elsewhere.
Stopping a turn
Section titled “Stopping a turn”import { AuraTurnCancelledError } from "@strangeworks-inc/strangeworks-aura-sdk";
const controller = new AbortController();
// e.g. a Ctrl-C handler — stop both sidesprocess.on("SIGINT", () => { void aura.cancel(project.id, conversationId); controller.abort();});
const reply = await aura .chat(project.id, "Solve this.", { conversationId, signal: controller.signal }) .catch((err) => { if (err instanceof AuraTurnCancelledError) return err.partialContent; throw err; });from aura_sdk import AuraTurnCancelledError, AuraTurnTimeoutError
try: reply = await aura.chat(project.id, "Solve this.", timeout=300)except AuraTurnTimeoutError as timed_out: # still running server-side await aura.cancel(project.id, timed_out.conversation_id) raiseexcept AuraTurnCancelledError as cancelled: partial = cancelled.partial_contentWhile the agent is working, Esc (or Ctrl-C) cancels the turn and keeps you in the session; a
second Ctrl-C quits. This stops the turn server-side, and whatever the agent produced first is
shown dimmed under ⊘ cancelled. Ctrl-C during --message does the same and exits 130.
cancel(projectId, conversationId) returns whether anything was stopped. Omitting the conversation
id cancels any running turn in the project, including one someone else started.
Driving the poll yourself
Section titled “Driving the poll yourself”To avoid blocking, dispatch the message and read state on your own schedule.
const dispatched = await aura.sendMessage(project.id, { content: "Solve this." });const conversationId = dispatched.message.conversation_id;
let reply;for (;;) { const state = await aura.getConversationState(project.id, conversationId); reply = [...state.messages] .reverse() .find((m) => m.role === "assistant" && m.sequence > dispatched.message.sequence); if (reply && state.turn.state === "idle") break; await new Promise((r) => setTimeout(r, 2000));}import asyncio
dispatched = await aura.send_message(project.id, "Solve this.")conversation_id = dispatched.message.conversation_id
while True: state = await aura.get_conversation_state(project.id, conversation_id) reply = next( (m for m in reversed(state.messages) if m.role == "assistant" and m.sequence > dispatched.message.sequence), None, ) if reply is not None and state.turn.state == "idle": break await asyncio.sleep(2)Match the reply the way chat() does — an assistant message whose sequence is above your
dispatch’s — rather than taking the newest row, which may be a system notice or another caller’s
message. Read the pinned conversation’s turn.state; getAgentStatus / get_agent_status is
project-wide, so on a shared project it also reports other people’s turns.
The interactive TUI
Section titled “The interactive TUI”aura chat without --message opens a full-screen terminal UI: a pinned header, a
scrolling transcript, and a composer at the bottom. It uses the alternate screen buffer, so your
shell’s scrollback is untouched, and prints the command to resume the session on exit.
| Key | Does |
|---|---|
enter |
Send |
shift+enter (or alt+enter) |
Newline — compose a multi-line message |
↑ ↓ |
Recall history; move between lines within a multi-line message |
pgup pgdn, wheel |
Scroll the transcript |
| drag with the mouse | Select text — releasing copies it to the clipboard |
click a ⧉ n badge |
Copy that message; /copy n does the same, /copy takes the whole thread |
esc |
Cancel the running turn, stay in the session |
/help, /exit |
In-session commands — a leading / acts locally rather than sending a turn |
ctrl-c twice, or exit |
Quit |
The chat session page has the full key list, including word motion, line editing and the quick-reply picker.
- Read results — executions, output files, the solution.
- Errors — the three ways a turn can end badly, and which is which.