KL

kevin0x5/langdriver

Productivity workflow
11 stars Quality 57 Trend 22MIT

A minimalist agent in ~500 lines of Ruby: ReAct loop + macOS Seatbelt sandbox + security approvals + MCP + Skills + persistent memory

Overview

LangDriver is a lightweight Agent runtime implemented in under 500 lines of Ruby, with ReAct, context compaction, persistent memory, Skills, MCP, Hooks, sandboxing, and permission approvals. Requires macOS and Ruby 3.0+ (uses Reline shipped with Ruby 3). The default config is already tuned for DeepSeek: - : use deepseek-v4-pro or deepseek-v4-flash. The compatibility names deepseek-chat / deepseek-reasoner will be retired on . - : DeepSeek v4 provides a 1M context window without an extra request field. Other providers may use different limits and field names; follow that provider's API documentation instead of copying DeepSeek values. - : declared via top-level reasoning_effort and thinking fields. LangDriver pipes the entire runtime.extra block into the request body verbatim. - : LangDriver uses the Chat Completions message/tool shape, but provider-specific models, fields, streaming details, and authentication may still require configuration or code changes.

README

LangDriver

中文

LangDriver is a lightweight Agent runtime implemented in under 500 lines of Ruby, with ReAct, context compaction, persistent memory, Skills, MCP, Hooks, sandboxing, and permission approvals.


Install

Requires macOS and Ruby 3.0+ (uses Reline shipped with Ruby 3).

git clone https://github.com/kevin0x5/langdriver && cd langdriver
ruby --version    # expect ruby 3.0+

Configure

1. API key

export DEEPSEEK_API_KEY="YOUR_DEEPSEEK_API_KEY"

2. langdriver.yaml

The default config is already tuned for DeepSeek:

runtime:
  model: deepseek-v4-pro                # or deepseek-v4-flash (cheaper / faster)
  compact_model: deepseek-v4-flash      # used for async chat compaction; falls back to model
  api_base: https://api.deepseek.com    # DeepSeek endpoint; other providers may require request-field changes
  api_key_env: DEEPSEEK_API_KEY
  max_steps: 50
  extra:                                # forwarded verbatim into chat/completions request body
    max_tokens: 384000                  # DeepSeek v4 single-call max output
    reasoning_effort: max               # recommended for complex agent tasks
    thinking: { type: enabled }         # chain-of-thought (DeepSeek-V3.2+ supports tool calls in thinking mode)

Notes:

  • Model names: use deepseek-v4-pro or deepseek-v4-flash. The compatibility names deepseek-chat / deepseek-reasoner will be retired on July 24, 2026.
  • Context limits: DeepSeek v4 provides a 1M context window without an extra request field. Other providers may use different limits and field names; follow that provider’s API documentation instead of copying DeepSeek values.
  • Max thinking effort: declared via top-level reasoning_effort and thinking fields. LangDriver pipes the entire runtime.extra block into the request body verbatim.
  • Switch provider: LangDriver uses the Chat Completions message/tool shape, but provider-specific models, fields, streaming details, and authentication may still require configuration or code changes. Remove unsupported DeepSeek-only fields such as thinking when appropriate.

3. Other declarative files (project root)

File Purpose
profile.md Assistant persona / tone (system prompt)
prompts.yaml Tunable prompt templates (system_suffix, compact) — delete to use built-in defaults
memory.md Long-term project facts; auto-injected into system prompt
summaries.md Auto-appended summaries (one block per 50 turns)
skills.yaml Per-skill enable / trust / priority
tools.yaml Permission ceiling for built-in file tools, external Skills, and the optional shell
hooks.yaml after_user_prompt / after_tool_use reminder text
mcp.yaml MCP server connections

First run

ruby langdriver.rb

In the REPL:

> read sample.txt and summarize it in one sentence

The model calls read, observes the content, and answers directly. No task-specific workflow is hard-coded; the model chooses each next step from context and tool results.

REPL commands (slash prefix; bare names also work):

/exit                  exit
/clear                 clear in-session chat (memory & summaries persist)
/memory                show current memory.md
/remember        append a fact to memory.md
/forget                wipe memory.md
/todos                 show current todo list
/skill list            list installed skills
/skill inspect   show effective permissions of a skill
/skill remove    uninstall
/mcp                   list MCP-provided tools

One-shot task (no REPL):

ruby langdriver.rb "count lines in sample.txt"

Artifacts: .langdriver/artifacts/result.md. Execution log: .langdriver/logs/run.jsonl. Todos: .langdriver/todos.json.


What problem does this solve

LangDriver has one design rule: keep the Agent runtime small. Task-level planning stays with the model; the runtime focuses on protocol, state, capabilities, and execution.

The result is a complete local Agent loop — ReAct, memory, Skills, MCP, Hooks, and permission-controlled execution — without a separate orchestration layer for each task type.

Model reasoning + tool calls + observations → next step or final answer
Design area LangDriver implementation Purpose
Planning The model emits standard tool_calls at each step No separate Planner layer
Memory Plain-text memory.md plus async summaries.md compaction Transparent, readable, and editable state
Prompts profile.md plus two optional templates in prompts.yaml Behavior stays outside runtime code
Routing JSON tool_calls dispatch directly to registered tools A short path from model decision to execution
Error recovery Tool results return to the same ReAct loop The next strategy is based on the actual observation

