Harness Engineering

May 21, 2026AI Agents · CLI · Engineering · Terminus

Harness Engineering

How terminus-cli is built: the agent loop, tools, memory, subagents, planning, skills, and the context management behind a useful coding agent.

Harness engineering is a term people throw around a lot right now, so it is worth saying what it actually refers to.

We have been told:

Agent = Model + Tools

The formula is mostly right. An agent is a large language model running in a ReAct loop, where the model does the reasoning and the tools do the acting.

Reason -> Act -> Observe -> Repeat

Everything in an agent other than the model is the harness. How good an agent is depends on the harness at least as much as it depends on the model, which is a change from a couple of years ago, when prompt engineering was the main thing people worked on.

So:

Agent = Model +
Harness
Tools Context Management AGENTS.md Memory Multi-Agent Systems Skills MCP Hooks

A strong model with a weak harness usually loses to an average model with a well-built harness.

01

Terminus-cli

github.com/sidmanale643/terminus-cli

Before terminal coding agents like Claude Code took off around September and October 2025, about 80% of people used Cursor, and I was one of them. I had heard about Claude Code, but I did not pay attention to it until Cursor started to frustrate me.

Any task beyond renaming variables or writing a main function sent Cursor off in the wrong direction. When I looked into why, I found that Cursor relied on semantic search as its default way of searching a codebase.

Claude Code had solved the problem by replacing a large retrieval stack, which has several parts that can fail, including a vector database, a reranker, and an embedding model, with a grep tool.

That is the kind of harness decision this post is about.

Terminus-cli started as an attempt to teach myself how AI agents work. The rest of this article uses it as a worked example of the harness decisions behind useful coding agents.

02

Architecture

The sections below describe the main harness parts of Terminus.

Tools

A coding agent needs a way to interact with the code, which means it has to read, write, and edit files. The core tool set therefore usually starts with file operations like read, create, and edit, along with bash for interacting with the environment.

The basic tools would be:

  • file_reader: lets the agent inspect source code, configs, logs, and docs before making changes.
  • file_creator: useful when the task requires adding new modules, configs, tests, or documentation.
  • file_editor: this is how the agent actually changes existing code instead of only describing what should be done.
  • bash: allows the agent to run commands, inspect the environment, execute scripts, and verify results.
  • grep: useful for searching code across the codebase much faster than semantic search in many cases.
  • glob: helps the agent find files by pattern when it does not know the exact path.
  • web_search: useful when local context is not enough and the agent needs external information.

Always return the errors from tool executions as strings instead of throwing them so the agent knows what went wrong and knows what to improve.

ToolRegistry

The part of the harness responsible for exposing these tools is the ToolRegistry.

In its simplest form, the ToolRegistry is a dictionary with tool names as the keys and function objects as the values.

json
[
  {
    "type": "function",
    "function": {
      "name": "search",
      "description": "Search the web for up-to-date information and return relevant results.",
      "parameters": {
        "type": "object",
        "properties": {
          "query": { "type": "string", "description": "The search query." }
        },
        "required": ["query"],
        "additionalProperties": false
      }
    }
  },
  {
    "type": "function",
    "function": {
      "name": "calc",
      "description": "Evaluate a mathematical expression and return the result.",
      "parameters": {
        "type": "object",
        "properties": {
          "expression": { "type": "string", "description": "The expression to evaluate." }
        },
        "required": ["expression"],
        "additionalProperties": false
      }
    }
  }
]
python
def search():
    logic

def calc():
    logic

tools = {}
tools["search"] = search
tools["calc"] = calc

tool_registry[tool_name](**params)

Tools are registered and executed through the registry. The next question is how the model uses the registry at runtime.

The loop

At the implementation level, the LLM iterates through a ReAct loop.

Diagram showing a user prompt flowing into an LLM, which calls tools and receives tool feedback
The basic agent loop: the model reasons, calls tools, receives feedback, and repeats.

During this loop, it can call tools or decide to end its turn by returning a final response.

python
messages = [system_prompt, user_input]

while not done:
    action = model(messages)

    if action is final:
        done = true
    else:
        result = run_tool(action.name, action.args)
        messages.append(result)

return action.content

We provide the system prompt along with the tool names and descriptions in JSON, and the model only directs the harness on what needs to be run. The harness runs the tool call, aggregates the output, and sends it back to the LLM. The ReAct loop continues until the model decides the next step.

Why bash matters

Bash is the tool that gives Terminus, and most other coding agents, most of their range.

Without bash, every capability has to be designed as a separate tool. Running tests would need a test runner tool, and inspecting git history would need a git tool. The same is true for checking disk usage, running a formatter, installing a package, running a migration, starting a dev server, calling another CLI, reading environment variables, or reproducing a bug from the terminal, and each one becomes another tool definition, another schema, and another option the model has to choose from.

With bash, the harness gives the agent controlled access to the operating system. The agent runs the same commands a developer would run, e.g. npm test, pytest, git diff, rg, curl, ls, cat, or make. A small tool set therefore covers a large amount of work.

Bash does not replace proper tools. File edits should still go through a structured edit tool when possible, and dangerous commands need sandboxing, permissions, and clear feedback. For discovery, verification, and running the automation a project already has, bash covers almost everything.

Terminal agents became useful quickly for the same reason. They did not need a separate integration for every framework, because they run in the same environment as the developer and use the commands that are already installed.

ToDo

A coding agent working through a multi-step task can lose track of what it has already finished, so it helps to give the agent a tool that keeps a todo list and updates that list as tasks succeed or fail.

Diagram showing todo_read, todo_write, and todo_update tools writing to a ToDo JSON file
The todo tools give the agent a small task file to update, instead of tracking progress in the conversation.

The todo feature in Terminus is a file-backed task list with three tools:

  • todo_write: add one or multiple tasks with status pending, in_progress, or completed.
  • todo_read: return the current list.
  • todo_update: update status for one or multiple existing tasks by exact task text.

It persists to .todos.json in the current working directory as:

json
{"items":[{"task":"...","status":"pending"}]}

.todos.json is read only when Terminus explicitly calls todo_read, todo_write, or todo_update.

Key behaviour:

  • Duplicate task text is ignored on write.
  • todo_update matches tasks by exact string.
  • If the list becomes empty or all items are completed, Terminus deletes .todos.json automatically.

Ask user question

The point of the tool is to handle ambiguity. A coding agent should ask a clarifying question when several implementations are possible and would give different results, or when guessing could waste time, break expectations, or make unsafe changes.

In practice, the agent asks a question to settle intent before it acts, instead of quietly picking one option and hoping it matches what the user meant.

Asking also saves time and money, because the agent spends less effort on decisions the user did not want.

The ask_question tool defines structured questions with exactly three options and optional multi-select support. When the model calls that tool, the agent loop detects it specially and ends the turn immediately, recording the question and skipping any later tool calls so it can wait for the user's answer.

Ask User Question TUI in Terminus
The AskUserQuestion interface in Terminus.

Plan mode

Coding agents are good at doing things, but they are not always good at deciding what to do first, which is what plan mode is for.

In a system with many tools, planning lets the agent sort the work before it starts spending files, tool calls, and context. Planning helps most when the task is vague, spread across several files, or easy to approach in the wrong order.

A few practical reasons:

  • It separates working out the job from executing the job.
  • It gives the agent a short list of steps to follow when context gets trimmed or compacted.
  • It surfaces assumptions and risks earlier, before code changes pile up.
  • The user can list precise requirements early on.
  • It saves time, effort, and cost because the agent knows beforehand where to look and what to look for.

In terminus-cli, a planning pass helps the agent decide what to do now and what to leave until later.

Plan mode in terminus:

  • replace system prompt with planner prompt
  • restrict access to tools other than read-only tools like read files, grep, and web search
  • create a plan and maybe ask users clarifying questions if needed
  • allow the user to approve the plan or suggest changes until they ask to implement it

You can enter plan mode manually with /plan.

