Architecture

A single binary, hexagonal design, zero SDK dependencies. Here's how it all fits together.

Layered architecture

Ports-and-adapters pattern. Each layer depends only on the interfaces below it.

 CLI Layer
main · run · chat · validate · init · config · TUI · REPL
 Engine
Session · Step · tool dispatch · auto-continue · structured output
LLM
Tools
MCP
Config
 Ports & Adapters
ConfigSource · Secrets (OS Keychain) · StateStore · MemoryStore

Engine loop

Every user message goes through the same cycle. The engine is provider-agnostic — it only sees events.

 User message
 Session.Step()
 Provider.Stream()
 Event Loop

EventText
stream to terminal

EventToolCall
collect calls

EventUsage
count tokens

EventDone
check stop reason

EventError
return error
end_turn
done, return
tool_use
execute → loop
max_tokens
continue → loop

Self-registering, zero coupling

Providers register via init() + llm.Register(). The engine calls llm.Resolve("provider/model") and gets back a Provider interface.

 llm.Resolve("provider/model")
Provider Registry
Anthropic
Custom SSE
OpenAI
OpenAI SSE
Ollama
NDJSON
Gemini
Compat
Alibaba
Compat
LiteLLM
Compat
All LLM clients are stdlib-onlynet/http, bufio, encoding/json. No SDK dependencies. Gemini, Alibaba, and LiteLLM use WithCompat() which disables OpenAI-specific features.

Tool dispatch & safety

Every tool call goes through the registry. File writes are gated by user confirmation. Undo reverts the last write.

 Model requests tool call
 tools.Registry.Run()
Built-in Tools
shell
fs_read
fs_write
⚠️ confirm
fs_list
git
test_run
delegate
MCP tools
 Result → back to model as tool_result

Hub-and-spoke pattern

Orchestrator agents delegate to specialists. Spokes return structured JSON. Multiple delegations run in parallel.

 User request
 Hub Agent
Decomposes task, delegates, synthesizes
spoke-coder
own model + tools
spoke-reviewer
read-only
spoke-planner
plans only
Structured JSON Response
{ "answer": "...",
  "sources": [{ file, summary }],
  "confidence": "high",
  "caveats": ["..."] }
 Hub synthesizes with citations
[spoke-coder: main.go:10-25] ...

MCP integration flow

Agent references an MCP server by name. The manager spawns it, handshakes, discovers tools, and namespaces them.

mcp: [jira, confluence]
agent frontmatter
 mcp.Manager.Open()
Spawn process
stdio transport
JSON-RPC handshake
initialize + initialized
tools/list
discover tools
Namespaced Tools (via ToolAdapter)
jira__get_issue
jira__search
jira__add_comment
confluence__get_page
confluence__search
confluence__update_page
 Merged into tools.Registry alongside builtins

Provider-native JSON enforcement

When response_schema is set in frontmatter, the engine enforces valid JSON via each provider's native mechanism.

response_schema: { type: object, ... }
agent frontmatter
Anthropic
response-tool + forced tool_choice
OpenAI
json_schema strict mode
Ollama
format field
 Engine buffers full response (no streaming)
Extract "answer"
display to user
Store full JSON
hub sees structured data

How AgentCTL compares

FeatureAgentCTLCursorGitHub CopilotAiderContinue
InterfaceCLI / TUIIDE (VS Code fork)IDE pluginCLIIDE plugin
Agent definitionMarkdown filesBuilt-inBuilt-inConfig flagsJSON config
Multi-provider✓ 6 providers~ 3✗ OpenAI only✓ Many✓ Many
Local models✓ Ollama~ Limited
MCP support✓ Stdio
Sub-agents✓ Hub-and-spoke
Structured output✓ JSON schema
File write confirm✓ Always✓ Diff view~ Auto
Version control✓ Git built-in~ IDE git~ IDE git✓ Auto-commit
Binary size7.8 MB~500 MBPluginpip installPlugin
Dependencies✓ ZeroElectronVS CodePythonVS Code
PriceFree (+ API costs)$20/mo$10/moFree (+ API)Free (+ API)

Design decisions

Engine is provider-agnostic

No provider-specific code in the engine or tools. Swap backends without touching core logic.

Stdlib-only LLM clients

No SDK deps. Every HTTP call is plain net/http. Easy to debug, easy to fork.

Keys in the OS keychain

macOS Keychain or Linux libsecret. Never in config files. Never in plaintext.

MD files are truth

Agents, skills, tools, MCP servers — all defined in Markdown with YAML frontmatter.

Shell over native tools

K8s, Terraform, Helm accessed via shell — no client-go, no version coupling, binary stays small.

Ports for extensibility

ConfigSource, Secrets, StateStore interfaces. Swap implementations without engine changes.