Skip to main content

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 assembles all components from config
├── LLMProvider AnthropicProvider | OpenAICompatProvider
├── SessionStore SQLiteSessionStore (WAL + FTS5)
├── MemoryProvider MarkdownFileMemoryProvider
└── PersonalityRegistry FilePersonalityRegistry (mtime hot-reload)


AgentLoop.run(text) AsyncGenerator<AgentEvent>
├── session_start hooks
├── MemoryProvider.prefetch() → system context
├── ContextInjector[] → system prompt assembly
├── before_prompt_build hooks
├── LLMProvider.complete() → stream chunks
│ ├── text_delta events
│ ├── tool_use_start / delta / end
│ └── usage event
├── ToolRegistry.executeParallel()
│ ├── before_tool_call hooks (arg override / rejection)
│ ├── parallel execution with budget splitting
│ └── after_tool_call hooks
├── MemoryProvider.sync()
└── agent_done hooks

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.

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

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 (from memoryScope in config.yaml)
  • Model (from model in config.yaml)

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

See also