Planner prompt

      <role>
      You are Terminus in plan mode. You are an expert software-development
      planner whose job is to produce implementation-ready plans, not to perform
      the implementation.
      </role>

      <plan_mode_boundaries>
      You ARE allowed to:
      - inspect the repository with read-only tools
      - read project instructions, source files, tests, configs, schemas, and docs
      - use web search only when current external information is actually needed
      - load relevant skills when available
      - ask clarifying questions when product intent or tradeoffs cannot be inferred

      You are NOT allowed to:
      - implement the requested change
      - edit, create, delete, move, or format files
      - run code, tests, builds, package managers, migrations, or generated snippets
      - install dependencies
      - delegate implementation work
      - claim you verified behavior by running commands
      </plan_mode_boundaries>

      <tool_guidance>
      Plan mode is intentionally read-only. Use only planning-safe tools:
      - `ls`, `glob`, `grep_search`, and `file_reader` for local exploration
      - `todo_write`, `todo_read`, and `todo_update` for multi-step planning
      - `load_skill` when the task clearly matches an available skill
      - `ask_question` only for meaningful user decisions
      - `web_search` only for current external facts that affect the plan

      Never use mutation or execution tools in plan mode, including `file_editor`,
      `file_creator`, `bash`, `sandbox`, `subagent`, or `send_notification`.
      </tool_guidance>

      <planning_process>
      1. Ground yourself in the actual repo before finalizing a plan. Inspect likely
        entrypoints, relevant modules, tests, and project instructions.
      2. Separate discoverable facts from user preferences. Resolve discoverable
        facts through read-only exploration instead of asking the user.
      3. Ask clarifying questions only when the answer materially changes the plan
        and cannot be derived from the codebase or prompt.
      4. Identify the smallest coherent implementation approach that fits existing
        architecture, conventions, and dependencies.
      5. Produce a decision-complete plan: another engineer should be able to
        implement it without choosing APIs, file boundaries, or test scope.
      </planning_process>

      <quality_bar>
      - Be concrete about behavior, interfaces, data flow, and affected modules.
      - Prefer existing patterns and dependencies over new abstractions.
      - Include risks, assumptions, and unknowns without inflating the plan.
      - Keep the plan concise and information-dense; omit irrelevant rollout,
        deployment, or monitoring sections unless the change actually needs them.
      - Do not include emojis.
      </quality_bar>

      <output_format>
      Produce one structured implementation plan with these sections:

      **Summary**: Goal, intended user-visible behavior, and success criteria.
      **Implementation Changes**: Concrete subsystem-level changes, including
      important files or modules when needed to remove ambiguity.
      **Public APIs / Interfaces**: CLI commands, function signatures, schemas,
      prompts, tool contracts, events, or config changes. State "None" if unchanged.
      **Tests**: Specific test cases, scenarios, and verification commands an
      implementer should run.
      **Assumptions & Risks**: Defaults chosen, unresolved questions, edge cases,
      compatibility concerns, and risk areas.

      Combine sections only when the task is very small. If you lack enough
      information to produce a reliable plan, ask focused clarifying questions
      instead of guessing.
      </output_format>

The /init command

Every time an agent opens a codebase it has not seen before, it knows nothing about the project. It does not know where the entry points are, which commands are safe to run, what style the project follows, or which directories only hold generated files. The first few tool calls usually go into basic orientation instead of the actual task.

/init saves that orientation pass so later sessions can reuse it. Run it once and Terminus walks the project. It reads the file tree, checks the obvious config files, finds the run and test commands, notes local conventions, and writes the useful parts into an AGENTS.md file at the root.

The file becomes the agent's project memory. Later sessions start by reading it instead of rediscovering the same facts:

  • how to install dependencies
  • how to run the app
  • how to run tests, lint, and formatting
  • where the important code lives
  • which environment variables matter
  • project-specific gotchas and conventions

The important part is that AGENTS.md is plain text. The agent does the first draft, but the project owner should treat it like documentation. If the generated file misses a command, names the wrong env var, or overstates a convention, edit it. The next agent run will inherit the corrected version.

The feature is useful because it moves context out of the conversation and into the repository, where it survives between sessions.

Init prompt

    You are generating or updating an AGENTS.md file for the project in the current working directory.
    AGENTS.md is a high-signal reference that orients an AI agent to this specific codebase.
    It is not documentation for humans; every line must earn its place.

    Do not write AGENTS.md until you have thoroughly explored the codebase.

    ## CRITICAL — Output format
    Your entire response must be ONLY the raw markdown content of AGENTS.md.
    Do NOT include any preamble, summary, explanation, or meta-commentary.
    Do NOT say "Here is the AGENTS.md" or "I have created...".
    Start your response directly with "# AGENTS.md" and the first section header.

    ## Step 1 — Discover existing instruction files
    Before doing anything else, check for these files and treat them as primary sources:
    - AGENTS.md, CLAUDE.md
    - .cursor/rules/, .cursorrules
    - .github/copilot-instructions.md

    If any exist, read them first. If AGENTS.md already exists, your task is to update it —
    re-explore the codebase and identify what is stale, missing, or newly relevant.

    ## Step 2 — Explore the codebase
    Use available tools to build understanding in this order:
    1. Root-level files: README, manifests (package.json, pyproject.toml, Cargo.toml, etc.), lockfiles, CI config
    2. Tech stack: language(s), frameworks, key libraries and their versions
    3. Project structure: top-level modules, submodules, and how they relate
    4. Architecture: data flows, API boundaries, service interactions, notable patterns
    5. Operational: how to install, run, test, build, and deploy the project
    6. Security: input validation, auth, secrets handling, subprocess usage
    7. Known issues: existing bugs, TODOs, workarounds, technical debt

    ## Step 3 — Write AGENTS.md
    Populate only these sections, and only with verified, repo-specific information:

    ### What this project does
    One short paragraph. What problem it solves and what it produces.

    ### Tech stack
    Bullet list: language + version, frameworks, key libraries. Nothing obvious or generic.

    ### Project structure
    Only non-obvious layout decisions. Skip anything self-evident from filenames.

    ### How to run
    Exact commands for: install, dev server, test suite, build. Copy them verbatim from config files.

    ### Architecture & data flow
    End-to-end flows, API contracts, module boundaries, and service interactions.
    Include actual class names, file paths, and how a request or operation travels through the system.

    ### Adding features / extending
    Patterns for the most common extension tasks in this codebase (e.g., adding a new endpoint, component, tool, provider, or model).
    Include the specific files to touch and any registration steps.

    ### Conventions & gotchas
    - Patterns that diverge from framework or language defaults
    - Environment setup quirks or required secrets
    - Common mistakes an agent would make without this context

    ### Testing strategy
    How to verify changes. If there is no formal test suite, describe ad-hoc verification steps.

    ### Security & safety
    Input validation rules, auth mechanisms, secrets management, and subprocess safety.
    Flag any user-input surfaces that reach shell commands or file system operations.

    ### Known issues & technical debt
    Existing bugs, TODOs, workarounds, and fragile areas. Prevent the next agent from rediscovering them.

    ### Dependency & build notes
    Lockfiles, package managers, CI/CD pipelines, and deployment specifics.

    ## Rules
    Exclude:
    - Generic software advice
    - Long tutorials or exhaustive file trees
    - Obvious language conventions
    - Speculative claims or anything you could not verify

    If a file like any of these exists:
    CLAUDE.md, AGENTS.md, `.cursor/rules/`, `.cursorrules`, `.github/copilot-instructions.md`
    treat them the same.

    REMINDER: Output ONLY the raw markdown. No explanations before or after.

The @ command

The @ command lets you attach file context directly to a Terminus prompt. Instead of copying and pasting code, reference a file by prefixing its path with @:

bash
terminus "Optimize the code in @src/agent.py"

When Terminus sees @src/agent.py, it:

  1. Resolves the path relative to the current working directory.
  2. Reads the file contents from disk.
  3. Strips the @src/agent.py reference from the user-facing prompt.
  4. Injects the file contents into the agent's context as a structured block, labelled with the filename.

The agent receives both the cleaned prompt and the file contents as separate, clearly delimited inputs, so it knows exactly what you're asking and what code it's working with.

bash
terminus "Compare @src/agent.py and @src/coordinator.py"

The command helps because the model has no access to your local files and cannot safely guess which file you mean or what it currently contains. With the @ command, the context is explicit and the prompt stays short, so debugging, refactoring, explaining code, and writing tests all get faster and more accurate.

Agent skills

Agent Skills are modular, reusable packages of instructions, scripts, and context that extend what an agent can do. Instead of putting every workflow, guideline, and domain rule into the system prompt, skills let the harness expose specialized capabilities only when they are relevant.

Context is not free. If every possible instruction is loaded up front, the model carries irrelevant rules through the whole conversation. Skills are a way to manage context as much as they are a way to add capability.

At a high level, a skill is just a folder with a required SKILL.md file and optional supporting files:

text
my-skill/
  SKILL.md
  scripts/
  references/
  assets/
  eval/

SKILL.md is the entry point. It contains frontmatter that describes the skill, followed by instructions for how the agent should use it.

markdown
---
name: frontend-design
description: Use when building polished frontend pages, components, or apps.
---

Follow the existing design system first. Use responsive layouts, verify the UI in a browser,
and keep the first screen focused on the actual product experience.

The frontmatter gives the harness a cheap index of available skills without loading every skill body into model context. Terminus scans skill folders and uses the frontmatter fields to build a compact registry. Only the skill name, description, and trigger hint are added to the system prompt.

In the Terminus harness, this happens in src/prompts/system_prompt.py. The main get_system_prompt(cwd=None) function builds the base system prompt and then appends the result of get_skills_prompt(project_dir).

When the user asks for something, the model sees the available skill summaries and decides whether a skill applies. If it does, Terminus loads that skill's SKILL.md on demand and injects the full instructions into the conversation. Supporting files are still not loaded automatically.

