Pluggable durable agent runtime — long-running LLM agents with checkpoint/resume, CLI tool execution, and markdown skill definitions.
An autonomous agent that can run for hours, survive crashes, and resume exactly where it left off. No database, no message queue, no Kubernetes — just a binary, a config file, and a directory of checkpoints.
agent-loop run research --task "Research the history of TypeScript"
The agent searches the web, reads pages, synthesizes findings, and writes a report — autonomously. If the process crashes mid-run, agent-loop resume <session-id> picks up from the last checkpoint.
- Durable checkpointing — JSONL transcript + atomic checkpoint snapshots. Survives crashes.
- CLI tool execution — Any command-line tool becomes an agent tool. Secure:
execFile(no shell), command whitelist, timeout enforcement. - Markdown skills — Define agent capabilities in markdown files with YAML frontmatter. No code required.
- Context compaction — Dual-threshold auto-summarization (70% soft, 90% hard) enables runs that exceed a single context window.
- Session management — List, inspect, tail, and resume sessions.
- Configurable prompts — All system prompts, nudges, and thresholds live in
config.prompts.default.yaml; override any subset per model config. No prompt strings baked into code. - Sub-process orchestration — A skill can dispatch other agent-loop runs as named sub-processes and introspect/resume them via the
check_process/resume_processbuilt-ins, addressing them by name (never by session UUID). - Web UIs — An audit inspector (raw transcripts, SSE live tail) and a clean end-user console (ChatGPT-style, condensed progress) — see below.
# Install dependencies
bun install
# Set your API key
echo "ANTHROPIC_API_KEY=sk-ant-..." > .env
# Run the research agent
bun run src/cli.ts run research --task "Research how Temporal handles durable execution"
# In another terminal, watch the session
bun run src/cli.ts tail <session-id>| Command | Description |
|---|---|
agent-loop run <skill> [--task "..."] |
Start a new agent session |
agent-loop resume <session-id> |
Resume a paused or crashed session |
agent-loop list |
List all sessions with status |
agent-loop status <session-id> |
Show detailed session info |
agent-loop tail <session-id> |
Stream transcript in real-time |
agent-loop inspect <session-id> |
Show latest checkpoint contents |
agent-loop audit [--port N] |
Launch the audit web UI |
agent-loop console [--port N] |
Launch the end-user console web UI |
A built-in inspector for live and historical sessions. Browse the session list, watch transcripts stream in real-time (SSE), expand collapsible request/response payloads, search across the timeline, and start / stop / resume / delete sessions from the browser.
bun run src/cli.ts audit # default port 3900
bun run src/cli.ts audit --port 4000 # custom port
bun run src/cli.ts audit --config config.spark.yaml # different sessions dirThen open http://localhost:3900 in your browser.
The --config flag controls only which sessions/ directory the UI
reads from (via persistence.dir); the audit server itself doesn't
call any LLMs. Sessions appear as soon as they're created on disk, and
the UI groups sub-process sessions under their parent so multi-stage
orchestrations show as an expandable tree.
The audit server is a pull-based observer — your agent-loop sessions do not need to know about it. They write to disk; the UI tails the files. You can launch the UI before or after starting any session, and you can leave it off entirely without affecting agent runs.
A clean, end-user-facing front-end (ChatGPT-style) for dispatching agent runs and watching condensed progress. Distinct from the audit UI: it hides the raw payloads and shows one-line status updates instead. Runs as its own instance on its own port (default 3901), so it can run alongside the audit UI.
bun run src/cli.ts console # default port 3901
bun run src/cli.ts console --config config.spark.yaml # choose sessions dir + dropdown defaultsThen open http://localhost:3901.
- New session — enter a prompt, pick an agent (skill) and a config from two dropdowns, and submit. Each session is one prompt dispatched to one agent (no multi-turn follow-ups).
- Session list (left) shows only top-level sessions; dispatched sub-processes stay hidden.
- Progress log is collapsed by default — folded it shows just the running agent and the latest update; expanded it shows a condensed, one-line-per-event stream (tool calls, results, status changes) rather than raw request/response payloads.
- The final answer (the agent's last text) is rendered when the run completes.
It shares the same JSON/SSE API as the audit server and is likewise a pull-based observer of the on-disk session files.
Add a .md file to skills/:
---
name: my-skill
description: What this skill does
tools:
- name: search
description: Search the web
command: curl
args: ["-s", "https://api.example.com/q=${query}"]
schema:
query: { type: "string", description: "Search query" }
timeout: 30
idempotent: true
---
## Instructions
Tell the agent what to do with these tools.Then run it: bun run src/cli.ts run my-skill --task "Do the thing"
| Field | Description |
|---|---|
name |
Tool name the LLM calls |
description |
What the tool does (shown to the LLM) |
command |
CLI command (must be in allowedCommands config) |
args |
Argument array with ${param} placeholders |
schema |
Parameter definitions (type + description) |
timeout |
Seconds before the tool is killed |
idempotent |
Whether re-execution on crash recovery is safe |
Edit config.yaml:
model:
provider: anthropic
model: claude-sonnet-4-6
maxTokens: 8192
temperature: 0.2 # Optional — only sent to the provider when set
session:
maxContext: 200000 # Tokens before compaction triggers
checkpointInterval: 5 # Checkpoint every N iterations
timeout: 21600 # Max run time in seconds (6 hours)
tools:
cli:
allowedCommands: # Whitelist of CLI commands
- curl
- jq
- grep
- python3
timeout: 120 # Default tool timeout (seconds)
# Optional — override prompts/thresholds (see Prompts & thresholds below)
prompts: ./config.prompts.mymodel.yamlAll system prompts, nudge messages, context markers, and loop/compaction
thresholds live in config.prompts.default.yaml, which is always loaded
at startup — there are no prompt strings baked into the code. A model
config can override any subset via the prompts: key, pointing at a YAML
file (path, relative to the config) or an inline object. Missing keys
fall through to the defaults; if a required key is missing from both the
defaults and any override, agent-loop fails fast at startup.
# Inline override of just the thresholds you care about
prompts:
thresholds:
compaction_soft_ratio: 0.6
loop_match_length: 4This lets you tune prompts and thresholds per model without forking code.
Any OpenAI-compatible API works. For LM Studio:
# config.lmstudio.yaml
model:
provider: lmstudio
model: qwen/qwen3-next-80b
maxTokens: 8192
baseUrl: http://localhost:1234/v1
session:
maxContext: 32000 # Adjust to your model's context windowbun run src/cli.ts run research --task "Research topic" --config config.lmstudio.yamlSupported providers: anthropic, lmstudio, openai-compat (any OpenAI-compatible endpoint).
src/
├── core/
│ ├── loop.ts # Agentic loop + signal handling
│ ├── session.ts # State machine, token tracking, checkpointing
│ ├── compaction.ts # Dual-threshold context compaction
│ ├── context-trim.ts # Per-tool context trimming (preserve/keepLast)
│ └── prompts.ts # Loads config.prompts.default.yaml + overrides
├── tools/
│ ├── cli.ts # CLI tool executor with security model
│ └── builtin.ts # Built-in tools: check_process / resume_process
├── skills/
│ └── loader.ts # Markdown skill parser
├── persistence/
│ └── file-store.ts # JSONL transcript, atomic checkpoints, file locking
├── providers/
│ ├── anthropic.ts # Thin @anthropic-ai/sdk wrapper
│ └── openai-compat.ts # OpenAI-compatible endpoints (LM Studio, etc.)
├── audit/
│ └── server.ts # Audit web UI server (shared JSON/SSE API)
├── console/
│ └── server.ts # End-user console web UI server
├── types.ts # Shared type definitions
└── cli.ts # Commander-based CLI
Core loop: model call → tool execution → checkpoint → repeat until the model returns text with no tool calls.
Durability: Every message is fsync'd to a JSONL transcript. Checkpoints (full state snapshots) are written every N iterations via atomic rename. On crash, resume replays transcript entries after the last checkpoint.
Security: CLI tools run via execFile (no shell interpolation). Commands must be in the whitelist. Arguments are passed as arrays. Template placeholders are expanded with URL encoding.
bun test # 127 tests — unit + integration
bun test --watch # Watch modeMIT