A ANDROMEDADocumentation › Guides

Intelligence

Memory across conversations, delegation to subagents, plans the agent must actually finish, external tools over MCP, and work that runs in the background.

Memory

Memory follows Mastra's shape: one Memory object with pluggable storage, vector store and embedder. Pass a resource (who the memory belongs to) and a thread (one conversation) with each call.

import { Agent, Memory } from "./src/libs/index.ts";

const agent = new Agent({ name: "assistant", instructions: "Be helpful.", memory: new Memory() });
await agent.generate("Remember that releases happen on Fridays.", {
  memory: { thread: "planning", resource: "project-acme" },
});

The three memory types

  • Message history. Every message is saved, and the last lastMessages (default 40) are replayed. The window moves in half-size steps so the configured model engine can reuse its prompt cache.
  • Working memory. A Markdown document shown on every turn, which the agent rewrites with update_working_memory. With scope: "resource" (default) it follows you into every thread.
  • Semantic recall. Messages are embedded; each prompt pulls in the topK most similar older messages, plus neighbours. If the embedding model is missing, recall is skipped with a warning.
import { LocalStore, LocalVector, Memory } from "./src/libs/index.ts";

const memory = new Memory({
  storage: new LocalStore(".memory"),
  vector: new LocalVector(".memory"),
  embedder: "mxbai-embed-large:335m",
  options: {
    lastMessages: 20,
    semanticRecall: { topK: 3, messageRange: 2, scope: "resource" },
    workingMemory: { enabled: true, scope: "resource" },
  },
});

Shortcuts work too: { options: { lastMessages: 20, semanticRecall: true } }. Other options: goals (add_goal/complete_goal tools), generateTitle and readOnly.

In the CLI

bun run main.ts --memory local --resource-id brian --thread-id andromeda-development
memory:
  provider: local            # none | local | qdrant
  last_messages: 40
  working_scope: resource
  semantic_recall:
    enabled: true
    embed_model: mxbai-embed-large:335m
    top_k: 5

With Docker Model Runner, use a pulled embedding model such as ai/nomic-embed-text-v2-moe. It produces 768-dimensional vectors; use a separate collection such as andromeda_memory_dmr or re-embed your existing collection when changing models.

/memory shows working memory, /resume lists saved conversations, /forget deletes the current one, and /reindex rebuilds recall vectors.

Qdrant

QdrantMemory keeps history, recall and ingested documents in one Qdrant collection. With Qdrant, relevant memories are added to each message automatically, so search_memory is loaded only when you refer to something not shown.

