AbstractAssistant is a desktop app and a CLI. Its stable public surface is the user entry points, the local files they maintain, and the gateway routes they rely on.
See also:
Web, Watch and iPhone use the Gateway's account-scoped conversation read ledger at
GET/POST /api/gateway/me/session-reads. The shared Apple client exposes
SessionReadReceipt and SessionReadState; consumption records the displayed run revision,
and failed writes retain its unread mark. The web client uses AbstractUIC's shared read client.
A new answer remains unread independently of its request. Other clients refresh after
session.read on the personal event stream or when reconnecting. Installed native apps need a
build supporting this API. Existing device-local first-use baselines are not uploaded as reads.
From pyproject.toml: assistant, abstractassistant (same program) and build-macos-app.
assistant --help
assistant run --helpConnection settings resolve from global flags first, then the environment. Global flags must appear before a subcommand.
The first match wins:
--gateway-url URL(or its legacy environment aliasABSTRACTGATEWAY_URL/ABSTRACTFLOW_GATEWAY_URL);- the sign-in saved in Settings → Connection (
gateway_connection.json); - this computer's gateway:
- when AbstractGateway 0.6.0 or later is installed in the same Python environment as the
Assistant (as the AbstractFramework installer does), the gateway's own rule: its running
server, else its pinned OS service, else the port stored in its Network setting, else
127.0.0.1:8080; - otherwise (the macOS app bundle, which does not include AbstractGateway, or a gateway older
than 0.6.0), the local gateway pointer file
~/.abstractframework/gateway.json, which the AbstractFramework installer andabstractgateway serve(AbstractGateway 0.7.0 or later) write with the address the gateway listens on;
- when AbstractGateway 0.6.0 or later is installed in the same Python environment as the
Assistant (as the AbstractFramework installer does), the gateway's own rule: its running
server, else its pinned OS service, else the port stored in its Network setting, else
http://127.0.0.1:8080.
Tier 3 is consulted only when neither tier 1 nor tier 2 applies, so a launch that names its
gateway, or a saved sign-in, never depends on it. A sign-in saved against http://127.0.0.1:8080
follows tier 3 when this computer's gateway answers elsewhere; a sign-in saved for any other URL is
kept as you chose it. When tier 3 is needed and an installed AbstractGateway cannot be imported, or
its rule fails, the Assistant stops with that error instead of guessing a URL.
The pointer file is read once per launch and holds no token. A missing file is normal (no
installer ran, or the gateway was uninstalled) and gives tier 4. The app ignores the file, logs one
warning naming the reason, and uses tier 4 when the file is not a regular file (a symlink is
refused), is owned by another user or writable by other users (macOS, Linux), cannot be read, is not JSON, has a schema
other than 1, or has a url that is not http/https on 127.0.0.1, [::1] or localhost
with nothing after the port (no path, query or user info). These are the same rules, checked
against the same shared cases, as the AbstractFramework web apps and terminal consoles. The file's
format:
{"schema": 1, "url": "http://127.0.0.1:8081", "port": 8081,
"data_dir": "/Users/me/Library/Application Support/abstractgateway",
"updated_at": "2026-09-27T12:00:00Z", "written_by": "installer"}Only schema and url are read; the other fields are for the gateway and the installer.
--gateway-url URL— the gateway to use. Without--gateway-token, the sign-in saved for that gateway ingateway_connection.jsonis used.--gateway-token TOKEN--gateway-handover-file PATH— set by the AbstractGateway console when you click Open on the assistant's card. The file holds a one-time sign-in code (valid once, for two minutes); the app reads it, deletes it, trades the code with the gateway for a session and saves that session. You do not pass this flag yourself. Only a file the gateway wrote is accepted (a regular file you own, mode 0600, in the gateway's hand-over format, naming the gateway user who clicked Open); any other path, or a file that does not name the user, is refused and left untouched. If you are already signed in to that gateway as that same user, that sign-in is kept; a sign-in to another gateway or as another user is signed out and replaced, and the app tells you who is signed in and who was signed out. An Assistant that is already running does not receive a hand-over: quit it and click Open again. The flow is drawn in architecture.md.--version— prints the version fromabstractassistant/_version.py, the same value Settings → About and the macOS app bundle show.
assistant --gateway-url http://127.0.0.1:9090 --gateway-token "$ABSTRACTGATEWAY_AUTH_TOKEN" run --prompt "Summarize today's AI news with sources."Starts the menu-bar app and palette. It loads local state from ~/.abstractassistant/, connects
to the gateway, resolves the workflow chosen in Settings → Workflow and follows runs over
SSE.
Workflow resolution:
- Gateway default (the default choice): when the gateway reports a default workflow for
abstractassistant.agent.v1(default_agent_workflowson/api/gateway/bundles?executable_for=abstractassistant.agent.v1), each run starts withflow_id: "@default"andinterface: "abstractassistant.agent.v1", and the gateway resolves it at run start. When it reports none, the built-inabstractassistant-orchestratorworkflow runs, and Settings shows the gateway's reason. The app publishes that workflow to the tenant catalog with explicit versions (the first is 0.0.1) and labels it without a number until one exists. When neither is available, sending is blocked with a message saying why; the app never picks another workflow on its own. - A chosen workflow runs its latest published version. If it is removed from the catalog, sending is blocked with a message naming it until you pick another one.
The gateway's resolved_workflow for the last run start is shown in Settings → About.
Executes one turn in the terminal through the same workflow choice as the tray. Tool approvals are asked
interactively. The turn honors the same local overrides as the tray: chat model pin, media pins,
reasoning effort, MTP depth and reply streaming from preferences.json — reasoning and MTP only
when the chat model's capability card allows them, as in the tray; the workspaces come from the
gateway (the chat's subset, else your account default, else the gateway policy).
--stream on|off overrides the app's Stream replies switch (on unless switched off in
Settings) for this turn. The run always says _runtime.stream explicitly: true only to a gateway
that advertises live replies, otherwise false and, for an explicit on, a bracketed line on
stderr says it was not sent. While the reply
streams, its text is written to stderr as it arrives (the model's reasoning is not printed), a
line in brackets says when a step could not stream or its live text was discarded, and the final
answer is printed once on stdout — so assistant run --prompt … > answer.txt captures only the
answer. Without a saved sign-in or
--gateway-token, a gateway that requires sign-in answers 401; the command then prints how to
sign in (open the Assistant from the gateway console, connect once in Settings → Connection, or
pass --gateway-token) and exits with status 2.
ABSTRACTGATEWAY_URL/ABSTRACTFLOW_GATEWAY_URL— legacy alias of--gateway-url(see Which gateway the app connects to)ABSTRACTGATEWAY_AUTH_TOKEN/ABSTRACTFLOW_GATEWAY_AUTH_TOKEN— bearer tokenABSTRACTASSISTANT_GATEWAY_TTS_MODEL,ABSTRACTASSISTANT_GATEWAY_TTS_PROVIDER,ABSTRACTASSISTANT_GATEWAY_TTS_VOICE,ABSTRACTASSISTANT_GATEWAY_STT_MODEL,ABSTRACTASSISTANT_GATEWAY_STT_PROVIDER— optional speech engine pins for environments without the Settings window
All under ~/.abstractassistant/:
preferences.json— local overrides and UI preferences (reference in settings.md)gateway_connection.json— gateway URL, sign-in mode, token or session statesession_cache.json,sessions/<id>/session.json— the rebuildable session cache: active session, local labels, fetched titles, the last gateway list, cached transcripts, last run id, granted workspace root (architecture.md)automations_notified.json— which automation results, failures and waits were already shown as a tray notification, so a relaunch does not repeat them (automations.md). Deleting it can repeat notifications for items still unseen on the gateway; nothing else is lost.downloads/,gateway_audio/— downloaded artifacts and cached speech audio
Read, never written: ~/.abstractframework/gateway.json, the local gateway pointer file (see
Which gateway the app connects to).
Each run carries only what the user chose locally; blank values are omitted so the gateway default applies:
| Pin | Source |
|---|---|
provider, model, base_url and _runtime.provider/model/base_url |
chat model override |
_runtime.thinking |
reasoning effort |
_runtime.speculation |
MTP depth: false for Off, {"mode": "native_mtp", "num_draft_tokens": N, "require_acceleration": true} for a depth; omitted to follow the gateway default, and omitted when the chat model's capability card does not say it supports MTP at that depth (or cannot be read) |
_runtime.stream |
Stream replies, ALWAYS sent: true when the switch is on and the cached /discovery/capabilities answer advertises streaming.deltas: true, false otherwise (the gateway's streaming.default never decides for this app) |
workspace_root |
the chat's private workspace, once the gateway gave it on the chat's first run (blank before); which other workspaces the run may use is resolved by the gateway at run start — the chat's subset (/sessions/{id}/workspaces) > the account default (/workspace/policy/me) > the gateway policy — never sent per run |
image_provider/image_model, image_edit_*, image_upscale_*, video_*, image_to_video_*, music_*, sound_output |
media route overrides |
_runtime.allowed_tools, _runtime.tool_policy |
per-tool modes (Off / Ask / Auto) |
system |
the workflow's base prompt plus an addendum (voice conversation) when one applies |
The run stream (/api/gateway/runs/{run_id}/ledger/stream) carries, next to the durable step
events, two live events with no id: line: llm.delta
(call_id, seq, text, channel = content | reasoning, snapshot, truncated, run_id,
root_run_id, parent_run_id, node_id) and llm.delta_end (call_id, seq, reason =
completed | failed | cancelled | unavailable, detail; cancelled with detail: "reinvoked"
means the call is being run again under the call id <step_id>:reinvoke). The app resumes the stream only
from the cursor of durable step events, so live events never move it. On every (re)connect it drops
its live text and applies the snapshots the gateway re-sends; it ignores deltas for a call whose
durable llm_call record it already holds, and never shows live text again once the final answer
is on screen. A malformed live frame is skipped (reported once per connection) and never ends
the stream. Whether the gateway offers live replies is read from
/discovery/capabilities → streaming.deltas.
-
Replay view (AbstractGateway 0.17.0+; detected from
/openapi.jsonpaths, once per client):GET /api/gateway/runs/{run_id}/view(If-None-Matchwith the cursor ETag -> 304) andGET /api/gateway/runs/{run_id}/view/stream(SSEview,delta,done; reconnects sendLast-Event-ID). The client copy is kept withabstractruntime.replay.apply_delta; a delta that is not newer than the copy is dropped. Python:GatewayClient.supports_replay_view(),get_run_view(),stream_run_view();abstractassistant.gateway.replay_view(follow_run,ViewFold,ViewProjector,LiveText). Without the routes the app folds the ledger routes below with AbstractRuntime's reducer and the run status line says so. A failed discovery is an error, not the older path. -
Sign-in and identity:
/api/gateway/apps/desktop-handover(console hand-over, loopback),/api/gateway/session/login,/api/gateway/session/logout,/api/gateway/about(version rows for Settings → About;/api/gateway/discovery/capabilitiesis read when it is absent) -
Apps:
GET /api/gateway/apps(Settings → Workflow shows Open in AbstractFlow when theflowrow is installed and mounted at/apps/flow/) andPOST /api/gateway/apps/flow/open(path/?bundle=<id>&version=<version>&flow=<flow>,origin= the gateway URL; the answer'sopen_urlis opened in the browser) -
Workflow:
GET /api/gateway/bundles?executable_for=abstractassistant.agent.v1(the Settings → Workflow list: only the assistant workflows the signed-in person may run, each withownerandshipped; a gateway that ignores the parameter is reported, not listed),/api/gateway/visualflows,/api/gateway/visualflows/{flow_id}/publish,/api/gateway/admin/workflow-catalog/promote -
Sessions:
GET /api/gateway/runs?limit=200&offset=N&root_only=true&include_ledger_len=falsepages (the session list, 100 sessions at a time;&session_kind=chat,discussionis added when the gateway listssession_kindamongruns.list.filtersin its capabilities); titles come from a session's first run'sinput_dataand transcripts from its latest run'shistory_bundle(architecture.md) -
Conversation reads:
GET/POST /api/gateway/me/session-reads(AbstractGateway 0.19.0 or later; immutable account-scoped run revisions; filtered GETs and POST batches contain at most 100 turns; on an older gateway the cards show no unread marks). The native client exposessession_readsandmark_session_reads.archive_sessioncallsPOST /api/gateway/sessions/{session_id}/archive; the response must confirm that same session. -
Runs:
/api/gateway/runs/start,/api/gateway/runs/{run_id},/api/gateway/runs/{run_id}/input_data,/api/gateway/runs/{run_id}/history_bundle(the session transcript seed),/api/gateway/runs/{run_id}/ledger(older gateways only: folded here),/api/gateway/runs/{run_id}/ledger/stream(live reply frames only),/api/gateway/commands(pause, resume, cancel, inject_guidance, resume of waits) -
Discovery and configuration (read):
/api/gateway/me,/api/gateway/discovery/capabilities,/api/gateway/discovery/providers,/api/gateway/discovery/providers/{provider}/models,/api/gateway/discovery/models/capabilities,/api/gateway/discovery/tools,/api/gateway/config/capability-defaults,/api/gateway/workspace/policy,/api/gateway/workspace/policy/self -
Speech and media catalogs:
/api/gateway/audio/speech/models,/api/gateway/audio/transcriptions/models,/api/gateway/voice/voices,/api/gateway/voice/defaults(the default engines Settings → Voice shows),/api/gateway/vision/provider_models,/api/gateway/vision/adapters,/api/gateway/audio/music/providers,/api/gateway/audio/music/models -
Voice library (AbstractGateway 0.16.0):
/api/gateway/voice/languages,/api/gateway/voice/clones(list withlanguage; create from a recording, multipart),/api/gateway/voice/clones/{voice_id}(rename, delete),/api/gateway/voice/design(a voice from a description): the desktop's Create a voice dialog and the grouped Voice list (your voices, engine voices, shared voices);voice/clonesis sent the route's provider when it is a cloning engine, and nolanguagewhen the recording's language is left to detection -
Speech execution: run-scoped
voice/tts,voice/tts/streamandaudio/transcriberoutes; attachments:/api/gateway/attachments/upload -
Artifacts:
/api/gateway/runs/{run_id}/artifacts, artifact metadata and content download -
Run workspace files:
GET /api/gateway/runs/{run_id}/workspace(where the run's files are and whether this client is on that machine) andGET /api/gateway/runs/{run_id}/workspace/content?path=(a copy of a picture an answer names, or of a file opened from the files view, when the client is on another machine) -
Your event stream (automations.md):
GET /api/gateway/me/events?after=<sequence>&limit=1000(the pages read at start and after a reconnect),GET /api/gateway/me/events/stream?heartbeat_s=15(the live tail, resumed with theLast-Event-IDheader),POST /api/gateway/me/events/notifications({"channel": "assistant", "outcome": "sent", "record_key", "device"}after each notification the app shows). A gateway that answers 404 has no stream: the app reads the automation list on its timers instead. -
Automations (automations.md), used only when the capabilities advertise
contracts.common.automations.available: true:GET /api/gateway/automations(every page, followingnext_cursor) andPOST /api/gateway/automations(create, with arequest_id; the automation window sendstitle,target({"flow_id": "@default", "interface": "abstractassistant.agent.v1"}for Gateway default, elsebundle_ref+flow_id) with the task asinput_data.prompt, aschedule@2trigger ({"kind": "every", "every"}with optionalstart_at;{"kind": "once", "at": "YYYY-MM-DDTHH:MM"};{"kind": "daily"|"weekly"|"monthly", "at": "HH:MM", "days"?, "day"?}; optionalcountanduntil; notime_zone, which the gateway fills from your account) or anemail.received@1trigger,context.mode,policy.tool_approval=auto|ask, and, when chosen,notify: {"channels": ["console", "email"]}andpolicy.email_allowed_recipients)POST /api/gateway/automations/schedule-preview({trigger}; nothing stored): the window's line for every schedule kind, Repeat included (first_run_sentence), and the time zone (time_zone, shown for Daily, Weekly, Monthly and Once). The list and the automation view show every schedule row's (schedule@1and@2) servedschedule_rule_textverbatim (a missing one reads—;AutomationsClientpasses every summary throughserved_summary, so a gateway older than round 16, which serves onlynext_fire_at, still lists: next run = thatnext_fire_at, in UTC),next_run_atandnext_run_local(cut toYYYY-MM-DD HH:MM <time_zone>)GET /api/gateway/automations/{id}andPATCH /api/gateway/automations/{id}(edit: the changed fields amongtitle,trigger,context,notifyandpolicy.email_allowed_recipients, withexpected_revisionandcommand_id; the Assistant never sendstargetor the rest ofpolicy, which only a directPATCHchanges)POST …/{id}/commands(automation.pause,automation.resume,automation.run_now,automation.stop_current,automation.archive)GET …/{id}/occurrences(runs, newest first),GET …/{id}/attention(unseen items),POST …/{id}/seen(the last displayed item's cursor),POST …/{id}/discuss(occurrence_index,prompt)GET /api/gateway/trigger-sources(the automation window requiresscheduleversion 1; it offers When an email arrives only whenemail.receivedversion 1 is listed)GET /api/gateway/me/email(whether your email account is usable; AbstractGateway 0.8.0+)GET /api/gateway/triggers/connectionsandGET /api/gateway/me/email/accounts, read only when a row's trigger is a feed, a webhook or a mailbox and the row has notrigger_text: the connection's label and the mailbox's address name the trigger ("When Python news has a new item", "When mail arrives in you@example.org")
Every refusal is read from the
detail.reason_codeenvelope. A waiting run of an automation is answered with theresumecommand on/api/gateway/commands, whose payload follows the wait's kind:{"response": text}forask_user,{"approved": true|false}fortool_approval,{"payload": <JSON>}forevent.
The Python modules (gateway client, controller, session stores, voice manager, workers, UI) are implementation surfaces for the desktop app, not a committed public embedding API. To automate the assistant, use the CLI, or AbstractGateway directly for durable workflow execution.