# Northwind Desk — API Base URL: `http://northwind-desk.agents.mycelen.com` A live HTTP API for the **Northwind Desk** agent, built on Irene. Every path is relative to the base URL. ## Auth - **key mode** — send the deploy key in the `X-Irene-Key` header. Add an optional `X-Irene-End-User` header to run many of *your* downstream users on one key (each gets a private history + workspace). - **users mode** — each end-user signs in (`POST /auth/start` → emailed code → `POST /auth/verify`), then mints a stable personal key (`iru_…`) and sends it as `X-Irene-Key`. Calls then run **as that user**, with their own connected accounts. - **open mode** — no key; every request acts as the owner. - **Phone calls** — the voice service sends a per-call key (`irc_…`) minted when the call was admitted; it resolves to a visitor (open number), a key holder (shared PIN) or a signed-in user (own PIN), never the owner. - **Anonymous visitors** are never asked: the owner's per-tool setting decides (Allow everywhere runs it, Deny refuses it); otherwise only read-only tools run. ## Sessions & memory A session is one conversation — its chat history **and** file workspace persist across turns. Get the id from the first `/stream` frame (or `/run`'s `session_id`) and pass it back in the next request to continue. ## Approvals (HIL) Dangerous tools pause for approval. On `/stream` you get a `paused` event → resolve via `POST /resume` (on a second connection). On `/run` they auto-allow / park-for-owner / deny by policy. **To skip approvals**, an authenticated non-visitor caller can send `approve_all: true` (or `approve_tools: [names]`) on `/run`/`/stream` — gated tools then run with no pause. (The owner can disable this per-agent; it never bypasses a `needs_connect`.) ## Per-user accounts (users mode) The agent acts as each user's OWN connected account. If a tool needs one the user hasn't connected, the stream emits `needs_connect` and the run ends — connect via `POST /me/connect/{kit}/start`, then re-send. ## Endpoints ### POST /run Send one message, get the full reply when it's done. Pass `session_id` back to continue a conversation (history + file workspace persist). Tools the owner set to allow run without asking; tools set to deny are refused. Set `approve_all` to auto-approve every other gated tool for this turn (or `approve_tools` for a subset) — it never unblocks a tool the owner denied. **Body** - `message` (string) *(required)* — What you want the agent to do. - `session_id` (string) — Continue a prior session. Omit to start fresh. - `approve_all` (boolean) — Auto-approve every gated tool this turn (skip HIL), except tools the owner denied. - `approve_tools` (array) — Auto-approve only these tool names; others still pause. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/run \ -H 'Content-Type: application/json' \ -d '{"message": "find one popular post about python and give me the title", "approve_all": true}' ``` Response: ```json { "answer": "Here's a popular post: \"Is python still okay today?\"", "session_id": "f4842fc8\u2026", "usage": { "input": 1200, "output": 90 }, "awaiting_approval": [], "denied": [] } ``` ### POST /stream Same body as `/run`, but streams Server-Sent Events — each line is `data: ` and the stream ends with `data: [DONE]`. Append `content` chunks for prose, render `tool_call_*` for a live activity view, and on a `paused` event POST `/resume`. Event types: session · content · reasoning · tool_call_started · tool_call_completed · paused · resumed · needs_connect · done · error · partial_error · cancelled. **Body** - `message` (string) *(required)* — What you want the agent to do. - `session_id` (string) — Continue a prior session. - `approve_all` (boolean) — Auto-approve every gated tool this turn, except tools the owner denied. - `approve_tools` (array) — Auto-approve only these tool names. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/stream \ -H 'Content-Type: application/json' \ -d '{"message": "draft a reply to my latest comment", "approve_all": true}' ``` Response (SSE): ``` data: {"type":"session","data":{"session_id":"f48…"}} data: {"type":"content","data":{"text":"Sure — "}} data: {"type":"done","data":{}} data: [DONE] ``` ### POST /attach Reconnect to a turn that is STILL RUNNING after a closed tab or a reload. The run's lifetime is the server's, not the connection's — dropping the stream does not kill it, and this re-reads it. Repaint the past turns from `GET /sessions/{sid}/messages` FIRST, then hold this stream open. It serves the events produced from NOW ON (never a replay from the top), plus any HIL `paused` event that is still unresolved. If no run is live, the stream carries one terminal `done` frame with `data.reattach = "no_active_run"` and closes, so the page settles instead of waiting. **Body** - `session_id` (string) *(required)* — The session whose run you are rejoining. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/attach \ -H 'Content-Type: application/json' \ -d '{"session_id": "f48\u2026"}' ``` Response (SSE): ``` data: {"type":"session","data":{"session_id":"f48…"}} data: {"type":"content","data":{"text":"second half"}} data: {"type":"done","data":{"usage":{}}} data: [DONE] ``` ### POST /stop Stop this session's running turn SERVER-side (the Stop button). A run is decoupled from its HTTP connection, so closing the tab cancels nothing — this does. It is IDEMPOTENT: call it as often as you like. `stopped` is `true` only when a live run was actually cancelled, and a second call answers `false`. The stopped turn keeps what it already produced and ends with a terminal `cancelled` event on its stream (see `/attach`). **Body** - `session_id` (string) *(required)* — The session whose run to stop. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/stop \ -H 'Content-Type: application/json' \ -d '{"session_id": "f48\u2026"}' ``` Response: ```json { "ok": true, "stopped": true } ``` ### POST /resume When `/stream` emits a `paused` event the run blocks until you resolve it. POST this from a SECOND connection while the stream stays open. You may only resolve your own session's pauses. Tools the owner set to allow never pause; tools set to deny are refused without pausing. **Body** - `session_id` (string) *(required)* — From the paused event. - `tool_call_id` (string) *(required)* — From the paused event. - `decision` (string) *(required)* — allow | allow_for_session | allow_everywhere (owner only: allow this tool on every channel, then run) | answer | deny - `tool` (string) — The paused tool's name (decision=allow_everywhere). - `note` (string) — Optional note (allow). - `payload` (object) — Answer payload (decision=answer). - `reason` (string) — Reason (deny). ```bash curl -X POST http://northwind-desk.agents.mycelen.com/resume \ -H 'Content-Type: application/json' \ -d '{"session_id": "f48\u2026", "tool_call_id": "tc_9", "decision": "allow"}' ``` Response: ```json { "ok": true, "decision": "allow" } ``` ### POST /nudge Inject a message into a turn that's already running (it folds into the live run). **Body** - `session_id` (string) *(required)* — The running session. - `text` (string) *(required)* — The nudge message. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/nudge \ -H 'Content-Type: application/json' \ -d '{"session_id": "f48\u2026", "text": "actually, keep it under 50 words"}' ``` Response: ```json { "ok": true } ``` ### GET /tools The agent's tools and which are `gated` (need approval) — so you know what `approve_all` would auto-run. ```bash curl http://northwind-desk.agents.mycelen.com/tools \ -H 'Accept: application/json' ``` Response: ```json { "tools": [ { "name": "reddit_search_across_subreddits", "description": "Search Reddit across subreddits.", "gated": true } ] } ``` ### GET /sessions Every session belongs to the calling identity. Also: POST /sessions/new, DELETE /sessions/{id}, GET /sessions/{id}/messages (transcript), POST /sessions/{id}/compact, GET /sessions/{id}/context. ```bash curl http://northwind-desk.agents.mycelen.com/sessions \ -H 'Accept: application/json' ``` Response: ```json { "sessions": [ { "id": "f48\u2026", "name": "Reddit cleanup", "updated_at": 1719500000 } ] } ``` ### GET /sessions/{sid}/messages Every record of the session, in the order it happened — NOT a chat-only role/text list. Each record carries its own data and its `ts`. Roles: `user` and `assistant` carry prose (`assistant` chunks from one author coalesce into a single record); `reasoning` carries the turn's thinking, coalesced the same way; `tool` carries the call — `tool_call_id`, `name`, `args`, and any images/view the tool declared; `tool_result` carries that call's result, matched by the SAME `tool_call_id`; `pause` and `resume` carry a gate and the decision that answered it; `done` and `cancelled` carry how the turn ended. `pause_state` is a DIFFERENT thing: an approval still waiting on the user right now, not a record of a settled one. - `sid` (path) — Session id (path). ```bash curl http://northwind-desk.agents.mycelen.com/sessions/SID/messages \ -H 'Accept: application/json' ``` Response: ```json { "session_id": "f4842fc8\u2026", "messages": [ { "role": "user", "text": "check the file", "ts": 1719500000.11 }, { "role": "reasoning", "text": "The user wants the report read.", "member": "", "ts": 1719500000.42 }, { "role": "tool", "tool_call_id": "tc1", "name": "write_file", "args": { "path": "out.txt" }, "ts": 1719500000.63 }, { "role": "tool_result", "tool_call_id": "tc1", "name": "write_file", "result": { "ok": true, "path": "out.txt" }, "ts": 1719500001.08 }, { "role": "assistant", "text": "Wrote the file.", "member": "", "ts": 1719500001.44 }, { "role": "done", "termination_reason": "completed", "usage": {}, "ts": 1719500001.45 } ], "pause_state": null } ``` ### GET /files Files the agent produced in a session's workspace. Download one with GET /files/raw?session_id=…&path=…. Workspaces persist across the caller's sessions. - `session_id` (query) — Session id. ```bash curl http://northwind-desk.agents.mycelen.com/files?session_id=VALUE \ -H 'Accept: application/json' ``` Response: ```json { "files": [ { "path": "chart.png", "mtime": 1719500000 } ] } ``` ### POST /files Upload a file INTO a session's workspace (multipart/form-data, field `file`) — the agent then reads it with its file tools. 25 MB max (over → 413); a same-named file is auto-suffixed. Uploading to a session you don't own → 404. Do NOT set `Content-Type` yourself — let the client set the multipart boundary. **Body** (`multipart/form-data`) - `file` (file) *(required)* — The file to upload (binary). - `session_id` (query) — Session id. ```bash curl -X POST http://northwind-desk.agents.mycelen.com/files?session_id=VALUE \ -F 'file=@/path/to/file' ``` Response: ```json { "session_id": "f48\u2026", "path": "report.pdf", "name": "report.pdf", "size": 20841 } ``` ### GET /approvals OWNER only: tool approvals parked by autonomous /run turns. ?status=all|pending|allowed|denied. - `status` (query) — Filter (default pending). ```bash curl http://northwind-desk.agents.mycelen.com/approvals?status=VALUE \ -H 'Accept: application/json' ``` Response: ```json { "approvals": [ { "tool_name": "reddit_create_reddit_post", "status": "pending", "session_id": "f48\u2026", "created_at": "2026-06-28T\u2026" } ] } ``` ### GET /health Liveness + the agent's name and auth mode. No auth. ```bash curl http://northwind-desk.agents.mycelen.com/health \ -H 'Accept: application/json' ``` Response: ```json { "ok": true, "agent": "reddit_multitenant", "name": "Reddit Multitenant", "auth_mode": "users" } ``` Full machine-readable spec: `GET /openapi.json`.