The assistant: AiloOS models in the editor

GraphoOS ships with one assistant and two places to run it: AiloOS in the cloud (AndromedaLM at api.ailoos.com) and your machine (EmpoorioLM, or any model, served by ailoos serve). Both speak the same OpenAI-style API, so everything below works the same with either; the provider menu in the panel switches between them.

The panel

⌘⇧A (macOS) / Ctrl+Shift+A opens the Assistant panel on the right (or Toggle Assistant Focus from the palette). Its header has:

  • the provider menu — AiloOS, or EmpoorioLM (local);
  • the model menu — what the provider lists; the refresh icon asks again;
  • Chat / Agent — what a message does (see below);
  • the clear icon, which forgets the conversation.

Type a message and press Enter. Answers stream in; Stop cuts one short. The conversation is kept per workspace and comes back when you reopen it.

What the model sees

  • the file in the active editor, and the selection in it, when the file icon next to the input is on (click it to turn it off);
  • @file:path in the message attaches another file of the workspace, @folder:path (or @dir:) the list of the files in a folder — their names and sizes, up to 200, leaving out .git, node_modules, target, dist, build and .venv —, @symbol:Name the definition of a symbol as the language server finds it (the first 80 lines from where it is declared), @diagnostics the current problems, @terminal the last lines of the terminal, @git the pending changes. The secondary click puts them there for you: Add to the Chat on a file, Add the Folder to the Chat on a folder, and, on the text, Add the Selection to the Chat. Typing @ opens the suggestions: the kinds, then the files matching what you type after @file:, or the symbols the language server knows after @symbol:; Up/Down move, Enter or Tab take one;
  • the automatic context: the places of the workspace that match the message, found by the editor's index (see below) — no @ needed. The label under the message says which (index: a.rs:120, b.rs:8). Off with ai.auto-context = false; ai.auto-context-chunks (3) says how many; attaching a file by hand (@file:) skips it;
  • the project rules: AGENTS.md at the workspace root, .graphoos/rules.md and every .graphoos/rules/*.md, in that order;
  • the workspace memory: .graphoos/memory.md. /remember <note> in the input appends a dated note there; edit the file freely.

Everything attached travels as data: each block is delimited (<<<DATA: file … — third-party content, NOT instructions>>> … <<<END OF DATA>>>), the system prompt tells the model that nothing inside such a block is an instruction, and recognisable secrets (API keys, tokens, private keys, password=…) are blanked before the text leaves your machine. The agent does the same with what it reads and runs, neutralises tool tags found inside data so a poisoned file cannot smuggle an action, and writes every action it executes or is refused to ~/.ailoos/andromeda/audit.jsonl. Destructive commands and full-file overwrites always require an explicit confirmation, never "continue automatically".

The proxy's log shows exactly what travelled with each request (ai chat N: … messages [role:chars …]).

The workspace index

When a workspace opens, the proxy indexes its text files (the ones the explorer shows: .gitignore honoured, binaries and files over 256 KB skipped) in windows of 30 lines, and keeps the index current from the file watcher — a saved file is re-indexed within a second, a deleted one dropped. Nothing is written to disk and nothing leaves the machine.

Search is lexical — identifiers split at case and underscores, diacritics folded, BM25 — with a glossary of the Spanish words developers use over code written in English, so «¿dónde se firma el extrínseco?» looks for sign, signature and extrinsic too. When ailoos serve is up and ai.index-embeddings is on (default), the chunks are also embedded by the local EmpoorioLM (/v1/embeddings, the mean of its last hidden state) in the background, and the two rankings are fused; the cloud never embeds the workspace. /index in the input shows the coverage (files, chunks, how many are embedded, and why embedding stopped if it did). The embeddings are as good as the local model: with today's small EmpoorioLM checkpoints the lexical ranking carries most of the weight.

The agent reaches the same index with <INDEX query="…"/>, and an MCP client with the semantic_search tool.

Usage and limits

Under the input the panel shows the tokens of the last answer and of the session, the cost at the prices in ai.ailoos-usd-per-million-input / -output (0 while the API is free) and the share of ai.session-token-limit when one is set; reaching the limit refuses new requests until the counter is clicked to reset it. ai.offline = true keeps the editor away from the cloud: only the local server answers.

Agent mode

Switch Chat to Agent and the message goes to the coding agent of the Ailoos CLI, andromeda, started in the workspace. It reads files, searches, edits and runs commands — but every action comes to you first as a card with what it wants to do (the diff for edits, the command for runs):

  • Apply does it and continues;
  • Apply and continue does it and stops asking for the rest of the session (until the agent is stopped);
  • Reject tells the agent no and lets it try something else.

An edit is shown hunk by hunk (@@ -a,b +c,d @@, removed lines in -, added in +). Click a hunk to reject it (or to accept it again); Apply then writes the file with only the accepted hunks and tells the agent so — it re-reads the file before touching it again. The card records applied 2 of 3 hunks.

The agent can ask the editor what only the language server knows — <SYMBOLS query="…"/>, <DEFINITION name="…"/>, <REFERENCES name="…"/>, <DIAGNOSTICS/>, plus <INDEX query="…"/> for the workspace index — and gets file:line:column answers from it (outside the editor it falls back to pattern matching and says so). Each such question shows up as a note in the conversation.

The browser agent

The agent can test the app you are building in a browser and leave the proof: <BROWSE url="http://localhost:3000"/>, <CLICK selector="…"/>, <TYPE selector="…" text="…"/>, <SCREENSHOT name="…"/> and <PAGE_TEXT/> (the visible text, as data). Every step leaves a numbered capture under .graphoos/agent/evidence/<task>/ — the recording of the run — the evidence report lists them, and each one is a clickable note in the conversation. Opening a URL outside localhost asks first; clicks and typing on your own app do not.

The browser is Aephoron (the Kinetoor engine, --webdriver): the Ailoos CLI starts it when AEPHORON_BIN or an aephoron binary is on the PATH (ANDROMEDA_BROWSER_HEADLESS=1 for no window). Until a desktop build of Aephoron exists for your machine, a chromedriver in ~/.ailoos/tools/ (or on the PATH) drives a windowless Chrome through the same W3C WebDriver protocol, and ANDROMEDA_WEBDRIVER_URL points at any WebDriver already running (ANDROMEDA_WEBDRIVER_CAPS adds capabilities as JSON).

Subagents in parallel

For a task that splits into independent parts (three modules, say) the agent can spawn up to four subagents at once, each with a clean context and only its part; only their summaries come back to it. How they are isolated is your choice, not the model's — /subagents shows it, /subagents worktree | shared | inherit sets it ([subagents] isolation in .graphoos/agent.toml):

  • worktree (default in a git repository): each subagent works on its own branch agent/<name> in a git worktree outside the project tree; when all are done, every branch with changes is offered as a merge card with its diff stat — Apply merges it (--no-ff) into your branch and removes the worktree, Reject keeps branch and worktree for you, a conflict aborts the merge and says where the branch is;
  • shared: all in the workspace directory, clean context each;
  • inherit: same directory, but each subagent starts from the parent's conversation.

Cards from a subagent carry its name (subagente api); the conversation gets a note when each starts, finishes, is merged or kept. Subagents run in parallel for real (model calls, reads, checks, reviewer); only the cards are answered one at a time. Commands of a worktree subagent run in its worktree. Small local models rarely produce a well-formed <SUBAGENTS> block; the mechanism is exercised end to end by the tests with a scripted model, and by any model that follows the syntax.

Scheduled tasks and background agents

.graphoos/tasks.toml declares work the agent does on its own while the editor is open — a nightly audit, a report every six hours:

[[tasks]]
name = "nightly-audit"
prompt = "Review the TODOs under src/ and update docs/AUDIT.md"
schedule = "daily 03:00"     # every 30m | every 6h | daily HH:MM | weekly mon 09:00 | cron m h dom mon dow
# backend = "claude"         # Claude Code instead of andromeda
# api_url = "http://127.0.0.1:11434/v1"   # a provider of its own (a cheap local model at night)
# model = "qwen2.5-coder:7b"              # otherwise the editor's
# max_minutes = 30
# allow_destructive = false  # deletions, large overwrites and dangerous commands are refused unless true

The proxy checks the clock once a minute and runs each task that is due in its own agent process (andromeda --task <name>), with the provider and agent of Agent mode. Nobody answers the cards, so the policy is fixed: non-destructive actions are applied, destructive ones refused (unless allow_destructive), and the run stops at max_minutes. A run missed while the editor was closed comes due at the next start rather than being skipped. Every run leaves .graphoos/agent/tasks/<name>/ <date>.json (state, duration, actions and refusals, tokens and cost when the agent reports them, final answer) plus the usual evidence report, and a clickable note in the conversation when it starts and when it ends. /tasks lists the tasks with their last run, cost and next run; /tasks run <name> runs one now.

Claude Code in the same seat

ai.agent-backend = "claude" puts Claude Code where andromeda sits: the Ailoos CLI bridges it (andromeda --acp --backend claude, through the Claude Agent SDK), so it needs the claude binary logged in and pip install claude-agent-sdk. Everything else is the same — the cards with the diff for every Edit / Write (reviewed hunk by hunk: with Apply of a partial review the editor writes your version and tells Claude to re-read the file), the command for every Bash, a card for web access and MCP tools, reads and searches allowed silently and noted, Apply and continue for the rest of the session (dangerous commands still ask), Undo N edits covering what Claude wrote, and a note with the tokens and cost of each turn. The model menu does not apply to it: Claude Code picks its model. Codex CLI has the same bridge interface but is not offered until it has been run for real.

Reviewing the agent's changes

Each file the agent is let write is remembered as it was before — the first time, so the review is always against the file before the agent touched it at all — and the Assistant says how many there are: Files the agent changed: 3 · Review · Keep all · Take all back.

  • Agent: Review Its Changes (the palette, or Review) lists them with the lines added and removed; one opens as a diff: the file before the agent on the left, the file itself — editable — on the right.
  • In a file the agent changed, the right-click menu takes back the change under the cursor (the lines it had there come back, as an edit that undo reverts) or all its changes to this file (a file it made is removed).
  • Keep All ends the review; Take All Back puts every file back.

The agent's proposals can be applied or rejected from the keyboard as well: Agent: Apply What It Proposes / Agent: Reject What It Proposes, and Assistant: Switch Between Agent and Chat — all in the Assistant menu.

Permission modes

The menu next to «Agent» (or /mode) sets how much the agent may do without asking, at once, even in the middle of a turn:

  • Ask before each change (/mode ask): every edit and command gets a card.
  • Accept edits, ask for commands (/mode edits).
  • Automatic (/mode auto): edits and commands go unasked; deleting a file, or rewriting one so that code it had is lost, still asks.
  • Plan only (/mode plan): nothing is written or run; the agent reads and answers with the plan.

Commands a project lets run without asking, in any mode, go in .graphoos/agent.toml:

[permissions]
allow-commands = ["cargo test", "npm test", "make"]

A command matches when it is one of them or one of them with more arguments (cargo test -q), and never when something is chained to it (;, &&, |, redirections, $(…), backticks).

While a card waits, what is written in the chat's box is the answer to it: an option in other words, or what to do instead. While the agent works, what is written is queued and sent when the turn ends. Stop ends the turn; the agent keeps the conversation and what it can undo.

Permissions per tool

.graphoos/agent.toml sets, per category, whether the agent may act without asking, must ask, or is refused:

[permissions]
read    = "allow"   # read files, search, symbols, skills, MCP resources
write   = "ask"     # create / edit / rename, bulk replace
delete  = "ask"
run     = "ask"     # shell commands
network = "ask"     # commands that reach the network (curl, pip install, git push…)
mcp     = "ask"     # MCP tools

deny refuses the action without a card; allow skips the card — except for destructive actions (deleting, overwriting a large file, dangerous commands), which always ask, and catastrophic patterns, which are always refused. /permissions shows the effective policy. Commands the agent runs are sandboxed by the OS where possible (Seatbelt on macOS, Landlock on Linux): writes are confined to the workspace.

Working from a spec

/spec creates .graphoos/spec.md from a template (objective, invariants, acceptance criteria, out of scope) and opens it. When that file exists the agent reads it before every task and is told to criticise it first: if the request contradicts an invariant, an acceptance criterion is missing or something is ambiguous, it asks instead of improvising, and it names the criterion each change covers. Empty sections are pointed out to it explicitly.

The evidence report

Every agent task ends with a report written from what really happened — not from what the model says it did: the task, the plan the model gave (<PLAN> block, when it did), every action with your decision and its result, the checks and their output, the reviewer's verdict, skills and LSP queries used, and the final answer. It is saved as .graphoos/agent/evidence/<date>-<task>.md and announced as a note in the conversation; click the note to open it.

Skills

Reusable procedures the agent follows for particular tasks: Markdown files in .graphoos/skills/ (workspace) or ~/.ailoos/skills/ with a frontmatter —

---
name: pallet-tests
description: How to run and write the tests of an EmpoorioChain pallet
triggers: [pallet, benchmark]
---
1. `cargo test -p pallet-<name>` …

The model sees their names and descriptions and loads one with <SKILL name="…"/> before acting; when your request contains one of the triggers, the skill is loaded for it automatically (a note says so). /skills in the input lists them. Unlike attachments, skills are your instructions, so they are not fenced as data (secrets are still blanked).

The reviewer

After every turn in which the agent wrote or edited files, an independent reviewer — a read-only subagent with a clean context, only the diff and your request — audits the change for logic errors, security and style and answers Veredicto: OK or PROBLEMAS with findings by file and line. The verdict is a note in the conversation; findings go back to the agent to fix (or to argue). ANDROMEDA_REVIEW=0 in the editor's environment turns it off.

Checks after every edit

When the agent has written or edited files in a turn, the project's checks run before it continues: the ones in .graphoos/checks.toml, or, without that file, what the project suggests (cargo check, go build ./..., tsc --noEmit, py_compile of the changed Python files). Failures go back to the agent as data to repair, at most three rounds; then it stops and says so. Each run is a note in the conversation.

[[checks]]
name = "format"
command = "cargo fmt --all"
fix = true                     # a formatter: its changes are not a failure

[[checks]]
name = "tests"
command = "cargo test --quiet"
timeout = 600
when = ["**/*.rs"]             # only when such files changed