The loading flow looks like this:

  1. Terminus discovers skills from configured skill directories.
  2. It reads each SKILL.md frontmatter block.
  3. get_system_prompt() appends get_skills_prompt(project_dir).
  4. The prompt exposes only compact skill metadata to the agent.
  5. The agent selects a skill when the user's task matches its description.
  6. The model calls load_skill with the selected skill name.
  7. LoadSkill.run() calls agent.load_skill(match).
  8. Agent.load_skill() injects the full SKILL.md as a separate system message.
  9. Additional files are loaded only if the skill workflow calls for them.

The pattern is progressive disclosure applied to agent context. The model learns that a skill exists, and it pays the context cost of the full skill only when the task needs it.

MCP

MCP, or Model Context Protocol, is a standard way to connect an agent to external tools and data sources without hardcoding every integration directly into the agent. Instead of Terminus needing bespoke code for GitHub, Slack, databases, browsers, or internal services, an MCP server can expose those capabilities through a common tool interface.

Diagram showing MCP as a standardized protocol between AI applications and data sources or tools
MCP sits between AI applications and external data or tool systems as a standardized bridge.

Tool growth is one of the easiest ways to ruin an agent harness. If every integration becomes a built-in tool, the registry gets noisy, the system prompt gets larger, and the model has more overlapping options to choose from. MCP lets Terminus keep its core tool set small while still allowing project-specific or user-specific tools to be attached when someone configures them.

In Terminus, MCP support is implemented as a bridge between external MCP servers and the existing ToolRegistry. The important file is src/mcp_bridge.py. It reads a project-level terminus.mcp.yaml, starts the enabled MCP servers over stdio, asks each server for its tools, and wraps each remote MCP tool in a normal Terminus tool adapter.

yaml
servers:
  demo:
    command: uvx
    args: ["demo-server"]
    cwd: "."
    env:
      TOKEN: "${MCP_TEST_TOKEN}"
    timeout_seconds: 60

The config loader resolves cwd relative to the current project, interpolates environment variables like ${MCP_TEST_TOKEN}, skips disabled servers, and validates that each server has a command, argument list, environment map, and positive timeout.

Once a server is connected, Terminus converts every MCP tool into an OpenAI-style function schema through McpToolAdapter. Tool names are namespaced and sanitized so they do not collide with built-in tools:

text
MCP server tool:   server = "github", tool = "create_issue"
Terminus tool:    mcp__github__create_issue

From the model's point of view, there is no special execution path. An MCP tool appears in the same tool schema list as grep_search, file_reader, bash, or load_skill. When the model calls mcp__github__create_issue, the adapter calls the original MCP server tool and formats the result back into plain text for the ReAct loop.

The flow looks like this:

  1. ToolRegistry creates an McpClientManager.
  2. McpClientManager loads terminus.mcp.yaml.
  3. Each enabled server is started through the MCP stdio client.
  4. Terminus initializes a ClientSession and calls list_tools().
  5. Each remote tool becomes a McpToolAdapter.
  6. The adapter is registered in tool_box beside native tools.
  7. The model calls the namespaced tool like any other function.
  8. The adapter forwards the call to session.call_tool().
  9. The MCP result is normalized into text, structured JSON, or a compact binary-content summary.

Operationally, Terminus exposes MCP through slash commands:

  • /mcp or /mcp status: show configured servers, connection state, tool counts, and warnings.
  • /mcp tools: list discovered MCP tools grouped by server.
  • /mcp refresh: tear down the existing MCP connections, rediscover servers, and rebuild the MCP portion of the tool registry.

MCP works as an extension layer on top of the Terminus tools. Core coding operations stay local and predictable, and external capabilities are loaded from project config, namespaced, refreshed explicitly, and passed through the same registry and ReAct loop as the rest of the harness.

Hooks

Hooks are the places where the harness observes or steers the agent loop without changing the model's reasoning step. They are useful because an agent does much more than return text. It streams status, runs tools, updates the UI, spawns workers, handles cancellation, writes traces, and sometimes reports back to a parent coordinator.

Terminus does not currently implement hooks as a separate user-configurable plugin file. It uses callback hooks in the runtime. The CLI creates callbacks from the display layer and passes them into Agent.run() or Coordinator.run(). The agent loop then calls those hooks at specific moments: when the model is thinking, when a tool is selected, when a tool returns output, when todos change, when a worker emits an event, and when the user cancels the run.

The shape in src/main.py looks like this:

python
response = self.agent.run(
    enriched_message,
    status_callback=handler.update_status,
    todo_display_callback=lambda todos: self.display.render_todo_panel(todos, handler=handler),
    tool_call_callback=handler.display_tool_call,
    tool_output_callback=handler.display_tool_output,
    stop_event=self.stop_event,
    worker_event_callback=self._emit_worker_event,
)

The important hooks Terminus uses are:

  • status_callback: reports model reasoning, mode switches, compaction status, malformed tool calls, and general progress messages to the UI.
  • tool_call_callback: fires before a tool runs, with the tool name, a human-readable label, and parsed arguments.
  • tool_output_callback: fires after a tool returns, so the UI can show the result without waiting for the whole agent turn to finish.
  • todo_display_callback: fires after todo_write, todo_update, or todo_read when the tool output contains todo items.
  • worker_event_callback: emits worker lifecycle and detail events such as worker_spawned, worker_detail, worker_notification, and worker_status.
  • notification_callback: lets a worker call send_notification and forward progress or final output back to the coordinator.
  • stop_event: cancellation hook checked before the run starts, after model calls, before tool execution, and inside worker flows.

Inside src/agent.py, the hooks sit directly on the ReAct loop. After the model returns tool calls, Terminus parses the arguments, calls tool_call_callback for each pending tool, executes the tools, and then calls tool_output_callback with each result. The terminal UI stays live, and the model-facing loop stays the same, which is reason, act, observe, and repeat.

Subagents and coordinator workers use the same idea, with an extra layer of wrapping. When a subagent is launched, Terminus wraps its status, tool-call, and tool-output callbacks so the parent UI receives worker events rather than raw output from the child agent. A child tool call becomes a worker_detail event, and a child status update can become a worker_notification. When the subagent finishes, the parent receives a worker_status event with the final result.

The send_notification tool is the explicit worker-to-coordinator hook. A worker can send a structured payload with status, summary, and final_response. Terminus attaches the private _notification_callback at runtime, so the notification is routed to the coordinator instead of being treated as normal user-visible chat.

Tracing uses the same hook points. When Langfuse is enabled, Agent.run() creates a trace for the run and spans around tool execution. Tracing is not exposed as a model tool. It is instrumentation on the harness side, attached around the same events the UI hooks use.

Hooks keep side effects out of the prompt. The model does not need to know how to repaint the terminal, update the todo panel, cancel workers, or write traces, because it only chooses actions. The harness watches those actions and runs everything around them.

Session management

Terminus saves chat history across sessions, so closing a terminal in the middle of a debugging session does not lose the conversation.

Conversation continuity. When you close your terminal and come back the next morning, your previous messages are all still there. You can resume exactly where you left off without re-explaining your codebase or re-establishing context with the model.

Tool logs and traces. Agentic workflows generate a lot of noise: file reads, shell commands, search results, API calls. Keeping a faithful record of every tool invocation and its output means you can audit what the agent actually did, not just what it told you it did.

Terminus stores all of it in SQLite, which needs no infrastructure, keeps everything in one file on disk, and has a query interface expressive enough to filter session history however you need. Each session gets its own record, and messages and tool traces are stored relationally, so you can reconstruct the full timeline of any conversation.

ColumnTypeDescription
idINTEGERAuto-increment primary key
nameTEXTThe session name you provide
timestampTEXTISO 8601 datetime, for example 2026-05-20T14:30:00.123456
chat_historyTEXTJSON string of the message array

Chat History JSON example:

json
[
  {
    "role": "system",
    "content": "<role>\nYou are terminus-cli, a CLI-based coding agent...\n</role>........"
  },
  {
    "role": "user",
    "content": "How do I configure a new LLM provider?"
  },
  {
    "role": "assistant",
    "content": "You can configure a new provider using the `/connect` slash command..."
  }
]

Subagent delegation

Delegation is simple. The main agent hands off a task, a subagent does the work, and only the final answer comes back, without the intermediate tool calls.

Everything an agent reads lands in its context, including stack traces, large files, command output, and half-relevant search results. The model then has more text to carry and more irrelevant detail to work through, and it is more likely to focus on the wrong thing.

A subagent keeps that work out of the parent context. Say the agent needs to explore a folder, read ten files, run a few commands, and come back with a recommendation. If the main agent does the work directly, every step grows its context. If a subagent does it, the parent makes one call and gets one result back.

The pattern works well for tasks like:

  • inspecting whole chunks of the codebase
  • pulling the one useful line out of a 500-line log
  • running a quick sanity check without losing your place
  • writing a self-contained chunk of code when the scope is obvious

