Experimental local-first LLM agent runtime on Gleam/BEAM.
This repository is also a meta-experiment: can a coding agent build and evolve a practical agent runtime in mostly pure Gleam, with BEAM/JS externals kept at typed boundary modules only.
Current status: experimental, functional local-first runtime.
Implemented now:
- Gleam project skeleton
- root supervisor
- HTTP server via Mist + Wisp
GET /healthGET /ready- actor-backed HTTP session API
- SSE session/run event streams
- persisted event envelopes in storage backend
- SSE replay via
Last-Event-ID - JSON event-log endpoints for debugging
- scored notes-memory recall with auto-writeback + importance tags
- session memory debug/read endpoint
- dedicated run actor + run supervisor foundation
- run lifecycle routed through run actor with progress events
- run scratchpad + plan task state on runs
- tool call lifecycle routed through run actor with persisted events
- tool result application and assistant response loop through run actor
- local async built-in tool runtime with timeout + retry handling
- per-run provider fallback chain selection
- per-run tool policy overrides (allowed tools, capabilities, max risk)
- basic agent executor route for end-to-end local runs
- provider abstraction with
basic-local, OpenRouter, and OpenAI-compatible adapters - OpenRouter and OpenAI-compatible verify routes for live configuration checks
- true upstream incremental OpenRouter stream transport
- persisted
run.chunkevents for assistant output - persisted
memory.writeback_failedobservability events - env-based config loading
- domain error taxonomy
- typed IDs, message/session/run/tool-call models
- pure session/run/tool-call state transitions
- repository contracts for sessions/messages/runs/tool calls and event log
- local durable persistence adapter with schema versioning
- session actor transaction pipeline for atomic session/run/tool/event commits
- session supervisor + session manager runtime wiring
- in-memory event bus for live fan-out
- tests for config, transport, domain core, persistence, session runtime, HTTP API, tools, and event bus
- CI workflow
- Erlang/OTP 28+
- Gleam 1.15+
rebar3onPATH
gleam runDefault bind: http://localhost:4000.
Agent-only runtime for debugging:
cd agent && gleam runHealth check:
curl http://localhost:4000/healthOptional: create .env in project root. Runtime loads it at startup via glenvy before config parsing. Real process env still wins over .env values.
Example:
LUMMY_OPENROUTER_API_KEY=your_key_here
LUMMY_DEFAULT_PROVIDER_ID=openrouter
LUMMY_DEFAULT_MODEL_SELECTION=openai/gpt-4o-miniCopy-paste flow:
- create session
- append user message
- start
/api/v1/sessions/:id/agent-runs - watch SSE or inspect event-log/tool-calls/session snapshot
Full example doc:
docs/quickstart.md
Replay missed SSE events:
curl -N -H 'Last-Event-ID: 1' http://localhost:4000/api/v1/sessions/<session-id>/eventsRead persisted event backlog:
curl http://localhost:4000/api/v1/sessions/<session-id>/event-log
curl http://localhost:4000/api/v1/runs/<run-id>/event-log?after=1Inspect scored recalled memory for session:
curl http://localhost:4000/api/v1/sessions/<session-id>/memory
curl http://localhost:4000/api/v1/sessions/<session-id>/memory?query=gleam%20releases&limit=3Drive run lifecycle through run actor:
curl -X POST http://localhost:4000/api/v1/runs/<run-id>/transition \
-H 'content-type: application/json' \
-d '{"state":"loading_context","occurred_at":"2026-04-16T06:00:11Z"}'Drive tool call lifecycle through run actor:
curl -X POST http://localhost:4000/api/v1/runs/<run-id>/tool-calls \
-H 'content-type: application/json' \
-d '{"tool_call_id":"tool-call-1","tool_name":"calculator","input_json":"{\"expr\":\"1+1\"}","started_at":"2026-04-16T06:30:11Z"}'Apply tool result and resume model loop:
curl -X POST http://localhost:4000/api/v1/tool-calls/<tool-call-id>/apply-result \
-H 'content-type: application/json' \
-d '{"ended_at":"2026-04-16T06:30:12Z","output_json":"{\"result\":2}","message_id":"message-tool-1","content":"{\"result\":2}","created_at":"2026-04-16T06:30:12Z","resume_at":"2026-04-16T06:30:13Z"}'Complete run with assistant response:
curl -X POST http://localhost:4000/api/v1/runs/<run-id>/respond \
-H 'content-type: application/json' \
-d '{"message_id":"message-assistant-1","content":"The answer is 2","created_at":"2026-04-16T06:30:14Z","ended_at":"2026-04-16T06:30:15Z","input_tokens":10,"output_tokens":6,"total_tokens":16,"estimated_cost_micros":100}'Run higher-level basic agent path:
curl -X POST http://localhost:4000/api/v1/sessions/<session-id>/agent-runs \
-H 'content-type: application/json' \
-d '{"run_id":"run-1","trigger_message_id":"message-1","model_selection":"openai/gpt-4o-mini","provider_id":"openrouter","started_at":"2026-04-16T06:30:10Z"}'Run with provider fallback chain and tighter tool policy:
curl -X POST http://localhost:4000/api/v1/sessions/<session-id>/agent-runs \
-H 'content-type: application/json' \
-d '{"run_id":"run-2","trigger_message_id":"message-1","model_selection":"openai/gpt-4o-mini","provider_id":"openrouter","provider_fallback_ids":["basic-local"],"skill_ids":["research"],"started_at":"2026-04-16T06:31:10Z","allowed_tools":["calculator","notes_memory"],"allowed_tool_capabilities":["math","memory"],"max_tool_risk":"low"}'Verify OpenRouter wiring directly:
curl -X POST http://localhost:4000/api/v1/providers/openrouter/verify \
-H 'content-type: application/json' \
-d '{"model":"openai/gpt-4o-mini","prompt":"Say hello in five words."}'Verify OpenAI-compatible wiring directly:
curl -X POST http://localhost:4000/api/v1/providers/openai-compatible/verify \
-H 'content-type: application/json' \
-d '{"model":"gpt-4o-mini","prompt":"Say hello in five words."}'UI source lives in ui/ and is served by backend at GET /.
Build development assets:
bash scripts/build-ui-dev.shThis inlines CSS imports only, keeps JS unminified, and skips asset hashing/compression.
Build production assets:
bash scripts/build-ui-prod.shThis runs the Lustre JS minifier, PostCSS normalization/autoprefixing/CSS minify, writes an asset version, and emits gzip/Brotli assets for production serving.
Then run server and open:
gleam run
# browse http://localhost:4000Notes:
- UI currently targets
openrouteronly - UI talks to same-origin backend routes, so no CORS setup needed
- backend still requires
LUMMY_OPENROUTER_API_KEY - UI opens live session SSE stream at
/api/v1/sessions/:id/events - typing
/opens slash commands for/newand discovered/skill:<name>entries - UI includes debug pane for HTTP actions, SSE status, and streamed events
- backend now logs request + run/provider/tool lifecycle to Erlang logger
| Variable | Default | Notes |
|---|---|---|
LUMMY_SERVICE_NAME |
lummy_agent |
Service label in responses |
LUMMY_ENV |
dev |
Environment label |
LUMMY_HOST |
localhost |
Mist bind target |
LUMMY_PORT |
4000 |
HTTP port |
LUMMY_DATABASE_PATH |
lummy_agent.db |
Local SQLite persistence file |
LUMMY_SECRET_KEY_BASE |
dev fallback | Must be at least 64 bytes |
LUMMY_DEFAULT_PROVIDER_ID |
basic-local |
Default provider id when request says default or leaves config-driven selection |
LUMMY_DEFAULT_PROVIDER_FALLBACK_IDS |
`` | Comma-separated fallback chain appended after per-run fallback ids |
LUMMY_DEFAULT_MODEL_SELECTION |
basic-local |
Default model name when request says default |
LUMMY_OPENROUTER_API_KEY |
`` | Required for provider id openrouter |
LUMMY_OPENROUTER_BASE_URL |
https://openrouter.ai/api/v1 |
OpenRouter API base URL |
LUMMY_OPENROUTER_SITE_URL |
http://localhost |
Optional HTTP-Referer header |
LUMMY_OPENROUTER_APP_NAME |
lummy_agent |
Optional X-Title header |
LUMMY_OPENROUTER_TIMEOUT_MS |
30000 |
HTTP timeout for OpenRouter requests |
LUMMY_OPENAI_COMPATIBLE_API_KEY |
`` | API key for OpenAI-compatible endpoint |
LUMMY_OPENAI_COMPATIBLE_BASE_URL |
https://api.openai.com/v1 |
Base URL for OpenAI-compatible provider |
LUMMY_OPENAI_COMPATIBLE_TIMEOUT_MS |
30000 |
HTTP timeout for OpenAI-compatible requests |
LUMMY_AGENT_SYSTEM_PROMPT |
built-in local-first prompt | Base system prompt for model requests |
LUMMY_AGENT_CONTEXT_MESSAGE_LIMIT |
12 |
Recent message count kept verbatim in prompt window |
LUMMY_AGENT_CONTEXT_SUMMARY_CHARS |
600 |
Max summary chars injected for older context |
LUMMY_AGENT_CONTEXT_SUMMARY_MODE |
model |
model or deterministic |
LUMMY_AGENT_CONTEXT_SUMMARY_INPUT_CHARS |
2400 |
Max chars fed into model-backed summarisation |
LUMMY_AGENT_MAX_STEPS |
4 |
Max agent loop turns per run |
LUMMY_AGENT_MAX_TOTAL_TOKENS |
16000 |
Total token budget across full run |
LUMMY_AGENT_MAX_COST_MICROS |
5000000 |
Estimated cost budget across full run |
LUMMY_AGENT_MAX_DURATION_MS |
60000 |
Wall-clock budget across full run |
LUMMY_AGENT_TOOL_TIMEOUT_MS |
5000 |
Per-tool timeout |
LUMMY_AGENT_TOOL_MAX_RETRIES |
1 |
Retry count for retryable tool failures |
LUMMY_AGENT_TOOL_FILE_ROOT |
. |
File tool sandbox root |
LUMMY_AGENT_TOOL_SHELL_WORKING_DIRECTORY |
. |
Working directory for shell_command |
LUMMY_AGENT_TOOL_NOTES_PATH |
.lummy_agent_notes.md |
Durable notes-memory file |
LUMMY_WEB_SEARCH_ENGINE |
tavily |
Web search engine: tavily, brave, or kagi |
LUMMY_TAVILY_API_KEY |
`` | Tavily API token for web_search (Authorization: Bearer ...) |
LUMMY_TAVILY_BASE_URL |
https://api.tavily.com |
Tavily API base URL |
LUMMY_TAVILY_SEARCH_COUNT |
10 |
Number of Tavily search results requested |
LUMMY_BRAVE_API_KEY |
`` | Brave Search API subscription token for web_search (X-Subscription-Token) |
LUMMY_BRAVE_BASE_URL |
https://api.search.brave.com/res/v1 |
Brave Search API base URL |
LUMMY_BRAVE_SEARCH_COUNT |
10 |
Number of Brave search results requested (clamped to 1-20) |
LUMMY_KAGI_API_KEY |
`` | Kagi API token for web_search (Authorization: Bot ...) |
LUMMY_KAGI_BASE_URL |
https://kagi.com/api/v0 |
Kagi API base URL |
LUMMY_KAGI_SEARCH_LIMIT |
10 |
Number of Kagi search results requested |
LUMMY_AGENT_ALLOWED_TOOLS |
calculator,web_fetch,web_search,file_read,file_write,shell_command,notes_memory |
Comma-separated tool allowlist |
LUMMY_AGENT_ALLOWED_TOOL_CAPABILITIES |
math,filesystem_read,network,external_api,memory |
Default capability allowlist; blocks file_write and shell_command by default |
LUMMY_AGENT_MAX_TOOL_RISK |
low |
Global tool risk ceiling |
LUMMY_AGENT_EXTERNAL_PROVIDER_MAX_TOOL_RISK |
low |
Extra risk ceiling applied when non-basic-local provider is in chain |
LUMMY_SKILLS_PATH |
./skills |
Directory of local Agent Skills packages (<name>/SKILL.md) used by explicit skill_ids |
LUMMY_AGENT_SKILL_DISCOVERY_PATH |
./skills |
Directory scanned for discovered slash/model-visible skills; relative paths resolve under LUMMY_AGENT_TOOL_FILE_ROOT |
LUMMY_MCP_SERVERS_PATH |
./mcp_servers.json |
Local MCP server config file |
gleam format --check src && gleam check
cd agent && gleam format --check src test && gleam check && gleam test
cd server && gleam format --check src test && gleam check && gleam test
cd shared && gleam format --check src test && gleam check && gleam test
cd ui && gleam format --check src test && gleam check && gleam testWindows + Zig note:
cd agent && bash ../scripts/with-zig-env.sh gleam testOr from cmd.exe / PowerShell in project root use bundled rebar3.cmd wrapper.
- Plan:
PLAN.md - Active implementation checklist:
TODO.md - Bootstrap notes:
docs/bootstrap.md - Domain core notes:
docs/domain-core.md - Persistence notes:
docs/persistence.md - Session runtime notes:
docs/session-runtime.md - Skills v1:
docs/skills.md - MCP v1:
docs/mcp.md - Quickstart:
docs/quickstart.md - HTTP API notes:
docs/http-api.md - Event stream notes:
docs/event-stream.md - Event log notes:
docs/event-log.md - Changelog:
CHANGELOG.md