Skip to main content
explanation

What does Ethos look like in 90 seconds?

Ethos has one core abstraction and a handful of interfaces around it. This page is the 90-second tour. Every term linked below has an entry in the glossary.

The one core abstraction​

AgentLoop is an AsyncGenerator<AgentEvent>. You give it a user message; it streams typed events back — text, tool calls, usage, errors, completion — until the turn is done.

Every dependency AgentLoop needs (LLM provider, session store, memory provider, personality registry, tool registry, hook registry) is an interface defined in @ethosagent/types and injected at construction. Core never imports concrete implementations.

The turn cycle​

~/.ethos/config.yaml wiring.ts LLMProvider SessionStore MemoryProvider PersonalityRegistry AgentLoop.run(text) session_start hooks MemoryProvider.prefetch() ContextInjector[] before_prompt_build hooks LLMProvider.complete() ToolRegistry.executeParallel() MemoryProvider.sync() agent_done hooks assembles all components from config AnthropicProvider | OpenAICompatProvider SQLiteSessionStore (WAL + FTS5) MarkdownFileMemoryProvider FilePersonalityRegistry (mtime hot-reload) AsyncGenerator<AgentEvent> → system context → system prompt assembly → stream chunks text_delta events · tool_use_start / delta / end · usage event before_tool_call hooks (arg override / rejection) parallel execution with budget splitting after_tool_call hooks
The turn cycle: config assembles the components in wiring.ts, and AgentLoop.run() streams each turn through hooks, context assembly, the LLM stream, and parallel tool execution.

Three things worth noticing in this diagram:

  1. Streams, not batched responses. Every step that emits output yields to the generator. The CLI prints text as it arrives; channel adapters update messages mid-flight.
  2. Hooks fire at every boundary. session_start, before_prompt_build, before_tool_call, after_tool_call, agent_done — each is a registration point for cross-cutting concerns (auth, audit, rate limiting).
  3. Tools execute in parallel within a budget. When the model returns multiple tool_use blocks in one turn, ToolRegistry.executeParallel runs them concurrently and splits an 80k-character result budget across them.

The surface architecture​

AgentLoop is a library. It does not know what is consuming its events. Nine surfaces ship today — all of them thin wrappers that feed user input into the loop and render whichever AgentEvent subset matters for their medium.

AgentLoop web-api (Hono + oRPC) Web (React) Desktop (Electron) Direct consumers CLI TUI (Ink) VS Code ext. MCP server ACP server Channel adapters (via gateway) Telegram Discord Slack WhatsApp Email AsyncGenerator<AgentEvent> HTTP + SSE for browser clients ethos serve --web native tray, global shortcuts readline REPL React-Ink terminal dashboard sidebar panel in editor serves Ethos as MCP tools Agent Comm. Protocol
Nine surfaces consume the same AgentLoop event stream: web and desktop through web-api, five direct consumers in-process, and five channel adapters via the gateway.

The web and desktop apps connect through the same web-api layer — a Hono HTTP server with oRPC-typed endpoints that streams AgentEvent over SSE. This is the only API surface; there is no separate REST API. Both apps talk to the same AgentLoop instance the CLI uses.

Direct consumers embed the loop in-process. The CLI, TUI, and VS Code extension each construct an AgentLoop and consume its generator directly. The MCP server and ACP server wrap the loop behind their respective protocols, letting external agents call Ethos tools or delegate tasks to Ethos personalities.

Channel adapters are the thinnest layer. Each adapter translates platform-specific webhook payloads into InboundMessage and renders AgentEvent back into platform-specific API calls. The gateway handles dedup, routing, and per-bot loop isolation.

AgentEvent — the streaming contract​

Everything the agent does is one of these eight event types:

type AgentEvent =
| { type: 'text_delta'; text: string }
| { type: 'thinking_delta'; thinking: string }
| { type: 'tool_start'; toolCallId: string; toolName: string; args: unknown }
| { type: 'tool_progress'; toolName: string; message: string; percent?: number }
| { type: 'tool_end'; toolCallId: string; toolName: string; ok: boolean; durationMs: number }
| { type: 'usage'; inputTokens: number; outputTokens: number; estimatedCostUsd: number }
| { type: 'error'; error: string; code: string }
| { type: 'done'; text: string; turnCount: number }

Consuming the generator:

for await (const event of agentLoop.run('explain this codebase')) {
if (event.type === 'text_delta') process.stdout.write(event.text)
if (event.type === 'tool_start') console.log(`\n[${event.toolName}]`)
if (event.type === 'done') console.log(`\nTurns: ${event.turnCount}`)
}

A surface (CLI, channel adapter, web UI) renders whichever subset of events it cares about. The contract is the same everywhere — same event types, same fields, same semantics.

Injection at construction​

AgentLoop receives every component via AgentLoopConfig. Nothing is global. The wiring.ts in the CLI reads ~/.ethos/config.yaml and assembles the loop:

apps/ethos/src/wiring.ts
const loop = new AgentLoop({
llm: new AnthropicProvider({ apiKey, model }),
session: new SQLiteSessionStore({ path: '~/.ethos/sessions.db' }),
memory: new MarkdownFileMemoryProvider({ dir: '~/.ethos' }),
personalities: new FilePersonalityRegistry({ dir: '~/.ethos/personalities' }),
tools: new DefaultToolRegistry(),
hooks: new DefaultHookRegistry(),
})

To use a different LLM, session store, or memory backend — implement the interface and inject it. Nothing else changes.

Extension points​

Every interface below is in @ethosagent/types (zero dependencies; safe to depend on from anywhere).

InterfaceDefault implementationSwap to
LLMProviderAnthropicProvider, OpenAICompatProviderAny HTTP-based LLM
SessionStoreSQLiteSessionStoreRedis, Postgres, in-memory
MemoryProviderMarkdownFileMemoryProviderVector store, database
PersonalityRegistryFilePersonalityRegistryRemote registry
ToolRegistryDefaultToolRegistryCustom filtering / routing
HookRegistryDefaultHookRegistryCustom hook execution
PlatformAdapterCLI readlineTelegram, Discord, Slack, WhatsApp, Email
PluginLoaderDefaultPluginLoaderPlugins register SQLite data sources, widget templates (widgets.yaml), and slash commands
SkillEvolver@ethosagent/skill-evolverAnalyzes eval output and proposes skill improvements

What a personality changes​

A personality lives at ~/.ethos/personalities/<id>/ — three files (SOUL.md, config.yaml, toolset.yaml). Switching personalities atomically changes:

  • System prompt (from SOUL.md)
  • Tool access (from toolset.yaml)
  • Memory scope (personality:<id>, derived from the personality id — not a setting)
  • Model (the model role or modelRegistry alias in config.yaml, as resolveTurnModel resolves it)

The mental model is: a personality is a role-bound configuration of the agent, not a prompt string. The researcher and the engineer are not the same agent in different costumes — they have different tools, different memories, different models. The next page explains why that matters.

Newcomers usually go from here in this order:

  1. Why is personality the unit? — the headline thesis
  2. Why Ethos? — honest comparison to LangChain, CrewAI, OpenClaw, Hermes
  3. Use Ethos: quickstart — install, talk to the agent, switch personalities
  4. Use the web dashboard — personalities, memory, cron jobs, and MCP from the browser

See also​