Delegation is not a way to hand off a problem you have not defined. It works when the task has a clear boundary. "Find where auth is implemented and summarize the relevant files" is a good subagent task, and "refactor the whole auth system" usually is not, unless the parent supplies constraints, the expected files, and a definition of done.

The parent still owns the problem. It decides what to delegate, reads the result, and integrates it. A vague task does not disappear when you hand it off. The same vague task reappears inside a second context window.

Diagram showing a main agent delegating exploration work to a subagent and receiving findings back
Subagent delegation keeps intermediate work out of the parent agent context.

In Terminus, the subagent tool is deliberately simple. It creates a fresh Agent instance, gives it the task, and returns the final answer.

text
Main agent:     user goal -> plan -> one subagent call -> final answer
Subagent:       delegated task -> file reads -> bash output -> final answer
python
class SubAgent(ToolSchema):
    def __init__(self):
        self.name = "subagent"
        self.subagent = None  # Lazy initialization

    def description(self):
        return dedent("""
        Delegates a complex task to a separate agent instance with its own context.
        Use this to offload tasks that would consume too much of the main agent's
        context window, such as reading large files, processing verbose outputs,
        or performing multi-step operations.

        The subagent has access to all the same tools as the main agent but operates
        independently with its own conversation history.
        """).strip()

    def json_schema(self):
        return {
            "type": "function",
            "function": {
                "name": self.name,
                "description": self.description(),
                "parameters": {
                    "type": "object",
                    "properties": {
                        "task": {
                            "type": "string",
                            "description": "The task assigned by the main agent for the subagent to complete",
                        }
                    },
                    "required": ["task"],
                },
            },
        }

    def run(self, task: str, _status_callback=None, _stop_event=None, _tool_call_callback=None, _tool_output_callback=None):
        try:
            from src.agent import Agent

            self.subagent = Agent()
            self.subagent.add_system_message()

            result = self.subagent.run(
                user_message=task,
                status_callback=_status_callback,
                tool_call_callback=_tool_call_callback,
                tool_output_callback=_tool_output_callback,
                stop_event=_stop_event,
            )

            return result

        except Exception as e:
            error_msg = f"Subagent execution failed: {str(e)}"
            print(f"[ERROR] {error_msg}")
            return error_msg

Async coordinator mode

Coordinator mode is the part of Terminus that runs bounded pieces of work in parallel, without putting every worker transcript into the main conversation. Instead of one agent holding the whole investigation, the coordinator launches independent workers, tracks their state, and combines only the structured handoffs they return.

The /coordinator command replaces the default agent mode with coordinator mode, which works as an orchestrator for async workers.

Async Coordinator Mode diagram A hand-drawn architecture diagram showing a user request entering the coordinator, coordinator tools spawning async worker tasks, isolated agents returning compact JSON handoffs, and the coordinator using an ephemeral digest to synthesize the final answer. Async Coordinator Mode parallel worker contexts, compact handoffs, guarded synthesis User goal one main request Coordinator owns conversation tracks WorkerHandle map /coordinator Coordinator tools spawn, await, list, stop get_worker_result schema-first calls worker_1 explorer Agent.arun() isolated context + tools worker_2 verifier asyncio.Task own stop_event worker_3 impl bounded ownership no nested delegation spawn_workers_batch() JSON handoffs done, evidence, risks Ephemeral digest not stored in context Final synthesis after guards pass state awareness without transcript pollution runtime guard: do not answer until active workers are awaited and completed results are consumed
Coordinator workers run as isolated async tasks and return compact handoffs for guarded final synthesis.

The implementation lives in src/coordinator.py. A Coordinator owns the coordinator conversation, an in-memory map of worker ids to WorkerHandle objects, and runtime guards that prevent it from finishing before active or completed worker results have been handled.

The coordinator has its own tool surface:

  • spawn_worker: starts one background worker.
  • spawn_workers_batch: starts multiple independent workers together.
  • await_workers: waits for selected workers to finish.
  • get_worker_result: collects a completed worker handoff.
  • list_workers: inspects known worker state.
  • stop_worker: requests cancellation for a running worker.
  • send_notification: lets a worker report progress or a final update back to the coordinator.

Most worker lifecycle tools are schema-first. The model sees them as callable tools, but the real state transitions are handled directly by Coordinator.run(). That keeps orchestration in the harness, where worker ids, task handles, cancellation events, result envelopes, and consumed-result bookkeeping can be managed deterministically.

The lifecycle is straightforward:

  1. The coordinator model calls spawn_worker or spawn_workers_batch.
  2. Coordinator.run() parses the tool call and dispatches spawn operations with asyncio.gather().
  3. Coordinator.spawn_worker() creates a fresh Agent, a worker-specific system prompt, a worker-specific tool registry, and an asyncio.Event for cancellation.
  4. The worker starts as an asyncio.Task running agent.arun().
  5. The spawn call returns immediately with an id such as worker_1.
  6. The worker continues in the background while the coordinator can keep reasoning, spawn other workers, list state, or wait for results.
  7. When the task completes, fails, or is stopped, a done callback updates the corresponding WorkerHandle.
text
Coordinator
  -> spawn_workers_batch(...)
      -> worker_1: Agent.arun() as asyncio.Task
      -> worker_2: Agent.arun() as asyncio.Task
      -> worker_3: Agent.arun() as asyncio.Task

Coordinator
  -> await_workers(["worker_1", "worker_2", "worker_3"])
  -> get_worker_result("worker_1")
  -> synthesize compact handoffs

Each worker is isolated, and it gets its own context window, prompt, stop event, and tool registry. Terminus also removes the tools that would make worker behavior harder to control, including nested delegation, todo management, and direct clarification questions. A worker can do focused investigation or implementation, and the coordinator stays responsible for deciding how the result affects the final answer.

The handoff contract is strict on purpose. Workers are instructed to return valid JSON with these top-level fields:

json
{
  "what_was_done": "...",
  "evidence": ["..."],
  "unresolved_risks": [],
  "exact_next_step": "...",
  "status": "completed"
}

Coordinator._normalize_worker_result() turns worker output into a result envelope. If the worker returns malformed JSON, legacy field names, or a max-iteration response, the coordinator still converts it into a usable partial or failure envelope with explicit risks and a next step. That makes the coordinator resilient to imperfect model compliance.

The coordinator never reads full worker transcripts. Instead, _build_worker_digest() creates a temporary system message for each reasoning iteration. The digest holds counts for running and blocked workers, completed handoffs waiting to be combined, failed or stopped handoffs, recent notifications, and short previews of results that have not been collected yet. Terminus appends the digest only to the current model call and does not store it in Coordinator.context.

The coordinator therefore knows the current worker state without carrying old worker output through the rest of the session.

The system also has runtime guards against premature final answers. If the coordinator tries to finish while workers are still running, Coordinator.run() records that attempted answer and forces another iteration with instructions to wait. If completed worker results exist but have not been collected, it forces another iteration requiring result retrieval. If the model still fails to collect pending results after the guard triggers, the harness can produce a fallback synthesis from the pending result envelopes.

The UI gets its information from worker events. WorkerHandle emits lifecycle updates such as worker_spawned, worker_notification, worker_detail, and worker_status. The CLI display layer receives those events through callbacks, so users watch workers start, run tools, report progress, and finish, while the coordinator still never loads raw worker transcripts into model context.

The async coordinator system is where concurrency lives in the harness. It provides isolated worker contexts, background asyncio.Task execution, structured JSON handoffs, temporary state digests, explicit await and result tools, and guards that force the coordinator to account for worker output before it answers.

Coordinator prompt
You are terminus-cli, a coordinator agent that can delegate software engineering work to multiple worker agents.

Your job is to:
- Help the user achieve their goal
- Decide what to do yourself versus what to delegate
- Give workers precise, bounded workstream briefs
- Synthesize worker outputs into one coherent answer for the user

Messages that you send are visible to the user. Messages from workers are visible only to you unless you choose to summarize them.

### Operating Model

Workers are asynchronous and concurrent. Use concurrency deliberately, not reflexively. A worker is for an independent workstream, not a tiny errand.

Coordinator-specific instructions override shared coding-agent instructions when they conflict.
In coordinator mode, you have coordinator tools, not the regular agent todo tools (todo_write, todo_read, todo_update).

You are responsible for:
- Keeping the critical path moving
- Delegating only tasks that are independent and well-scoped
- Avoiding duplicated work and conflicting edits
- Ensuring the final answer is based on awaited worker results, not guesswork

### Delegation Decision

Before spawning any worker, decide:
1. What is the immediate critical path you should handle yourself
2. Which workstreams are independent enough to run without coordination
3. What files, directories, subsystems, or hypotheses each worker owns
4. What evidence each worker must return for the result to be useful
5. Which existing running or completed worker already overlaps with the proposed work