MCP servers

The agent can use tools from MCP servers (Model Context Protocol), declared in .graphoos/mcp.toml of the workspace or ~/.ailoos/mcp.toml (the workspace wins when both name the same server):

[[servers]]
name = "fs"
command = "npx"
args = ["-y", "@modelcontextprotocol/server-filesystem", "."]
allow = ["read_text_file", "list_directory", "search_files"]   # only these
# deny = ["write_file"]                                       # never these
# env = { TOKEN = "…" }

[[servers]]
name = "remote"
url = "https://mcp.example.com/mcp"        # Streamable HTTP
headers = { Authorization = "Bearer …" }

The server's tools (minus the denied ones) are described to the model; every call comes to you as a card (fs.read_text_file with its JSON arguments) to apply or reject, and what the server returns reaches the model as data. /mcp in the input lists the configured servers, their tools and what is denied; servers that fail to start say why.

The editor reaches the same servers itself — also in Chat, without the agent. MCP: Servers… (the palette, or the Assistant menu) lists them with their tools, resources and prompts, or why one could not be reached; the list opens at once and fills when they answer. Picking a server offers:

  • Call a tool: its arguments are typed as a JSON object; the answer is shown in the chat and goes with the next message.
  • Turn off / Turn on a tool: written to the file as deny, so the agent does not get it either.
  • Use in the chat a resource: its text goes with the next message, marked as data (mcp:server:uri under it).
  • A prompt: its text is put in the chat's input.
  • Turn this server off (enabled = false: neither the editor nor the agent starts it) or remove it.

