Skip to content

Agent chat API

Santiago ships a built-in AI agent that runs inside the daemon: you send it a natural-language instruction and it drives the running profile for you using the automation API and the agent skill. This page documents the agent’s own HTTP surface — checking whether an LLM is configured, storing an API key, streaming a chat as Server-Sent Events, and managing per-profile history.

All endpoints live under the daemon base http://localhost:7891/api and use the standard envelope: { "ok": true, "data": {...} } on success, or { "ok": false, "error": { "code", "message" } } on failure. The daemon listens on localhost only.

MethodPathPurpose
GET/api/agent/statusIs an LLM configured? List available models.
GET/api/agent/api-keyGet the configured key for a provider (masked).
POST/api/agent/api-keySet the API key for a provider.
DELETE/api/agent/api-keyRemove the API key for a provider.
GET/api/agent/:profileId/historyRead stored chat messages for a profile.
POST/api/agent/:profileId/chatSend a message → SSE stream of the agent’s run.
POST/api/agent/:profileId/stopAbort the agent’s current execution.
DELETE/api/agent/:profileIdClear history and destroy the session.

The provider defaults to openai when the provider field or query parameter is omitted.

Before the agent can run, it needs an LLM API key. Keys are stored per provider in the daemon’s local auth store, not in the profile.

GET /api/agent/status tells you whether any provider key is configured and which models are available.

Check if the agent is ready
curl -s http://localhost:7891/api/agent/status | jq .data
Response
{
"configured": true,
"models": [
{ "provider": "openai", "id": "gpt-5", "name": "GPT-5" }
]
}

When no key is set, the daemon returns { "configured": false, "models": [] } — it never errors on this endpoint.

POST /api/agent/api-key stores a key for a provider. apiKey is required; an empty or whitespace-only value returns 400 BAD_REQUEST.

Store an API key
curl -s http://localhost:7891/api/agent/api-key -X POST \
-H 'Content-Type: application/json' -d '{
"provider": "openai",
"apiKey": "sk-..."
}'
Response
{ "ok": true }

GET /api/agent/api-key returns the stored key for a provider, masked to its first and last four characters. Pass ?provider= to target a non-default provider.

Read the stored key for a provider
curl -s "http://localhost:7891/api/agent/api-key?provider=openai" | jq .data
Response
{ "provider": "openai", "hasKey": true, "maskedKey": "sk-1…7f9c" }

If no key is stored, hasKey is false and maskedKey is null.

Remove the stored key for a provider
curl -s "http://localhost:7891/api/agent/api-key?provider=openai" -X DELETE
Response
{ "ok": true }

POST /api/agent/:profileId/chat sends one user message and streams the agent’s run back as Server-Sent Events. The profile must be running — if it isn’t, the daemon responds 404 with PROFILE_NOT_RUNNING. An empty message returns 400 BAD_REQUEST.

Send a message and stream the run
curl -N -s http://localhost:7891/api/agent/$PROFILE/chat -X POST \
-H 'Content-Type: application/json' -d '{
"message": "Go to example.com and tell me the page title"
}'

The -N flag disables curl buffering so you see events as they arrive. The response has Content-Type: text/event-stream; each event is a line of the form data: <json>\n\n.

Each data: line carries a JSON object with a type discriminator. These are the events the daemon emits during a run:

typeFieldsMeaning
tool_startname, paramsThe agent began an automation tool call (e.g. navigate, click).
tool_endname, okThat tool call finished; ok: false means it errored.
text_deltacontentA chunk of the agent’s text reply — concatenate deltas in order.
usageinput, output, totalToken totals, sent once after the run completes.
doneThe agent finished this turn.
errormessageThe run failed; the message is also stored in history.
Example event stream
data: {"type":"tool_start","name":"navigate","params":{"url":"https://example.com"}}
data: {"type":"tool_end","name":"navigate","ok":true}
data: {"type":"tool_start","name":"snapshot","params":{}}
data: {"type":"tool_end","name":"snapshot","ok":true}
data: {"type":"text_delta","content":"The page title is "}
data: {"type":"text_delta","content":"\"Example Domain\"."}
data: {"type":"done"}
data: {"type":"usage","input":4821,"output":137,"total":4958}

To interrupt a long-running turn, call the stop endpoint with the same profile id. It returns whether an active run was aborted.

Abort the current run
curl -s http://localhost:7891/api/agent/$PROFILE/stop -X POST | jq .data
Response
{ "aborted": true }

Each profile keeps its own chat history in the daemon. Messages are stored as you chat — user messages, the agent’s tool calls, agent text, and any errors.

Read stored messages for a profile
curl -s http://localhost:7891/api/agent/$PROFILE/history | jq .data
Response (shape)
{
"messages": [
{ "type": "user", "content": "Go to example.com and tell me the page title" },
{ "type": "tool", "content": "", "toolName": "navigate", "params": { "url": "https://example.com" } },
{ "type": "agent", "content": "The page title is \"Example Domain\"." }
]
}

Message type is one of user, tool (carries toolName and params), agent, or error.

DELETE /api/agent/:profileId clears the stored messages and destroys the in-memory agent session for that profile. The next chat starts fresh (and re-injects the skill).

Reset the agent for a profile
curl -s http://localhost:7891/api/agent/$PROFILE -X DELETE
Response
{ "ok": true }
StatuscodeWhen
400BAD_REQUESTapiKey or message missing/empty.
404PROFILE_NOT_RUNNINGThe target profile isn’t launched.
500AGENT_ERRORThe session couldn’t be created.
500INTERNAL_ERRORStoring or removing a key failed.

Mid-stream failures during a chat aren’t HTTP errors — the connection has already returned 200, so a failure arrives as an error SSE event instead.