memory:
  provider: qdrant
  qdrant:
    url: http://192.168.3.230:6333          # https:// enables TLS
    collection: andromeda_memory
    fallbacks: [http://localhost:6333, ":memory:"]
    probe_interval: 30

Keep QDRANT_API_KEY in .env or at memory.qdrant.api_key in andromeda.yml. API keys are refused over plain http:// unless you opt in. With fallbacks, Andromeda keeps working through a Qdrant outage and replays writes from .andromeda/qdrant-outbox.json into the primary when it returns.

Text is chunked along its headings, and re-ingesting a source replaces its old chunks. Extract documents first (for example with fetch_url, which reads PDFs with pdftotext):

await memory.ingestText("project-acme", handbookMarkdown, { source: "https://example.com/handbook" });

Password-protected PDFs and Office files ask for their password (a masked prompt in the CLI). The password goes straight to decryption and never reaches the model, transcript or memory.

Custom providers

An agent only needs the MemoryProvider interface, so SQL, Redis or any other store works:

interface MemoryProvider {
  buildContext(resourceId: string, threadId: string, prompt: string): Promise<Message[]>;
  loadHistory(resourceId: string, threadId: string): Promise<Message[]>;
  persist(resourceId: string, threadId: string, newMessages: Message[]): Promise<void>;
}

Subagents

Two ways to use more than one agent:

  • AgentRunner: your code decides which agents run, in parallel, with a batch-wide limit.
  • SubagentManager: the model decides. It picks a host-approved profile and hands it a task.
import { Agent, SubagentManager, createBuiltinTools, createReadOnlyTools } from "./src/libs/index.ts";

const manager = new SubagentManager({
  parentModel: "gpt-oss:20b",
  maxConcurrency: 3,
  definitions: [
    { name: "explorer", description: "Read-only repository research",
      systemPrompt: "Investigate independently and cite files.", tools: createReadOnlyTools() },
    { name: "worker", description: "Implementation and verification",
      systemPrompt: "Implement the task and run focused checks.", tools: createBuiltinTools() },
  ],
});
const parent = new Agent({
  name: "lead",
  model: "gpt-oss:20b",
  systemPrompt: ["Lead the task.", manager.promptInstructions].join("\n\n"),
  tools: [...createBuiltinTools(), manager.delegateTool, ...manager.tools],
});

delegate(agent_name, task) starts a subagent, waits and returns its result; small local models use it far more reliably than spawn-and-wait. For parallel work the parent also gets spawn_subagent, wait_for_subagents, subagent_status, follow_up_subagent and cancel_subagent. A parent can narrow a profile's tools but never add to them. Call await manager.close() when the session ends.

The CLI ships an explorer (read-only, with web search) and a worker (coding tools). The agent uses the explorer before changing code it hasn't read, keeping its own context small. In plan mode the explorer is allowed and the worker is refused. ← on an empty prompt and /agents show running subagents.

Planning & verification

Small local models often describe a multi-step job, do part of it and report it finished. Planning gives each run a checklist it must work through, and verification checks that checklist against the tool calls that actually ran.

import { Agent, TaskVerifier } from "./src/libs/index.ts";

const agent = new Agent({
  name: "dev",
  model: "gpt-oss:20b",
  tools: [/* ... */],
  planning: true,                                   // adds update_plan
  planDir: ".andromeda/plans",                        // keep plans as Markdown
  verifier: new TaskVerifier({ model: "gpt-oss:20b", rounds: 2 }),
  maxToolIterations: 25,                            // plans take extra calls
});

The model writes a plan (goal, findings, steps with done_when checks, verification commands), then marks steps off:

Plan (1/3 done):
[x] 1. Create calc.py
[~] 2. Create test_calc.py
[ ] 3. Run the tests
Now do step 2: Create test_calc.py
  • Plan before changing files. The first write_file, edit_file or create_tool call of a run with no plan is sent back, asking for a plan that covers every requirement.
  • No unfinished steps. A reply while steps are still open is sent back with the list.
  • Plans outlive the run. With a planDir, a thread's unfinished plan is picked up again when you say "go ahead". You can edit the Markdown file between runs.

The verifier catches "said, not done": steps marked done with no tool call since, files claimed but never written, failures reported as success, commands written out instead of run, and "it works" with no check after the last edit. An optional small inspector model opens the files a turn made and checks each requirement.

agent:
  planning: true
  verifier:
    model: gpt-oss:20b      # null: model-free checks only
    rounds: 2
    inspector:
      model: granite4:7b-a1b-h

In the CLI, use plan mode to review a plan before it runs; see Permission modes.

MCP servers

Andromeda connects to Model Context Protocol servers over stdio, Streamable HTTP or legacy SSE. Each tool is named <server>_<tool> and goes through the same validation, permissions and logging as built-in tools. By default each server's tools form one on-demand group, keeping prompts small for local models.

Servers come from four places; later ones win on a name clash:

  1. ~/.andromeda/mcp.json: user scope, every workspace.
  2. <workspace>/.mcp.json: project scope, the same format as Claude Code's.
  3. Server files in <workspace>/mcp/: mcp/<name>.json or mcp/<name>/server.json, one server each (the same layout as the bundled servers in andromeda-js).
  4. mcp.servers in andromeda.yml.
{
  "mcpServers": {
    "playwright": {"type": "stdio", "command": "npx", "args": ["-y", "@playwright/mcp@latest"]},
    "postman": {
      "type": "http",
      "url": "https://mcp.postman.com/minimal",
      "headers": {"Authorization": "Bearer ${POSTMAN_API_KEY}"}
    }
  }
}

${VAR} and ${VAR:-default} are expanded from .env or the environment; a server whose variable is unset is skipped with a reason in /mcp. A server's env block can also hold literal values, so its credentials can live in andromeda.yml under mcp.servers.<name>.env.

KeyDefaultMeaning
enabledtruefalse keeps the entry but doesn't connect
deferredtruefalse sends the tools every turn
tools / exclude_toolsallAllow- or deny-list of tool names
prefixserver nameTool-name prefix; "" disables it
call_timeout120Seconds per tool call
max_output_chars20000Longer results are truncated

Tools the server marks read-only always run. In ask mode every other MCP tool needs approval; plan mode refuses them. MCP servers run with your privileges, so only add ones you trust. stdio logs go to .andromeda/mcp/<name>.log. Add servers from the command line with bun run main.ts mcp add.

Talking to other instances

Run Andromeda more than once (two terminals, a terminal and the background service, sessions in different projects) and the instances find each other. Each one announces its id, kind, model and workspace in ~/.andromeda/instances; --instance ID names a session (the default is its conversation id).

The collaboration tool group lets the agent hand work to another instance or ask it something:

  • list_andromeda_instances shows who is running.
  • message_andromeda_instance sends a message and, by default, waits up to two minutes for the answer; * sends an update to everyone without waiting.
  • read_andromeda_messages reads messages an instance hasn't picked up by itself.

The recipient picks a message up as soon as it is idle, answers it in an unattended turn (tool approvals and outbound checks still apply) and the answer goes back automatically. Both terminals show the exchange as [from planner] and [to planner]. Answers are never answered again, and a message relayed through more than three instances is refused, so two instances can't loop.

Plugins

A plugin is one module that adds a feature with no edits to the runtime: tool groups, new trigger kinds, slash commands, a short system-prompt note, SKILL.md folders and MCP servers. Built-ins live in src/app/plugins.ts (pages is one). Your own go in .andromeda/plugins/<name>/ or ~/.andromeda/plugins/<name>/ and load only once named in plugins.enabled, because they run code. /plugin shows what loaded and what failed.

import { definePlugin } from "../libs/plugins/index.ts";

export default definePlugin({
  name: "weather",
  description: "current weather and forecasts",
  setup({ sdk: { tool, z, ToolGroup }, config }) {
    return {
      toolGroups: [new ToolGroup("weather", "current weather for a place", [
        tool({ name: "get_weather", description: "Current weather for a city.",
               parameters: { city: z.string() }, execute: async ({ city }) => "..." }),
      ])],
      readOnlyTools: ["get_weather"],   // everything else goes through the permission gate
      commands: { weather: { hint: "<city>", description: "show the weather", run: (city) => "..." } },
    };
  },
});
plugins:
  enabled: [weather]
  disabled: []            # built-ins to turn off, e.g. [pages]
  config:
    weather: { units: metric }

Prefer toolGroups over always-on tools: the model loads a group with load_tools when it needs it, so its schemas don't cost prompt tokens on every turn. A plugin's triggerSources add new event kinds for create_trigger, alongside the built-in Gmail, calendar and file sources. examples/plugins/rss is a complete example.

Hooks

Shell hooks in .andromeda/hooks.json run your own commands at three points:

EventRunsExit code 2
PreToolUseBefore a tool callBlocks the call
UserPromptSubmitWhen you send a prompt; stdout is added as contextBlocks the prompt
StopWhen a turn finishesn/a

/hooks lists the configured hooks.

Background tasks

BackgroundTaskManager runs opted-in tool calls outside the agent loop, with state in SQLite. It handles concurrency limits, timeouts, retries, cancellation and recovery after a restart.

import { Agent, BackgroundTaskManager, backgroundTool } from "./src/libs/index.ts";
import { z } from "zod";

const research = backgroundTool({
  name: "research",
  description: "Research a topic in depth.",
  parameters: { topic: z.string() },
  execute: async ({ topic }) => runLongResearch(topic),
  timeoutSeconds: 600,
  maxRetries: 1,
});

const manager = new BackgroundTaskManager(".andromeda/background_tasks.db", { onFinished: report });
const agent = new Agent({ name: "researcher", tools: [research], backgroundTaskManager: manager });

A deferred tool returns a task ID immediately and the turn continues; awaited tools are tracked while the turn waits. Only the developer can make a tool eligible. onFinished is called exactly once per deferred result, even across restarts.

In the CLI, video generation runs this way. The status line shows ◐ 1 background task · generate_video 2m 14s, /tasks lists tasks, /tasks cancel <id> stops one, and the agent tells you when a task finishes.

Artifacts

ArtifactStore keeps what the agent extracted or made outside its context window, in <workspace>/.andromeda/artifacts.db.

  • Documents. fetch_url saves each document's full text and shows the model an 8,000-character preview with the artifact id. An unchanged local file is never extracted twice.
  • Made files. Speech, images, videos, screenshots and pages are recorded with what they were made from, so "play the life plan audio again" becomes a lookup instead of a folder search.

The model reads the store with list_artifacts(query, kind) and read_artifact(id, start, find). Delete artifacts.db to clear it.

import { Agent, ArtifactStore, createBuiltinTools } from "./src/libs/index.ts";

const artifactStore = new ArtifactStore(".andromeda/artifacts.db");
const agent = new Agent({ name: "assistant", tools: createBuiltinTools(".", { artifactStore }) });