Skip to content

Repository files navigation

agent-loop

Pluggable durable agent runtime — long-running LLM agents with checkpoint/resume, CLI tool execution, and markdown skill definitions.

What is this?

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.

Features

  • 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_process built-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.

Quick Start

# 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>

CLI Commands

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

Audit 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 dir

Then 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.

Console Web UI

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 defaults

Then 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.

Creating Skills

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"

Tool Definition

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

Configuration

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.yaml

Prompts & thresholds

All 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: 4

This lets you tune prompts and thresholds per model without forking code.

Using a Local LLM (LM Studio, Ollama, etc.)

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 window
bun run src/cli.ts run research --task "Research topic" --config config.lmstudio.yaml

Supported providers: anthropic, lmstudio, openai-compat (any OpenAI-compatible endpoint).

Architecture

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.

Testing

bun test              # 127 tests — unit + integration
bun test --watch      # Watch mode

License

MIT

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages