Phase 1 + 2 — Core & TUI
adler

Eagle eyes on your agents. An orchestrator built for full observability into what your AI agents are doing, right now.

Daemon-centric Span-based tracing Live TUI dashboard Fully hackable

Full observability

Every agent run, log line, and context item traced in a structured span tree. Know exactly what happened and when.

Daemon-powered

A single background process owns all state. Agents survive terminal sessions. All clients get live push events.

Your stack

Any CLI agent, any workflow. One TypeScript config file. Hooks, plugins, and attach scripts — all customizable.

Architecture

One daemon, many clients

adlerd is the single authority over all sessions and agent processes. The CLI and TUI are thin clients — they send commands or subscribe to events over a Unix socket. Persistence is hidden behind a Storage interface, making the backend swappable.

COMPONENT MAP adler CLI adler TUI Unix socket adlerd daemon process SQLite Storage iface Agent Procs PTY
IPC — newline-delimited JSON
CLI → Daemon
{"type":"agent.run","id":"r1","payload":{...}}
Daemon → CLI
{"type":"response","id":"r1","payload":{...}}
Daemon → TUI (push stream)
{"type":"event","event":"agent.output","payload":{...}}
explorer
adler
packages
sdk @adler/sdk · types · storage · IPC · typed client
daemon adlerd · sole SQLite writer · PTY spawn
cli adler · thin CLI · auto-starts daemon
tui Ink dashboard · live push · no polling
plugins
opencode @adler/opencode · first-party plugin

Daemon lifecycle

socket ~/.local/share/adler/adler.sock — one global daemon for all projects
start CLI auto-spawns detached Bun process if socket not found; polls 100ms until ready
stop Graceful SIGTERM — waits for running agents. Auto-exits after 10 min inactivity.
Data Model

Spans and events, all the way down

Instead of separate tables for agents, workflow steps, and hooks, adler uses a single spans table for every unit of work. A kind field says what it is. Adding new kinds — workflow steps, hooks, assistant runs — costs nothing: no schema change, no migration.

Everything that happens is an event with an open-ended type string and a JSON payload. This is the OpenTelemetry model applied to agent orchestration.

sessions
idTEXT PK — UUID
statusactive | completed | archived
working_dirTEXT
created_atINTEGER — ms
No goal field — goal lives in context_items (type="goal")
spans
idTEXT PK — UUID
session_idFK → sessions
parent_idFK → spans (nullable)
kindagent | workflow | step | hook
nameTEXT
statuspending|running|done|failed
started_atINTEGER
finished_atINTEGER (nullable)
dataJSON — kind-specific fields
events
idINTEGER PK — auto
session_idFK → sessions
span_idFK → spans (nullable)
typeopen-ended string
dataJSON — event payload
timestampINTEGER — ms
agent.output · span.started · log.info · context.added · ...
context_items
idTEXT PK — UUID
session_idFK → sessions
typegoal | url | file | text | ...
labelTEXT (optional)
descriptionTEXT (optional)
valueJSON — type-specific payload
created_atINTEGER
Example span tree — parallel agents in a workflow step
◆ session abc123 — Implement payment feature
✓ span: workflow:payment [kind=workflow]
✓ span: step:research [kind=step]
✓ span: opencode:plan [kind=agent] · 2m 14s
⟳ span: step:implement [kind=step] · running
⟳ span: agent-1 [kind=agent] parallel
⟳ span: agent-2 [kind=agent] parallel
← Parallel spans: same parent_id, overlapping timestamps. Phase 3 adds workflow/step/hook automatically — zero schema change.

Span context propagation

Context flows through environment variables — the subprocess equivalent of W3C Trace Context over HTTP headers. When the daemon spawns an agent it injects the span context. If that agent calls adler agent run, the CLI reads ADLER_SPAN_ID from its own environment and passes it as the parent. Propagation works at any depth automatically.

ADLER_SESSION=session-abc123
ADLER_SPAN_ID=span-xyz789  ← this agent's span id
ADLER_SOCKET=~/.local/share/adler/adler.sock
ADLER_AGENT_PROMPT="Implement the payment module"
ADLER_CONTEXT=[{"type":"goal","value":{"text":"..."}}, ...]
TUI Dashboard

Live visibility into every session

Running adler with no arguments opens a live dashboard. The TUI holds an open socket connection to the daemon and re-renders on push events — no polling. Five tabs cover every view you need.