Default to zero or one worker. Spawn multiple workers only when you can name distinct ownership boundaries. Good boundaries are separate subsystems, separate file sets, separate hypotheses, or separate verification targets.

Do not split a larger task into many one-line assignments. For larger work, create a small number of scoped workstreams with enough context for each worker to finish its part end-to-end.

Do not spawn redundant workers for the same question. If two proposed workers would inspect mostly the same files, answer the same question, or produce the same evidence, merge them into one worker brief or do the work yourself.

### When To Delegate

Delegate when:
- The task can be split into independent sub-problems
- Work is read-heavy, exploratory, or parallelizable
- A side task can run in the background while you continue reasoning locally
- Multiple files or subsystems can be investigated separately without coordination risk
- A bounded area can be owned end-to-end by one worker without blocking the coordinator

Do not delegate when:
- The task is trivial or faster to do yourself
- The next step depends immediately on the result
- The scope is ambiguous and likely to produce vague worker output
- Multiple workers would need to edit the same files or tightly coupled code
- You cannot write a complete worker brief with objective, boundary, constraints, verification, and definition of done

Prefer doing the immediate blocking step yourself. Use workers for sidecar tasks that materially advance the solution without blocking your next move.

### Concurrency Rules

- For read-only or independent workstreams, spawn all needed workers in a single turn.
- Prefer `spawn_workers_batch` only when launching multiple genuinely independent workers at once.
- Do not spawn workers one at a time across multiple turns if they were knowable together.
- After spawning workers, wait for the spawn tool result before calling `await_workers` or `get_worker_result`.
- Do not spawn workers and await worker results in the same assistant tool-call batch.
- For write-heavy tasks, do not run concurrent workers with overlapping write scopes.
- If multiple edits are needed across separate areas, assign each worker a distinct ownership boundary.
- Do not run separate explorer, implementer, and verifier workers over the same area at the same time. Sequence them after results, or assign one worker to own the bounded area.
- If ownership is unclear, explore first yourself or spawn one explorer to map ownership boundaries. Delegate edits only after the plan is clear.
- Before every spawn, check whether an existing worker already covers that boundary. Await or reuse that result instead of creating a duplicate.

### Worker Prompt Requirements

Every worker assignment must be a self-contained brief, not a one-line task. Include:
- Objective: the exact outcome the worker owns
- Context: the user goal and why this worker exists
- Ownership boundary: files, directories, subsystems, commands, or hypotheses in scope
- Non-goals: nearby work the worker must avoid
- Mode: read-only, propose-only, or edit-allowed
- Work plan: two to five concrete steps the worker should perform
- Constraints: assumptions, safety limits, style requirements, and things to avoid
- Verification: checks, tests, commands, or evidence expected when practical
- Handoff: the compact JSON fields required in the final answer
- Definition of done: what must be true before the worker stops

If you cannot fill those fields, do not spawn the worker yet.

Good worker tasks are narrow and verifiable. Bad worker tasks are broad, vague, or duplicative of your own reasoning.

Good brief:
Role: explorer
Objective: Trace how visitor counts are loaded and rendered.
Boundary: `src/app/api/visitors`, `src/lib/visitors.ts`, `src/components/VisitorCount.tsx`.
Mode: read-only.
Steps: inspect the API route, inspect the data helper, inspect the component, report the runtime flow and failure modes.
Done: return the exact files involved, observed fallback behavior, and the next implementation step.

Bad brief:
Check visitors.

### Worker Roles

When spawning workers, assign an explicit role:
- `explorer`: investigate code, gather facts, trace behavior, identify relevant files
- `implementer`: make or propose concrete code changes in a bounded area; assign exact ownership boundaries and require changed paths in the handoff
- `verifier`: validate behavior, find regressions, check test gaps, confirm claims
- `summarizer`: condense evidence from completed work into a concise synthesis

Pick the narrowest role that matches the task. Do not use a generic worker when one of these roles fits.
Do not spawn one worker per role just because a task is large. Roles describe output shape, not a required pipeline.
For implementers, specify exactly which files or directories they may edit, tell them to avoid unrelated changes, and require verification evidence when practical.

### Worker Result Contract

Workers have isolated contexts. Their intermediate reasoning is not shared with you unless they explicitly report it.

Workers are required to return a compact final handoff. When you read worker results, expect:
- `what_was_done`
- `evidence`
- `unresolved_risks`
- `exact_next_step`
- `status`

Treat `evidence` as the basis for synthesis, not the worker's confidence. Use `unresolved_risks` to decide whether to continue, retry, or qualify the final answer. Use `exact_next_step` as the default next action unless your own reasoning supersedes it.

Preserve provenance when synthesizing. Cite which worker produced which evidence or risk when that matters.

### Worker Lifecycle

- Use `spawn_worker` for a single worker.
- Use `spawn_workers_batch` for multiple independent workers in parallel.
- Use `list_workers` to inspect current worker state when needed.
- Use `get_worker_result` for a non-blocking check of a completed worker.
- Use `await_workers` to collect final worker outputs before final synthesis.
- Use `stop_worker` to cancel stale, stuck, or no-longer-useful work.
- Workers run in the background. You may continue reasoning while they run.
- Worker notifications are progress signals, not authoritative final results.
- A worker's awaited structured result is the authoritative output for synthesis.
- You MUST use `await_workers` before returning a final answer if any relevant workers were spawned.

### Failure Handling

If a worker fails, stalls, or returns weak output:
- Determine whether to retry, narrow the task, or do the work yourself
- Do not blindly respawn the same vague task
- Use `list_workers` if state is unclear
- Use `stop_worker` if the work is no longer useful

Treat worker output as evidence to evaluate, not truth to repeat uncritically.

### Synthesis Rules

Before answering the user:
- Await any relevant running workers
- Integrate worker results with your own reasoning
- Resolve contradictions between workers
- Prefer concrete evidence over confident summaries
- Call out uncertainty or incomplete results when necessary

Do not return a final answer while relevant workers are still running.

### User Communication

- Keep user-facing updates brief and useful
- Summarize progress and findings rather than exposing raw worker chatter
- Present a coherent final answer rather than a dump of worker outputs

### Tools

Do simple, local, or immediately blocking work yourself instead of delegating by default.

- `spawn_worker`: Spawn one worker with a name, description, and prompt.
- `spawn_workers_batch`: Spawn multiple workers concurrently in one call.
- `list_workers`: List tracked workers and their statuses.
- `get_worker_result`: Retrieve the result of a completed worker.
- `await_workers`: Wait for workers to finish and collect their results.
- `stop_worker`: Stop a worker by ID.
- `send_notification`: Send a notification when necessary, though worker updates are usually injected automatically.
Worker prompt
You are terminus-cli, a worker agent running under a coordinator.

Your job is to complete the coordinator's assigned workstream and return a compact handoff. The user does not see your intermediate messages unless the coordinator summarizes them.

### Assignment Contract

Treat the coordinator's assignment as your contract. It should include an objective, context, ownership boundary, mode, constraints, verification expectations, and definition of done.

If the assignment is a one-line task or lacks enough scope to work safely:
- Do not invent a broad mission
- Do only the smallest safe discovery needed to clarify what is missing
- Return `status: "blocked"` or `status: "partial"`
- Put the missing scope or decision in `unresolved_risks`
- Put the exact question or next action in `exact_next_step`

If the assignment is large but has a clear ownership boundary, decompose it internally and finish that bounded workstream end-to-end. Do not ask the coordinator to spawn more workers.

### Scope Discipline

- Work only inside the assigned ownership boundary.
- Do not inspect or edit unrelated files unless required to satisfy the assigned objective. If you must cross the boundary, record why in `evidence`.
- Do not duplicate likely work from other workers. Assume parallel workers exist and stay within your lane.
- Do not perform speculative refactors or cleanup.
- Do not change files unless the assignment explicitly allows edits.
- Do not use nested delegation.
- Do not ask the user questions. Report ambiguity in the handoff.

### Execution

Start by identifying:
- Objective
- Ownership boundary
- Mode: read-only, propose-only, or edit-allowed
- Definition of done

Then work through the assignment:
1. Inspect the relevant files, commands, or outputs before forming conclusions.
2. For implementation work, make the smallest change inside the allowed boundary.
3. Verify when practical with the assigned checks or the narrowest local check available.
4. Stop when the definition of done is met or when missing scope blocks safe progress.

### Handoff

Return valid JSON only. Do not wrap it in markdown.

Use these top-level fields:
{
  "what_was_done": "Concise summary of completed work",
  "evidence": [
    "File paths, command results, observed behavior, or concrete facts"
  ],
  "unresolved_risks": [
    "Unknowns, missing scope, failed checks, or conflicts"
  ],
  "exact_next_step": "The single most useful next action for the coordinator",
  "status": "completed | partial | blocked | failed"
}

For `evidence`, prefer facts the coordinator can verify: paths, symbols, commands, outputs, changed files, and observed behavior. Do not include long transcripts.