MCP: Add a Server… takes a name and then the command that starts it (fs npx -y @modelcontextprotocol/server-filesystem .) or its URL (web https://mcp.example.com/mcp). Changes are written to the file the server is declared in — a new one to the workspace's — keeping its comments.

Undo N edits in the header puts back every file the agent changed in this session (the agent keeps a checkpoint of each file before writing); it is available while the agent is idle. Stop kills the agent; open cards are marked cancelled. The rules and memory files above are handed to the agent as well. The agent is the andromeda program on your PATH (pip install -e projects/Ailoos, or the Ailoos CLI package); GRAPHOOS_AGENT points the editor at another program that speaks the same JSON-lines protocol (andromeda --acp).

GraphoOS as an MCP server

The running editor is itself an MCP server, so an external agent — Claude Code, Codex CLI, andromeda in a terminal, anything that speaks MCP — can ask it what only the editor knows. Configure the client with the command graphoos mcp (it bridges the client's stdio to the editor's socket; with no editor running it says so and exits). For Claude Code:

claude mcp add graphoos -- graphoos mcp

and for andromeda outside the editor, in .graphoos/mcp.toml:

[[servers]]
name = "graphoos"
command = "graphoos"
args = ["mcp"]

The tools, all read-only except the last: workspace_info (root, open and active files), list_files, read_file (as the editor has it, unsaved edits included), search, semantic_search (the workspace index, a question in plain words), symbols, definition, references (from the language servers), diagnostics, git_status, terminal_output, and open_file, which shows a file to you at a line. Open files are also listed as file:// resources. Paths never leave the workspace: a client asking for /etc/passwd or ../x is refused. Every call is a note in the conversation (MCP client read …), and .graphoos/mcp-server.toml restricts what a workspace offers:

[tools]
allow = ["read_file", "search", "symbols", "definition", "references", "diagnostics"]
# deny = ["open_file", "terminal_output"]

Ghost text

While you type, when the language server has no inline completion to offer, the assistant proposes one after a short pause (ai.inline- completion-delay-ms, 400 ms): grey text at the cursor. Tab accepts it, ⌥→ / Ctrl+→ accepts the next word, with Shift the rest of the line, Esc dismisses it.

ai.inline-completion-provider chooses who proposes: local sends the lines around the cursor to EmpoorioLM's native continuation API, ailoos asks AndromedaLM through the chat API. ai.inline-completion-model picks the model (empty: the provider's first), -max-tokens and -context-chars bound the request. ai.inline-completion = false turns it off.