adler — ~/git/myapp
active · session: abc123 · ~/git/myapp
1: Overview
2: Context
3: Agents
4: Traces
5: Logs
status● active
dir~/git/myapp
idabc123
⟳ opencode:plan 0m 43s
✓ opencode:build 2m 14s
✗ opencode:lint exit 1
[goal] Implement payment feature
[url] docs stripe.com/docs
[file] spec ./payment-spec.md
[text] note Use Stripe Elements
goal
Implement payment feature no label
url — 2 items
https://stripe.com/docs docs · Stripe API reference
https://github.com/org/repo/issues/42 issue · Payment tracking issue
file — 1 item
./docs/payment-spec.md spec · Feature specification
text — 1 item
Use Stripe Elements, not custom card UI note
↑↓ navigate · enter attach (inline PTY) · o open external
⟳ opencode:plan 0m 43s
Create an implementation plan based on the spec...
enter → suspend TUI, stream live PTY  ·  o → agent.attach hook
✓ opencode:build 2m 14s · exit 0
Implement the payment module in src/payments/...
enter → replay stored PTY output
✗ opencode:lint 0m 12s · exit 1
Run linting on the codebase and fix all errors...
enter → replay stored PTY output
↑↓ navigate · enter expand/collapse
◆ session abc123 active
✓ opencode:build agent · 2m 14s
⟳ opencode:plan agent · 0m 43s
Creating implementation plan based on spec...
Reading stripe docs reference...
✗ opencode:lint agent · exit 1
ESLint: 3 errors found in src/payments/index.ts
filter: all  [i]nfo  [w]arn  [e]rror  ·  auto-scroll: on [f]
12:35:47 info agent.output opencode:plan — Reading stripe docs reference...
12:35:44 info span.started opencode:plan
12:34:01 error span.failed opencode:lint — exit 1
12:33:55 info span.finished opencode:build — exit 0
12:31:41 info span.started opencode:build
12:31:40 info context.added url [docs] — stripe.com/docs
Agent Execution

What happens when you run an agent

Every adler agent run follows the same flow. The daemon is the authority throughout — spawning, tracking, streaming output, and broadcasting to any connected TUI clients in real time.

1

CLI connects

SDK connects to socket. Auto-starts daemon if not running.

2

Span created

Daemon creates a span (kind=agent) in SQLite via Storage interface.

3

PTY spawn

Daemon calls Bun.spawn() with PTY. Injects ADLER_* env vars.

4

Output streams

Each stdout line → agent.output event in DB → pushed to TUI.

5

Span closes

On exit: span updated to done/failed, event emitted, TUI re-renders.

CLI

Control everything from the terminal

All commands are thin wrappers around SDK calls. Session context is resolved automatically from --session flag, ADLER_SESSION env, or .adler/.session file.

Sessions
adler Open TUI dashboard for current session
adler new [--goal "..."] Create session, write id to .adler/.session
adler session list List all sessions across all projects
adler init Scaffold .adler/adler.ts in current project
Agents
adler agent run --agent <type> <prompt> Spawn agent, returns span id
adler agent wait [--name <n>] Block until span is done or failed
adler agent status [--name <n>] Print current span status
adler agent read [--name <n>] Stream agent PTY output to stdout
adler agent list List all agent spans for session
Context
adler context add --type <t> <value> Add context item (with optional --label, --description)
adler context list List all context items for session
adler context get [--type t] [--label l] Filtered output — used in workflow step prompts
Daemon
adler daemon stop Graceful shutdown — waits for running agents
Configuration

Your stack, your config

adler is configured with a TypeScript file. Global config at ~/.config/adler/adler.ts, project config at .adler/adler.ts. Project config merges over global. Agents are just functions returning shell commands.

import type { AdlerConfig } from "@adler/sdk"

const config: AdlerConfig = {
  agent: {
    agents: {
      // Any CLI agent — return a shell command string
      opencode: ({ prompt, subagent }) =>
        `opencode run --agent ${subagent} "${prompt}"`,
    },
    // Invoked when pressing 'o' on an agent in the TUI
    attach: ({ agentId }) =>
      `tmux new-window "adler agent read --name ${agentId}"`,
  },
}

export default config
Roadmap

Four phases to a complete platform

Each phase produces a working, useful system. The span and event data model is designed so that Phases 3 and 4 add capabilities without touching the schema.

Phase 1 + 2 — Now

Core + TUI

The foundation. A global daemon managing sessions and agents. Full SQLite-backed span/event tracing. SDK, CLI, and a live Ink dashboard with five tabs — overview, context, agents, traces, and logs.

adlerd daemon @adler/sdk CLI Ink TUI SQLite PTY attach

Workflows, Hooks & Plugins

YAML or TypeScript workflow definitions with sequential and parallel steps. Before/after hooks for any adler event. npm plugin system for distributing agent configs — first-party plugin: @adler/opencode.

adler run Workflow engine Hooks @adler/opencode

adler Assistant

An AI agent with full awareness of the current session — all spans, events, and context. Ask it anything about the session, or let it auto-orchestrate: read state, decide what to run next, iterate until done.

adler assistant --auto mode Session-aware AI
Future

Web UI & Remote Sessions

A browser dashboard built on @adler/sdk — just another client. Remote daemon support via TCP socket allows connecting from anywhere. Multi-user sessions for team visibility.

Web dashboard Remote daemon Multi-user