If you edited files, include the changed paths and the verification you ran. If verification was not run, say why in `unresolved_risks`.

The system prompt is one part of context engineering

A system prompt cannot make up for bloated tool definitions, noisy retrieval, or an overly long conversation history, and contradictory instructions loaded from elsewhere can override it.

Start minimal and expand from failures

The best workflow is iterative: start with a capable model, write a minimal prompt, test it, then add instructions and examples only for observed failure modes.

Write at the right altitude

A system prompt should sit between brittle hardcoded logic and vague advice that assumes context the model does not have. The useful middle is specific enough to guide behavior and flexible enough to let the model apply judgment.

Be explicit about scope

Newer models tend to follow prompts more literally. If formatting should apply everywhere, say so. If the model should use tools proactively, say when.

Structure the prompt like the desired behavior

Organize the system prompt into clear sections, e.g. role, operating principles, tool guidance, constraints, and output format. The structure of the prompt should match the structure of the output you want.

Use examples carefully

Examples are one of the strongest ways to steer format, tone, and decision-making. A few good examples beat a long list of abstract rules.

Treat tool definitions as prompt text

Tool definitions are loaded into the same context as the system prompt. They consume the same attention budget and influence behavior in the same way.

Keep the stable prefix stable

For production agents, prompt stability affects cost and latency. Prompt caching works best when the reusable prefix stays byte-for-byte stable across requests.

Do not use prompting as a substitute for reasoning budget

For models that expose an effort or reasoning parameter, that parameter is often the right lever for depth. The system prompt should define what to do and how to behave. The effort setting controls how much thinking the model spends on it.

Prompt for known risks, but accept the limit

Some failure patterns are worth naming directly, e.g. over-engineering, destructive actions, fixating on tests, and guessing about files the model has not opened. Prompts do not make a probabilistic system deterministic, though. For high-risk actions, the answer is still tool design, permissions, evals, and human approval at the right points in the loop.

03

Context Engineering

Prompt engineering is no longer the main lever. The larger one is managing everything a model sees across a whole session, including system prompts, tools, retrieved documents, conversation history, and runtime observations.

What it is

Context engineering is the practice of choosing and maintaining the set of tokens inside a model's context window during inference, and it is the successor to prompt engineering. Prompt engineering optimizes a single instruction block, and context engineering covers the whole context. A good system prompt cannot make up for bloated tool definitions, unfiltered retrieval, or a conversation history full of contradictions.


The attention budget

The context window is a limited resource. Every token you add uses part of the attention budget, and the returns fall off as the input grows. Research from Chroma across 18 current models found that performance degrades unevenly as input length grows, even on deliberately simple tasks.

The study looked at 18 models, used an input to output token ratio of 1000 to 1, and found that context quality starts degrading much earlier than the advertised context window suggests. At around 40% of the window, extra context can already hurt more than it helps.

Empirical findings

Models perform worse when the surrounding filler text is logically ordered than when it is shuffled and incoherent.

Irrelevant context forces the model to retrieve and reason at the same time, and both get worse.

Failures look different across model families. Claude declines to answer, GPT answers confidently with invented content, and Gemini produces random tokens.

Even simple copying tasks become unreliable at long context lengths.


What occupies the context window

Terminal-style context usage panel showing token categories, memory files, skills, messages, and free space
The context panel shows where the tokens in a session are going.

In a typical agent session, the window is already loaded before the user types anything, and it keeps growing as the user works with the agent:

  1. System instructions and identity prompts
  2. Memory files, skills, and configuration (e.g. CLAUDE.md)
  3. Tool definitions and MCP server schemas
  4. Retrieved documents or workspace files
  5. Conversation history and action-observation pairs
  6. Current user request and attached context
  7. Tool call results

All of these compete for the same attention budget.


Context rot

Context rot is the gradual decline in a model's ability to recall and reason accurately as the context grows. The decline is slow rather than sudden. Attention spreads across more relationships, and irrelevant history, redundant tool outputs, and old instructions all compete with the current task.

The effect shows up during long coding or research sessions. After enough tool calls and corrections, the model starts inventing details, forgetting constraints, missing obvious bugs, or agreeing with the user without checking. The phrase "You're absolutely right" is a reliable warning sign.

The fix Keep the context small, and clear tool outputs you no longer need. Use retrieval only when the task needs it, and save state to files. When you restart, restart from a summary you wrote yourself rather than from automatic compaction. For complex work, a written spec and a series of small focused tasks work better than one long conversation.

Long context is not free memory, and every extra token has a cost. Good systems manage context deliberately instead of adding more of it, and how you manage it has a large effect on how well the agent performs.

04

Context Management

Messages array design

Treat the messages array as a strictly append-only, prefix-stable sequence. Static content like system prompt, tool definitions, examples, and project context must occupy the earliest positions. Dynamic content like user messages, tool outputs, timestamps, and IDs must come after it.

Why prefix stability affects caching

Prompt caching works on content, not on conversation identity. When an inference engine processes a prompt, it splits the key and value tensors into fixed-size blocks and hashes each block together with the blocks before it. If block N has a matching hash, blocks 0 through N minus 1 are guaranteed to be identical. One changed token near the top therefore breaks the cache chain for every block after it.

For a deeper explanation of the caching mechanics behind this, refer to this prompt caching blog.

Construction rules for the messages array

  • Static prefix first, dynamic suffix last. Put reusable content at the beginning and volatile content at the end.
  • Append only. After a message is added, do not modify, truncate, reorder, or remove it.
  • Serialize deterministically. Render JSON, markdown, whitespace, casing, and templates the same way every time.
  • Keep volatile IDs out of the prompt body. Store request IDs, trace IDs, UUIDs, deployment hashes, and session tokens in provider metadata when possible.
  • Keep tool definitions stable. Avoid adding, removing, or reordering tool definitions mid-session.
  • Order retrieved context deterministically. Sort RAG results by a stable key with a fixed tie-breaker.
  • Use stable version markers. Use prompt version markers only for intentional cache busting.
05

Context Engineering Techniques in Terminus

Context size tracking

Terminus keeps an explicit ledger of the active conversation context. Each message is stored with its role and content, and the total context size is recalculated after every update.

It estimates token usage with a simple character-based heuristic: roughly 4 characters = 1 token. This estimate is used to decide when trimming or compaction should run.

Threshold-based trimming

Terminus does not wait until the context window is full.

When the context reaches about 50% of the model limit, it removes raw tool-output messages. Tool outputs are often large and noisy, so dropping them first saves space while keeping user and assistant messages intact.

Automatic context compaction

When context usage reaches about 75% of the model limit, Terminus compacts the conversation.

It summarizes older conversation history into a smaller system message. The latest user message is preserved directly so the immediate task remains clear.

Preserving high-priority instructions

During compaction, Terminus does not summarize everything.

It preserves the base system prompt and loaded skill prompts exactly as they are. This prevents important operating rules, project instructions, and task-specific skill guidance from being diluted by summarization.

Project instruction injection

Terminus reads AGENTS.md files from the current project and parent directories.

Terminus injects those files into the system prompt as authoritative project context, covering build commands, test commands, architecture notes, conventions, and known problems. The agent therefore behaves in a repo-specific way before it starts editing or answering.

On-demand skill loading

Skills are not fully loaded into the prompt by default.

The system prompt only lists the available skills with short descriptions. When a task clearly matches a skill, Terminus loads the full SKILL.md into context as a system message, so the default context stays small and specialized workflows are still available when a task needs them.

Explicit file reference injection

Users can reference files directly with @path/to/file.

Terminus parses those references, reads the files, removes the @file tokens from the original message, and appends the file content in structured tags:

xml
<file path="src/example.py">
...
</file>

The model gets precise code context without a broad repository scan.

Mode-specific prompts

Terminus swaps prompts depending on the operating mode.

In default mode, the agent is allowed to inspect, edit, run tools, and complete coding tasks. In plan mode, the prompt becomes read-only and focuses on repository discovery, implementation planning, risks, and verification strategy.

Tool output isolation

Tool results are stored as tool messages, separate from user and assistant messages.

Storing them separately makes it possible to remove raw tool outputs later without breaking the flow of the conversation. The choice is a simple one, and it makes context cleanup much easier.

Worker context isolation

In coordinator mode, Terminus can spawn worker agents.

Each worker gets its own isolated context window, which helps with large reads, focused investigation, verification, and implementation subtasks that would otherwise fill the main coordinator context.

Compact worker handoffs

Workers do not return full transcripts to the coordinator.

They are instructed to return compact structured JSON with:

json
{
  "what_was_done": "...",
  "evidence": ["..."],
  "unresolved_risks": [],
  "exact_next_step": "...",
  "status": "completed"
}

The coordinator gets useful output without importing every intermediate step.

Ephemeral worker digests

The coordinator builds a temporary digest of worker state for each reasoning iteration.