Tools the model can call

LangDriver follows the DeepSeek tool calls spec: standard tools[] schema, streamed tool_calls aggregation by index, paired role:tool messages with tool_call_id, and full reasoning_content plus tool-history replay in thinking mode. A response may contain multiple tool calls; the current executor runs them in order.

function what it does constraint
read read file path must be in tools.yaml read whitelist
search ripgrep / grep within read whitelist
write write file path must be in write whitelist
use_skill fetch a Skill’s SKILL.md informational
run_skill_script execute a Skill script external trust → Seatbelt sandbox
shell bash commands off by default; explicitly enabled in tools.yaml, independent of Skill trust, and run under a Seatbelt profile generated from the global read/write/network ceiling
todo persistent todo list (add / done / list / clear) stored in .langdriver/todos.json
mcp____ MCP tool call depends on the MCP server itself

Generated function-name separators use __ instead of .; MCP tool names must also satisfy the model API’s naming rules (^[a-zA-Z0-9_-]+$).


Persistent memory & async compaction

LangDriver keeps long-term context across sessions, without vector stores or embeddings — just text injection.

system prompt =
    profile.md
  + ## Memory             ← memory.md (auto-extracted entries are line-deduplicated)
  + ## Recent Summaries   ← last 10 blocks of summaries.md
  + (skill / mcp menu + tool-use guideline from prompts.yaml)

messages = [system] + last 50 turns + current user

How memory is populated:

  • /remember — explicit append
  • Auto-extraction during compaction (see below)
  • Manual edits to memory.md (plain markdown — gitignored by default; remove the gitignore entry if you want to share team-level facts via git)

Async compaction triggers every 50 completed turns:

  1. A background thread sends the oldest 50-turn slice to compact_model (default deepseek-v4-flash)
  2. Returns strict JSON {"summary": [...], "memories": [...]} per the compact template in prompts.yaml
  3. Summary is appended to summaries.md; memories are appended (line-deduped) to memory.md
  4. The next turn’s system prompt picks them up automatically
  5. Failure is retried up to twice, after 5s and then 10s; the final failure is logged to run.jsonl

This keeps raw active context bounded and supports long-running sessions through lossy summaries. Compaction is not exact recall: only the first 500 characters of each message are included in a compaction batch, and only the latest 10 summary blocks are injected.


Multi-turn chat protocol

Follows the DeepSeek multi-turn contract:

  • The latest 50 turns retain complete protocol history, including reasoning_content, assistant.tool_calls, and paired role:tool results
  • Uncompacted turns live in a separate buffer; each complete 50-turn batch is removed for compaction without relying on indexes into trimmed history
  • Once compacted, older raw turns are represented by summaries and long-term memory while the latest 50 remain verbatim

/clear wipes the in-session chat (memory & summaries are not affected).


Install a community Skill

ruby langdriver.rb skill install  

LangDriver clones and registers the Skill, then checks its requested write paths against tools.yaml. external Skill scripts run in Seatbelt; core Skill scripts run with host permissions.

ruby langdriver.rb skill inspect      # show effective permissions

Security model

External Skill permissions come from three intersecting layers:

  1. What the Skill declares (SKILL.md frontmatter): network: true, fs_read: ["~/Documents"]
  2. Your local override (skills.yaml): enablement, trust level, and narrower permissions
  3. The runtime ceiling (tools.yaml): allowed paths, environment variables, network, and command timeout

Effective permission is the intersection of all three. Network or out-of-bundle file access requires approval; an external Skill may not combine both.

Trust levels: core vs external

Level Behavior When
trust: core Skips the sandbox, no permission prompt Locally maintained, reviewed code that you intentionally allow to run with host permissions
trust: external Runs inside Seatbelt; network or out-of-bundle file access requires approval Community and other external Skills

core affects only that Skill’s scripts; it does not unlock the global shell. The shell must be explicitly enabled in tools.yaml and runs under its own generated profile. Core Skills and MCP servers are trusted integrations and should be reviewed before enabling.

Seatbelt sandbox (external Skills and explicitly enabled shell)

LangDriver gives external Skills and the explicitly enabled shell a dynamically generated deny-by-default profile with:

  • Runtime system paths needed by the interpreter
  • Reads and writes granted by the effective permission set
  • Network access only when the effective permission set enables it
(version 1)
(deny default)
(deny file-read* (subpath "/path/to/home"))
(allow file-read* (subpath "/usr/lib") (subpath "/bin") (subpath "/usr/bin"))
(allow file-read* (subpath "/path/you/authorized"))
(allow network*)              ; only present when the effective grant permits

Operations outside the generated profile are denied by Seatbelt at the syscall boundary.


Verify

ruby -c langdriver.rb
ruby test/test_langdriver.rb
ruby test/test_streaming.rb
test "$(wc -l < langdriver.rb)" -le 500

The suite covers thinking/tool-call stream aggregation and reasoning replay, complete protocol history, 50-turn compaction buffers, core/shell permission separation, Skill exit status, MCP environment isolation, and the 500-line hard limit. Actual Seatbelt syscall denial must be verified from a normal macOS process that is not already inside another sandbox.


License

MIT — see LICENSE.

View the full README on GitHub

Recommended Tools

Try a different keyword or remove a filter.

Install

npx skillfish add kevin0x5/langdriver
New to MCP? Start with the mcpskill.org guide →