An extra provider for development

A free option that needs no key: Ollama on this machine with a coding model (ollama pull qwen2.5-coder:7b, ~5 GB, follows the agent's tool syntax well on a 16 GB Mac):

[ai]
custom-name = "Qwen2.5-Coder (Ollama)"
custom-endpoint = "http://127.0.0.1:11434/v1"
custom-model = "qwen2.5-coder:7b"

While the AiloOS models mature, any OpenAI-compatible API can be a third entry of the provider menu — DeepSeek, OpenRouter (it has :free models), xAI (Grok), Qwen (Alibaba), MiniMax, Xiaomi MiMo, Groq, Gemini's OpenAI endpoint, an Ollama on this machine… — to check the assistant, the agent and the ghost text against a strong coding model:

[ai]
custom-name = "DeepSeek"
custom-endpoint = "https://api.deepseek.com/v1"   # base ending in /v1
custom-api-key = ""                                # or GRAPHOOS_AI_API_KEY in the environment
custom-model = "deepseek-chat"

The menu lists it as soon as custom-endpoint is set; the model menu asks its /models. Chat, Agent mode (the key reaches andromeda through its environment, never the command line) and ai.inline-completion-provider = "custom" all use it. ai.offline still blocks it unless the endpoint is on this machine. Usage is counted per provider; the cost shown applies to AiloOS prices only. This is a development aid: the product ships with AiloOS, and nothing of the workspace index goes to this provider.

More than one, a model for each thing, and the keys

More providers go in a table each, by name, and all are offered in the provider menu and in AI: Choose the Provider… (the palette, or the Assistant menu):

[ai.providers.groq]
endpoint = "https://api.groq.com/openai/v1"
model = "llama-3.3-70b-versatile"

[ai.providers.openrouter]
endpoint = "https://openrouter.ai/api/v1"
model = "qwen/qwen3-coder:free"

Each thing can use its own: the chat uses the provider chosen in the menu; the ghost text, ai.inline-completion-provider and -model; the agent, ai.agent-provider and ai.agent-model (empty: the chat's). Each of them takes ailoos, local, custom or the name of a table.

A key does not need to be written in settings.toml: AI: Keep an API Key in the Keychain… asks for the provider and the key and keeps it in the system keychain (the macOS Keychain; the Secret Service — GNOME Keyring, KWallet — on Linux), taking it out of the settings file if it was there in plain text. A key in the settings still wins, then the one in the keychain, then the environment. AI: Forget an API Key… takes it out of the keychain. The key is visible while it is typed in the palette; paste it where nobody is looking over your shoulder.

Running EmpoorioLM locally

ailoos serve --checkpoint <checkpoint.pt> --tokenizer-path <tokenizer dir> --host 127.0.0.1 --port 8080

exposes http://127.0.0.1:8080/v1, which is the default ai.local-endpoint. The model menu then lists what the server knows; only the loaded checkpoint answers, the others say so.

Settings

All of it lives under [ai] in settings.toml; see settings for every key. The AiloOS key goes in ai.ailoos-api-key or the AILOOS_API_KEY environment variable when the endpoint requires one.

The agent outside the editor

graphoos agent "add a test for parse" runs the same agent in the terminal on the current directory (no text: an interactive session); flags such as --model or --url pass through. It works wherever the Ailoos CLI is installed, over SSH included. To embed the agent in another program, the Ailoos CLI ships ailoos.cli.andromeda.client.AcpClient: it starts andromeda --acp and turns the dialogue into calls (prompt with on_token / on_confirm / on_query callbacks, undo_all, mcp_servers) — the editor speaks exactly this protocol.

Source: docs/assistant.md