The digest lists running workers, blocked workers, completed handoffs, pending risks, and next steps. Terminus appends it only to the current model call and does not store it in conversation history, so the model sees the state without growing the long-term context.

Persistent session history

Terminus stores session history separately from the active context.

The active context is built for the current model call, and the SQLite-backed history keeps earlier conversations available for retrieval or session restoration. Long-term storage is therefore separate from short-term prompt construction.

Manual user controls

Terminus exposes commands for context management:

  • /context shows the active context
  • /context_size shows current context size
  • /compact manually triggers compaction
  • /reset clears the active session
  • /skill <name> explicitly loads a skill

Users therefore have direct control when the automatic strategy is not enough.

06

Tool Design

A good agent tool is more than a wrapper around a backend API. It is an interface written for a caller that does not behave the same way every time, so the tool has to carry its own context.

A normal API assumes the caller already knows when to call it, what to pass, and how to read the result. An agent tool has to teach all of that through its name, description, schema, return shape, and error messages.

Start from the workflow, not your API surface

Backend APIs tend to expose implementation details like list_users, create_event, or get_ticket. Agents do better when a tool maps to a natural task step, e.g. searching for availability, summarizing a customer's recent history, or finding relevant logs with the surrounding lines.

Keep the tool set small and non-overlapping

Every loaded tool description competes with the task context. More tools can make an agent worse when several of them look like they do similar things.

Name tools for selection

Tool names should help the model choose correctly under pressure. In a large tool set, search is weak. jira_issues_search or gmail_messages_search are easier to select because they carry the domain and object type.

Make schemas hard to misuse

Parameter names should make the expected value obvious: user_id instead of user, query instead of input, absolute_file_path instead of path when relative paths would be unsafe.

Return high-signal output

Tool responses should prefer fields the agent can reason with: names, titles, timestamps, statuses, snippets, relationships, and concise explanations. Raw UUIDs, internal field names, large blobs, and irrelevant metadata should be omitted unless needed.

Make errors and truncation actionable

An error message is also a prompt. A good error says which parameter failed, why it failed, what values are valid, and what to try next.

Writing tool descriptions

A good tool description answers what job the tool performs, when to use it, when not to use it, what inputs it requires, what it returns, and what the agent should do with the result.

text
Use this tool to [specific job] when [task condition].

Do not use it for [nearby cases handled by other tools or by reasoning].

Inputs:
- [param]: [type, source, constraints, example]

Returns:
- [important fields and how to interpret them]
- [whether results may be truncated or paginated]

After calling:
- [recommended next step based on common result shapes]

Notes:
- [domain-specific syntax, permissions, or side effects]
07

Writing System Prompts

Writing a system prompt has shifted from crafting one perfect instruction to choosing the smallest set of useful tokens that improve the model's odds of doing the right thing.

The system prompt is no longer the only control you have. It is one part of a larger context window that also includes tool definitions, message history, retrieved documents, memory files, and sometimes framework instructions the user never sees.

System Prompt
<role>
You are terminus-cli, a CLI-based coding agent. You are an AI assistant that helps users with coding tasks by ACTIVELY using the available tools.
</role>

Today's date is 2026-05-21

If the user asks for help or wants to give feedback inform them of the following: 
- /help: Get help with using Terminus CLI
- To give feedback, users should report the issue at https://github.com/sidmanale643/terminus-cli/issues

IMPORTANT: Always refrain from using emojis unless explicitly requested by the User.

<tool_usage_instructions>
CRITICAL TOOL USAGE RULES:
1. When you need to use a tool, call it directly without combining explanatory text in the same response
2. After receiving tool results, you can then provide brief commentary
3. NEVER mix explanatory text with tool calls in the same response
4. If you need to use multiple tools, call them one at a time
5. Do not generate any markdown, code blocks, or explanations when calling tools
6. Simply make the function call and wait for the result

CORRECT PATTERN:
- User asks question -> You call tool -> Tool returns result -> You provide brief response

INCORRECT PATTERN:
- User asks question -> You write explanation AND try to call tool -> ERROR

Brief status updates are fine between tool calls, but tool-call messages must contain only the tool call.
</tool_usage_instructions>

<task_management>
You have access to todo tools (todo_write, todo_read, todo_update) to help you manage and plan tasks.
For ANY task that involves multiple steps, risky edits, or is expected to take several tool calls, you MUST use the todo tools.
Always start by using todo_write to create the list of steps, then use todo_update to mark tasks as in_progress when you start them and completed when you finish them.
Use todo_read to view the current list.
The todo tools are essential for planning and for breaking down larger complex tasks into smaller steps.
For small, direct tasks that can be resolved in a single tool call, avoid creating todos.
</task_management>

<changes>
When making changes to the codebase, first always understand the conventions of the codebase and the style of the codebase.
</changes>

<problem_solving_workflow>
Follow this structured approach for every task:

1. **Planning & Discovery**: Read the task, scan the codebase, and build an initial plan based on the task specification and what verification looks like.
2. **Build**: Implement the plan with verification in mind. Add focused tests when needed to verify code changes, and test both happy paths and edge cases.
3. **Verify**: Run tests, read the full output, compare results against the original request (not against your own code).
4. **Fix**: Analyze any errors, revisit the original spec, and fix issues.
</problem_solving_workflow>

IMPORTANT: Keep your responses short, since they will be displayed on a command line interface. Answer the user's question directly, without elaboration, explanation, or details. Avoid introductions, conclusions, and explanations unless you have made changes to the codebase.

<instructions>
- After every tool call look at the output and think about the next step you need to take.
- NEVER proactively create documentation files (*.md) or README files. Only create documentation files if explicitly requested by the User.
- Avoid using emojis unless explicitly requested by the User.
- Use tools iteratively until the task is complete
- Provide brief explanations of what you're doing as you work
- If you're unsure about something, use tools to gather information
- Do not add comments to the code unless explicitly asked to do so.
- Always prioritize using existing files rather than creating new ones
- Understand the user's intent, sometimes the user might just be trying to explore and understand the codebase help them do that
- Always prefer using the packages/libraries the user is already using, refer to file imports, pyproject.toml and requirements.txt
- Prioritize and strictly follow any custom user instructions if provided
</instructions>

<output_format>
- Provide brief, actionable updates as you work
- Use markdown formatting for clarity
- Explain changes AFTER you make them, not before
- NEVER use emojis unless specifically asked to
- NEVER create broad, unrelated test files or additional .md files unless specifically asked to
- Focused test files are allowed when they are needed to verify code changes
- NEVER add any comments or doc strings unless specifically asked to
- If you have made changes to the codebase, provide a brief explanation of the changes you made.
- NEVER use emojis in readme files.

</output_format>

<project_directory>
/Users/sidmanale/Development/terminus-cli
</project_directory>


<skills>
Skills are specialized instruction sets that provide domain-specific workflows, templates, and best practices for specific tasks. They are loaded on-demand to keep the context window clean.

How to use skills:
- If a relevant skill is already loaded in the conversation, follow its instructions carefully before the general instructions.
- If a task clearly requires an available skill that has not been loaded yet, use the `load_skill` tool to load it.

When to use skills:
- Use loaded skills when a user's task matches a skill's description or trigger keywords
- If unsure whether an unloaded skill applies, ask a brief clarifying question or continue with the best available general guidance
- Skills override general instructions when active

Available skills:
- **humanizer**: Remove signs of AI-generated writing from text. Use when editing or reviewing
text to make it sound more natural and human-written. Based on Wikipedia's
comprehensive "Signs of AI writing" guide. Detects and fixes patterns including:
inflated symbolism, promotional language, superficial -ing analyses, vague
attributions, em dash overuse, rule of three, AI vocabulary words, passive
voice, negative parallelisms, and filler phrases.
- **langfuse**: Interact with Langfuse and access its documentation. Use when needing to (1) query or modify Langfuse data programmatically via the CLI — traces, prompts, datasets, scores, sessions, and any other API resource, (2) look up Langfuse documentation, concepts, integration guides, or SDK usage, or (3) understand how any Langfuse feature works. This skill covers CLI-based API access (via npx) and multiple documentation retrieval methods.
- **test-echo**: A minimal skill for verifying the skill-loading pipeline. Echoes back the user's message to confirm the skill system is working. (trigger: /test-echo)
</skills>
    <AGENTS.md>
    - AGENTS.md is the authoritative source for project-specific context, build steps, test commands, coding conventions, and architecture decisions.
    - If AGENTS.md exists in the project root or parent directories, you MUST read and follow its instructions before making any changes.
    - Treat AGENTS.md as a complement to README.md: READMEs are for humans, AGENTS.md is for you.
    - If your changes make AGENTS.md inaccurate, update AGENTS.md to keep it in sync.

    AGENTS.md file content, escaped to preserve prompt boundaries:
    File: /Users/sidmanale/Development/terminus-cli/AGENTS.md
# AGENTS.md — terminus-cli

AI-powered CLI development companion. Python backend + React/Ink terminal UI.

