- Go 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github/workflows | ||
| agent | ||
| attachment | ||
| channel | ||
| config | ||
| conversation | ||
| creds | ||
| docs | ||
| event | ||
| input | ||
| internal/agentdocs | ||
| jsonutil | ||
| llm | ||
| rendering | ||
| security | ||
| server | ||
| skills | ||
| tools | ||
| .gitattributes | ||
| .gitignore | ||
| cli_active_input_test.go | ||
| cliui.go | ||
| config_cli.go | ||
| config_cli_test.go | ||
| CURRENT_STAGE_TASKS.md | ||
| example.config.toml | ||
| go.mod | ||
| go.sum | ||
| im_commands.go | ||
| im_commands_test.go | ||
| im_failed_inbox.go | ||
| im_failed_inbox_test.go | ||
| im_input.go | ||
| im_input_test.go | ||
| im_pending_input_test.go | ||
| im_progress.go | ||
| im_progress_test.go | ||
| im_runtime.go | ||
| im_runtime_test.go | ||
| im_session_commands.go | ||
| im_session_commands_test.go | ||
| im_sessions.go | ||
| im_sessions_test.go | ||
| init_cli.go | ||
| INTERACTIVE_CLI_TASKS.md | ||
| LICENSE | ||
| main.go | ||
| main_test.go | ||
| memory_cli.go | ||
| memory_cli_test.go | ||
| readline.go | ||
| readline_test.go | ||
| README.md | ||
| SECURITY_BASELINE_TASKS.md | ||
AgeAge
A self-hosted AI agent framework in Go. Wire any OpenAI-compatible LLM to a tool loop, teach it custom workflows with Markdown skill files, and reach it from a terminal, a chat room, or your own client — all from a single binary with a single config file.
Why AgeAge
Most AI agent frameworks are heavy, unpredictable black boxes that blindly consume tokens and require massive Python environments. AgeAge takes a different approach: maximum token efficiency, strict predictability, and zero deployment friction.
Core Philosophy
1. Context Economy & Token Efficiency — AgeAge aggressively protects your context window. An intelligent Router intercepts user intents, directing simple queries to cheap models and reserving strong models for complex reasoning. Deep tasks are offloaded to isolated sub-agents via delegation, keeping the main session clean and strictly scoped.
2. Semi-Automated Predictability (Pipelines) — LLMs are inherently non-deterministic, making repetitive workflows fragile. AgeAge introduces YAML Pipelines as an escape hatch: chains of LLM-driven agent nodes and deterministic auto nodes (direct tool calls, no reasoning). You only invoke the AI when cognitive work is actually required, eliminating infinite loops and guaranteeing reproducibility.
3. Transparent Execution & Zero-Leak Security — In supervised mode, destructive actions pause for explicit confirmation. More importantly, your secrets never touch the cloud. Through an encrypted local vault and {{cred:name}} placeholders, the LLM requests authenticated actions without ever seeing the actual passwords or API keys.
4. First-Class Multi-Platform UX — Whether you are in the terminal (ageage cli) or a group chat (Telegram, Discord, Matrix), the experience is tailored. Multi-session management works out of the box — every Matrix thread automatically spawns an isolated, persistent agent session. The CLI features live reasoning streams, inline file attachments (@file), and conversational rollbacks (/undo).
5. Ultra-Lightweight Single Binary — Written in Go, AgeAge deploys as a single standalone executable. It consumes less than 10 MB of RAM at idle, starts instantly, and requires zero heavy dependencies to run the core engine.
Delivery modes
| Mode | Command | Description |
|---|---|---|
| Interactive CLI | ageage cli |
Development, debugging, power-user tasks. /session, /clear, /build, /retry, @file attachments. |
| IM channels | ageage connect |
Telegram, Discord, Matrix. Per-room session management, typing indicators, ⏳/✅ reactions, thread sessions. |
| HTTP API | ageage serve |
OpenAI-compatible /v1/chat/completions. Drop-in backend for SillyTavern, OpenWebUI, and similar clients. |
| MCP server | ageage mcp |
Expose all registered tools as MCP tools over stdio so other AI systems can call them. |
Quick start
# 1. Clone and build
git clone https://github.com/your-org/ageage
cd ageage
go build -o ageage .
# 2. Create a workspace with a starter config
./ageage init
# For pipes and automation, use line-oriented prompts and an explicit target:
./ageage init --plain --dir ./ageage
# or copy the fully commented example and fill it in:
cp example.config.toml workspace/config.toml
# 3. Start chatting
./ageage cli
Minimum config:
[llm]
api_key = "sk-..."
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini"
Full configuration reference: docs/config.md
Basic usage
After ./ageage init, start a session with:
./ageage cli # interactive terminal session
./ageage cli -c path/to/config.toml
./ageage config # staged keyboard configuration editor
./ageage config tools # edit the default or custom tool allowlist
./ageage config edit # open the resolved TOML file in $VISUAL / $EDITOR
./ageage config validate # validate config.toml (script friendly)
./ageage memory # interactive memory manager
./ageage memory list # script-friendly memory listing
./ageage memory search "project preference"
./ageage memory export ./memory-backup.jsonl
ageage tools is retained as a compatibility alias for ageage config tools. An empty agent.tools value means the default tool set; skill-only tools are added only when a skill requests them. A non-empty custom allowlist can make selected skill-only tools available globally, and cannot be empty. See the configuration reference for editor and non-terminal behavior.
Slash commands
| Command | What it does |
|---|---|
/help |
Show this command list |
/build [description] |
Ask the Planner to author a reusable skill or pipeline |
/clear |
Clear the current conversation (keeps the session) |
/stop |
Interrupt the running task |
/summarize |
Compress conversation history with an LLM summary |
/undo |
Remove the last user→assistant turn |
/retry [text] |
Re-run the last message, optionally with extra instructions |
/think |
Show the last captured reasoning think-block |
/skills |
List loaded skills (/name invokes one directly) |
/session or /s |
Session management (see below) |
Type exit or quit to leave the CLI.
Sessions
Each conversation lives in a session under .ageage/sessions/. Session history persists across runs — the same session can be resumed with /s switch.
/s List sessions
/s new [name] Create and switch to a new session
/s switch <name> Switch sessions (prefix matching supported)
/s rename <new> Rename the current session
/s delete <name> Delete a session (moves it to the trash)
File attachments
Attach files inline with @path. Uploads provide metadata and a stable reference;
the agent decides when to call file_read to view an image or parse a document:
You ▸ Summarise @report.pdf and check @screenshot.png for issues
Other entry points
| Mode | Command |
|---|---|
| IM channels (Telegram/Discord/Matrix) | ./ageage connect |
| HTTP API (OpenAI-compatible) | ./ageage serve <data-dir> |
| MCP server over stdio | ./ageage mcp |
| Configure / inspect tools | ./ageage config, ./ageage config tools, ./ageage tools (compatibility alias) |
| Manage memory | ./ageage memory, ./ageage memory list, ./ageage memory search <query>, ./ageage memory export <path> |
| Inspect skills / credentials / scheduled tasks | ./ageage skills, ./ageage cred, ./ageage cron |
In IM mode, Matrix AgeAge commands use ! (for example !help and !session new); Telegram and Discord commands continue to use /. IM progress visibility is configured with [notifications], including per-channel overrides. See the channel configuration for details.
IM conversations share a target/surface abstraction for replies, topics, status
cards, questions and notifications. Matrix !session new [name] creates a named
thread root; session lists include previews, turn counts and links. Existing
threads keep their session when navigating with session switch. See the
IM conversation architecture for behavior and fallbacks.
Architecture overview
flowchart TD
subgraph entry [Entry Points]
direction LR
cli[CLI]
http[HTTP API]
im[IM Channels]
mcp[MCP Server]
end
fac[AgentFactory]
subgraph agt [Per-Request Agent]
rtr[Router] --> pln[Planner] --> loop[Execution Loop]
loop --> sum[Summarizer]
loop -.->|background| ev[Evaluator]
end
tr[(Tool Registry)]
lm[(LLM Client)]
sk[(Skills)]
entry --> fac
fac -->|CreateAgent| agt
agt --- tr
agt --- lm
agt --- sk
Key components
AgentFactory is the single initialization point. It loads config, establishes the LLM client, compiles skills, and wires up all shared resources (cron store, MCP sessions, security checker, credential manager). Every agent instance comes from the factory and shares underlying config without duplicating state. A skill watcher goroutine polls the skills directory every 2 seconds and hot-reloads .md and .yaml files on any change.
Agent is the conversation engine. It holds the full message history, the tool registry, the router, and the summarizer. One instance persists across turns; history grows with each exchange and is compressed or summarized when it exceeds configurable thresholds.
Router is an optional lightweight classifier that runs before each turn. It answers three factual yes/no questions about the request (needs_tools, needs_multiple_calls, needs_synthesis) and lists the required tools. The engine derives the model tier (base/medium/strong) from the checklist — eliminating semantic bias from manual labelling. The router also selects the active skill.
Planner & Evaluator work together for complex tasks with no existing skill. The Planner runs a sandboxed agent to author a new .md or .yaml skill file. Afterwards, the Evaluator runs asynchronously in the background to review the execution and potentially patch the generated skill. The /build command also triggers the Planner explicitly, passing recent conversation context.
Tool Registry is a simple map with thread-safe register/execute methods. Global tools are always available; skill-only tools (grep, glob, browser_*, update_todos, escalate) are injected at the start of a matched turn and removed when it returns.
Request flow (one user turn)
User message
│
▼
① Skill matching — word-boundary name match against loaded skills
② Skill-only tool injection — add declared tools; remove on turn exit (defer)
③ Router (optional) — answer 3 checklist questions; derive model tier; select tools & model
④ Planner (optional) — if tier=strong and no skill exists, auto-generate one
⑤ Model & tool selection
base → [llm] model, no delegate
medium → [router.medium] model if configured
strong → [router.strong] model, delegate injected
⑥ Execution loop (up to max_iterations)
LLM call → tool calls → LLM call → …
<think> blocks stripped from streamed output
Budget pressure compacts older tool turns
finish_task(status, summary) called → save history → return
[Guard] finish_task(status="success") blocked if any todo is still pending
⑦ Post-turn summarization & Evaluator
Background summarization (optional)
Background evaluator for auto-generated skills
Token efficiency
| Strategy | Effect |
|---|---|
| KV cache stability | Dynamic time, todos and session notes stay in a trailing context block. Raw history remains unchanged below the soft budget; skills and configured directives can intentionally change the prefix. |
| In-place compression | Under budget pressure, older tool exchanges become compact narratives while at least the latest 2 stay raw. Full-output references remain readable. |
| Tool result capping | Tool outputs in history are capped at 4 000 runes; the full result is used by the current iteration. |
| Conversation summarization | After a threshold, old pairs are replaced by an LLM-generated summary. |
| Router model tiering | Simple tasks stay on the base model; only complex multi-step tasks escalate to the strong model. The router itself runs on a cheap classifier model. |
Skills
Skills live in {workspace}/skills/. Any .md file with valid frontmatter is picked up automatically.
---
name: code-review
description: "Review code for bugs, style, and security."
tier: medium
required_tools:
- file_read
- grep
---
Read the given file(s) and return a structured review with severity ratings.
| Frontmatter field | Purpose |
|---|---|
name |
Word-boundary matched against user input (case-insensitive, spaces/underscores/hyphens equivalent). |
description |
Shown to the router for skill selection. |
tier |
base / medium / strong — bypasses the router LLM call when set. |
required_tools |
Tool allowlist for this turn. |
Model tiers
| Value | Model used | Delegate tool |
|---|---|---|
base |
Base [llm] model |
No |
medium |
[router.medium] model (if set) |
No |
strong |
[router.strong] model (if set) |
Yes |
Multiple skills can match simultaneously. Their tool lists merge and the highest tier wins. Skills are hot-reloaded — the change applies within 2 seconds.
Pipeline skills
Skills can define structured multi-step pipelines in YAML — chains of sub-agent nodes, direct tool calls, and foreach loops — instead of a single prompt body. See docs/skills.md for the full pipeline reference.
/build command
Type /build [description] to explicitly trigger the Planner from any mode. The Planner receives the last 8 turns of conversation as context and authors a new skill or pipeline file — without adding any of the build process to the main conversation history.
/build create a daily GitHub notification summarizer
Sub-agents and delegation
The delegate global tool and escalate skill-only tool each spawn an isolated sub-agent with its own registry and iteration budget. Sub-agents do not run the router and do not inherit personality files, giving clean isolation for complex tasks.
delegate uses [subagent.model]; escalate always uses [router.strong].
Multimodal attachments
CLI — attach files inline with @path syntax:
You ▸ Summarise @report.pdf and check @screenshot.png for issues
HTTP API — multimodal content arrays in request bodies are forwarded directly to the agent.
Matrix, Telegram, Discord — send files with or without a caption. Matrix
m.image and m.file, Telegram photos and documents, and Discord attachments
use the same staging pipeline as CLI files. Uploads supply filename, MIME, size
and an opaque attachment:att-… reference; file contents are not injected into
the model automatically. The agent chooses whether and when to use file_read
with that reference. A reference may also be passed in a delegated task.
Matrix preserves admission order for separate text and attachment events without waiting for the active run to finish. In group rooms, an explicit bot mention in a text message allows follow-up attachments from the same sender in the same conversation for 15 seconds. Existing thread routing remains supported.
multimodal.max_attachment_bytes limits each CLI/IM file to 10 MiB by default;
multimodal.max_turn_attachment_bytes limits the total successfully downloaded
bytes in one input to 40 MiB by default. At most 16 files are accepted per input.
Bodies are streamed into private files under the config directory's
data/tmp/staged/; original files remain available while history references them.
References are retained through history summarization and can be restored after
restart. Clearing history reclaims managed originals.
file_read reads text by line (up to 500 lines per call), parses documents with
the existing [multimodal.converters] settings, or returns images as vision parts
when multimodal.vision = true. Parsed document text is capped at 512 KiB and
cached by content and converter settings. Converters run for at most 60 seconds;
an output file exceeding the 1 MiB hard limit terminates conversion. Images with
vision disabled can use a configured text converter; otherwise reading reports
an explicit error. Upload/download failures are reported immediately, while
unsupported formats and conversion failures are reported when the agent reads.
Matrix audio/video and encrypted attachments remain unsupported.
The runtime returns a normalized agent.UserInput without open download streams.
New turns, active-run steering, and scoped ask_user answers all carry the same
attachment references. A successful image read appends a framework-generated
multimodal message after the entire tool-result batch, preserving provider message
ordering. These resource messages do not count as external user turns. The MCP
server also preserves image results as native MCP image content.
LLM compatibility
llm.Client wraps any OpenAI-compatible endpoint.
Google Gemini — when base_url contains generativelanguage.googleapis.com, a compatibility layer:
- Merges multiple system messages into one (Gemini requires exactly one)
- Merges consecutive same-role plain-text messages
- Strips
reasoning_contentfrom outbound messages - Preserves
thought_signatureon tool calls for Gemini's verification round-trip
Security model
blocked_commands— substring blocklist on everybashinvocationallowed_roots/forbidden_roots— path allowlist/denylist for all file tools; symlinks are fully resolved before checking- Supervised mode — destructive tools pause for
y / n / aconfirmation;bash.auto_allow_commandsdefines prefix patterns that skip confirmation in that directory credentials.toml— unconditionally blocked from all file tools regardless of config; cannot be overridden- IM group chats — bot only responds when @mentioned or replied to;
allowed_usersmust be configured or group messages are denied - Scoped interaction replies — confirmations and
ask_useranswers are bound to their channel, thread, session, and sender instead of being selected by arrival order - Network egress — web and browser tools reject local, private, link-local, multicast, and cloud-metadata targets by default and recheck native HTTP redirects
- Private memory storage — memory files use mode
0600; recall output is bounded and updates are synchronized and atomically replaced
Security migration notes
- Existing local API setups remain unauthenticated by default and continue to bind to
127.0.0.1. Before exposing the API beyond localhost, configureserver.api_key, explicitserver.cors_origins, request limits, and optionallyserver.health_auth. - Private web endpoints that previously worked are now blocked. Enable
allow_privateonly for the specific web or browser tool that needs trusted internal access, or useallowed_domainsto narrow its destinations. - IM approvals now require the same conversation scope and an authorized sender. Integrations that inject replies directly should use the scoped confirmation and user-input APIs.
- Tool extensions may implement
MetadataProviderto declare execution properties. Extensions without metadata remain compatible but are conservatively treated as side-effecting and medium risk.
See docs/config.md for the server and network settings, and docs/tools.md for tool behavior.
Credential system
Named secrets are stored AES-256-GCM encrypted. The master key lives at os.UserConfigDir()/ageage/master.key — separate from the workspace so the credential file cannot be decrypted even if the workspace is leaked.
ageage cred add <name> # prompt for value (no terminal echo)
ageage cred list # list names (never values)
ageage cred remove <name>
The agent uses {{cred:name}} placeholders in tool arguments. Values are substituted in-memory just before execution and scrubbed from all results before they enter conversation history.
Configuration quick-start
[llm]
api_key = "sk-..."
base_url = "https://api.openai.com/v1"
model = "gpt-4o-mini"
[agent]
max_iterations = 20
mode = "supervised" # "full" skips all confirmations
[router]
enabled = true
[router.classifier] # Fast cheap model for classification
model = "gemini-2.0-flash"
api_key = "AIza..."
base_url = "https://generativelanguage.googleapis.com/v1beta/openai/"
[router.strong] # Best model for complex/workflow tasks
model = "gpt-4o"
[eval] # Auto-generated skill quality checker
success_threshold = 3
[pipeline] # Pipeline execution overrides
foreach_concurrency = 4
Run ageage init for an interactive setup wizard.
Full reference: docs/config.md · docs/skills.md · docs/tools.md
Directory layout
ageage/
├── agent/ # Agent loop, factory, router, planner, evaluator, sessions
├── channel/ # IM connectors (Telegram, Discord, Matrix)
├── config/ # TOML config structs and loader
├── creds/ # AES-256-GCM credential store
├── docs/ # config.md, skills.md, tools.md, pipeline.md
├── llm/ # OpenAI-compatible HTTP client + Gemini compatibility layer
├── security/ # Command and path safety checker
├── server/ # HTTP API (OpenAI-compatible) and MCP server
├── skills/ # Skill loader and hot-reload watcher
├── tools/ # All tool implementations
├── cliui.go # Lipgloss CLI UI
└── main.go # CLI entrypoint (cobra)
Development
Run the local verification suite before submitting changes:
gofmt -w $(git ls-files '*.go')
go vet ./...
go test ./...
go test -race ./...
go build ./...
The GitHub Actions Test workflow runs formatting checks, vet, race-enabled tests, and
the build automatically for pushes to public and for pull requests. Tests use
temporary directories and local httptest servers; they do not require API keys,
model access, a browser installation, or public network access.
Dependencies
| Package | Purpose |
|---|---|
github.com/BurntSushi/toml |
Config file parsing |
github.com/PuerkitoBio/goquery |
HTML extraction fallback |
codeberg.org/readeck/go-readability/v2 |
Mozilla Readability content extraction |
github.com/modelcontextprotocol/go-sdk |
MCP client and server |
github.com/playwright-community/playwright-go |
Browser automation |
github.com/charmbracelet/lipgloss |
Terminal UI styling |
github.com/spf13/cobra |
CLI command structure |
gopkg.in/yaml.v3 |
Skill frontmatter parsing |
License
Context integrity and cache telemetry
Generated history summaries persist across turns and reloads. Compression checks
run between complete tool batches, including within long single tasks. The shared
soft input budget defaults to summarize.max_context_tokens = 24000; exact token
counts depend on the provider. Model summarization remains opt-in via
summarize.enabled.
Long tool outputs are saved in full and surfaced through recoverable references.
file_read supports line ranges and byte pages (start_byte, max_bytes) for
large logs and single-line JSON. Session notes use the active session's exact
path and refresh at each call without changing the stored prompt prefix.
CLI usage includes cache-read tokens and an observed-request hit ratio when the provider reports it, labels partial coverage, and shows unavailable telemetry explicitly. Run totals include synchronous router, summary and child-agent calls.