## Development Setup

**Requires:** Python 3.11+, [uv](https://docs.astral.sh/uv/), Node.js (for React UI)

```bash
uv sync
uv pip install -e .        # editable install is required; uv sync alone is not enough
cp .env.sample .env
# Edit .env with API keys (see below)
cd ui/react && npm install
```

**Environment variables** (`.env`):
- `GROQ_API_KEY` — Groq provider
- `OPEN_ROUTER_API_KEY` — OpenRouter provider
- `TAVILY_API_KEY` — Tavily web search
- `DAYTONA_API_KEY` — Daytona sandbox (optional)
- `LANGFUSE_PUBLIC_KEY`, `LANGFUSE_SECRET_KEY`, `LANGFUSE_HOST` — Langfuse observability (optional)

> **Env var bug:** `.env.sample` lists `OPENROUTER_API_KEY`, but `src/llm_service/openrouter.py` and the `/connect` command read/write `OPEN_ROUTER_API_KEY`. Use the underscore version or the provider will fail.

> **Gemini:** `.env.sample` includes `GEMINI_API_KEY`, but there is no registered Gemini provider. Gemini models are available via OpenRouter.

## Running the Application

```bash
terminus                  # Interactive mode (React UI by default)
terminus "query"          # One-shot query
terminus --classic        # Classic Rich/prompt_toolkit UI
python -m src.main --debug
```

## Architecture Notes

### Agent Loop (`src/agent.py`)
- Tool-calling architecture: agent decides → tool executes → result fed back
- Max iterations: `50`
- Raw tool-output trimming triggers at `50%` of model context limit
- Context compaction triggers at `75%` of model context limit
- Modes: `default` (system prompt) and `plan` (planner prompt)
- `@filename` syntax triggers file-reference enrichment before the agent sees the message
- Default provider: `groq`; default model: `openai/gpt-oss-120b`

### Coordinator Mode (`src/coordinator.py`)
- `/mode` switches between single agent and multi-agent coordinator
- Coordinator spawns background workers with roles: `explorer`, `implementer`, `verifier`, `summarizer`
- Worker max iterations: `12`; excluded tools: `subagent`, `todo`, `ask_question`
- Context is **not** shared between agent and coordinator modes

### React UI Bridge (`ui/react_display.py` ↔ `ui/react/src/`)
- **IPC:** JSON-line protocol over a Unix domain socket
- Python spawns `tsx src/main.tsx` (or `npx tsx src/main.tsx` if local binary missing)
- React inherits real TTY `stdin`/`stdout` so Ink raw-mode keyboard input works
- Socket path passed via `TERMINUS_SOCK` env var
- Key constraint: React components must **not** open sockets or mutate protocol state directly — bridge modules own that

### LLM Service (`src/llm_service/service.py`)
- Registered providers: `groq`, `openrouter`
- Model registry lives in `src/models/llm.py`

### Session & Context
- `SessionHistory` uses SQLite: `.db/chat_history.db` (persistent) + in-memory session table
- Context size tracked manually; compaction happens automatically
- `/reset` clears session history and restarts context
- `/compact` manually triggers context compaction

### Permissions
- Terminus effectively operates with a **3 tier permission model**:
- **Tier 1 — Read-only planning:** `/plan` mode can inspect files and structure, but cannot edit files, run commands, execute code, or delegate implementation work.
- **Tier 2 — Normal agent work:** default agent/coordinator flows can read, create, and edit project files through registered tools to implement requested changes.
- **Tier 3 — Elevated execution:** `bash` and `sandbox` are the highest-risk capabilities and should be used sparingly, with destructive commands, installs, or system-changing operations requiring explicit user confirmation.

### Skills
- Discovered from `.skills/` directory in the current working directory
- Each skill is a `SKILL.md` with YAML frontmatter (`name`, `description`, etc.)
- Loaded into context as a system message via `/skills` or `/skill <name>`

### Project Customization
- `terminus.md` in project root: loaded as custom instructions into the system prompt

## Frontend (React/Ink)

```bash
cd ui/react
npm run build   # TypeScript check only (tsc) — no bundler
npm run dev     # Run via tsx (standalone, for testing)
npm run start   # Run compiled dist/main.js
```

- Uses **Ink 5.x** + **React 18**
- TypeScript strict mode enabled
- Build output goes to `ui/react/dist/`

## Adding Tools or Features

1. Implement tool in `src/tools/`
2. Register in `src/tools/tool_registry.py`
3. If agent needs to know about it, ensure it's in the tool registry (agent auto-discovers)
4. Update `src/prompts/` if behavior changes
5. Update `README.md` if user-facing

## Testing

No formal test suite. Ad-hoc verification:

```bash
# Async system tests (run via uv recommended)
uv run python tests/test_async_system.py

# React UI socket smoke test
python test_react.py

# React UI component test
python test_react_ui.py

# React UI stability test (long transcript + worker activity)
python test_react_stability.py

# React UI unit tests (protocol, state, layout)
cd ui/react && npm run test
```

## Lint / Format

```bash
ruff check src/    # lint
ruff format src/   # format
```

No `ruff.toml` — uses pyproject.toml defaults. No pre-commit, no CI workflows.

## Key Conventions & Gotchas

- **Threading:** `stop_event` (`threading.Event`) is used pervasively for cancellation. Check it in long-running operations.
- **Ctrl+C handling:** Double Ctrl+C within 2 seconds exits; single Ctrl+C cancels the current turn and returns to the prompt.
- **File references:** `@path/to/file` expands to file content inline. Errors are shown as warnings.
- **Command palette:** F1 or any unrecognized `/command` opens the palette (`src/commands/palette.py`).
- **Editable install:** Must run `uv pip install -e .` after cloning; `uv sync` alone does not install the package in editable mode.
- **Node fallback:** If `ui/react/node_modules/.bin/tsx` is missing, Python falls back to `npx tsx`.
- **React build is typecheck only:** `npm run build` runs `tsc` — no bundler. The runtime uses `tsx` or `node` directly.
- **Langfuse:** If enabled, connectivity is tested on startup and a warning is printed if the trace endpoint is unreachable.
- **Debug logging:** Set `TERMINUS_DEBUG=1` to enable debug output (printed to stderr).
- **Gitignored:** `.agents/` and `.skills/` directories are in `.gitignore` — do not commit them.
- **Web search:** `WebSearch` tool is registered and available to both agent and coordinator. Requires `TAVILY_API_KEY`.

## Slash Commands (Interactive Mode)

| Command | Description |
|---------|-------------|
| `/help` | Help text |
| `/context` | Show current context |
| `/history` | Last 5 messages |
| `/reset` | Clear session |
| `/exit` | Quit |
| `/clear` | Clear screen |
| `/context_size` | Token count |
| `/compact` | Compress conversation context |
| `/models` | Switch model |
| `/connect` | Configure provider API key |
| `/skills` | List available skills |
| `/skill <name>` | Load a skill |
| `/copy` | Copy last response to clipboard |
| `/init` | Generate or update AGENTS.md |
| `/mode` | Switch between agent and coordinator mode |
| `/plan` | Create an implementation plan |

    </AGENTS.md>

The system prompt is one part of context engineering

A system prompt cannot make up for bloated tool definitions, noisy retrieval, or an overly long conversation history, and contradictory instructions loaded from elsewhere can override it.

Start minimal and expand from failures

The best workflow is iterative: start with a capable model, write a minimal prompt, test it, then add instructions and examples only for observed failure modes.

Write at the right altitude

A system prompt should sit between brittle hardcoded logic and vague advice that assumes context the model does not have. The useful middle is specific enough to guide behavior and flexible enough to let the model apply judgment.

Be explicit about scope

Newer models tend to follow prompts more literally. If formatting should apply everywhere, say so. If the model should use tools proactively, say when.

Structure the prompt like the desired behavior

Organize the system prompt into clear sections, e.g. role, operating principles, tool guidance, constraints, and output format. The structure of the prompt should match the structure of the output you want.

Use examples carefully

Examples are one of the strongest ways to steer format, tone, and decision-making. A few good examples beat a long list of abstract rules.

Treat tool definitions as prompt text

Tool definitions are loaded into the same context as the system prompt. They consume the same attention budget and influence behavior in the same way.

Keep the stable prefix stable

For production agents, prompt stability affects cost and latency. Prompt caching works best when the reusable prefix stays byte-for-byte stable across requests.

Do not use prompting as a substitute for reasoning budget

For models that expose an effort or reasoning parameter, that parameter is often the right lever for depth. The system prompt should define what to do and how to behave. The effort setting controls how much thinking the model spends on it.

Prompt for known risks, but accept the limit

Some failure patterns are worth naming directly, e.g. over-engineering, destructive actions, fixating on tests, and guessing about files the model has not opened. Prompts do not make a probabilistic system deterministic, though. For high-risk actions, the answer is still tool design, permissions, evals, and human approval at the right points in the loop.