Pony — In-process Agent Backend
When you need a self-contained agent that doesn't depend on Node, Bun, or a separate binary being installed on the host, you use Pony.
Previously, Avenor required an external agent process (like OpenCode or Codex) to handle the model loop. This created a dependency on the runtime environment of those external processes. Pony removes that boundary by running the model loop directly inside the Avenor binary.
Endpoint Configuration
The config file needs two top-level fields to connect to a model provider:
base_url— The full URL of an OpenAI-compatible endpoint. This works with Ollama, LM Studio, OpenRouter, vLLM, and anything that exposes a/v1/chat/completionsroute. The adapter appends/chat/completionsautomatically, sohttp://localhost:4000/v1andhttp://localhost:4000both work.api_key_env— The name of an environment variable that holds the API key. Pony reads it withos.Getenvat startup. It does not read from files or support path-based references likeos.environ/KEY_NAME.json{ "base_url": "http://localhost:11434/v1", "api_key_env": "OLLAMA_API_KEY" }Then set the variable before running:
bashexport OLLAMA_API_KEY="sk-..." avenor --backend pony --pony-config pony.json --agent mule --prompt "hello"For local endpoints that don't require authentication, set the var to any value:
bashexport OLLAMA_API_KEY="sk-no-auth"Or use the
AVENOR_PONY_API_KEYconvention from earlier smoke tests. The key point: an empty or unset variable sendsAuthorization: Bearerwith a trailing space, which some servers reject. Always set it to something, even a dummy value.
Agent Profiles
Pony is configured via a JSON file. Instead of a single global setting, you define named profiles. Each profile specifies its own model, system prompt, initial prompt, and which tools are allowed to run.
{
"base_url": "http://localhost:11434/v1",
"api_key_env": "OLLAMA_API_KEY",
"default_agent": "mule",
"profiles": {
"jockey": {
"model": "gpt-4o",
"system_prompt": "You are a jockey. Your job is to orchestrate other agents to complete complex tasks.",
"initial_prompt": "Break the incoming task into independent units. Spawn all mules up front with spawn_agents, then wait_for_done on each child to collect results.",
"tools": {
"local": false,
"orchestration": true
},
"max_tokens": 8192
},
"mule": {
"model": "llama3.1",
"system_prompt": "You are a mule. You perform mechanical tasks like reading and writing files.",
"initial_prompt": "You have file and shell tools. Do exactly what is asked — no scope creep, no extra features.",
"inject_agents_md": true,
"tools": {
"local": true,
"orchestration": false
}
}
}
}Prompt Lifecycle
Pony manages conversation history using four distinct layers of prompting. On session start, they assemble in this order:
- System Prompt: Defines the agent's core identity and rules. Sent with every API turn.
- AGENTS.md (if
inject_agents_md: true): Walks up from the working directory to findAGENTS.mdand appends it as a first-turn user message. This is how project conventions reach the agent. - Initial Prompt: A user message sent only on the first turn. Use this for run-level context that doesn't need to persist across every turn.
- Task Prompt: The actual instruction provided at invocation time via
--prompt,--prompt-file, or the control socket. This is appended as the most recent user message whenPromptis called.
On follow-up prompts (multi-turn within a single session), only the system prompt and the new task prompt are sent. The AGENTS.md and initial prompt are not re-injected.
Tool Surface
Tools are gated by profile configuration. A profile can have local tools, orchestration tools, both, or neither.
Local Tools
Enabled via "local": true. These allow the agent to interact with the local filesystem and shell. By default, additional skill directories are read-access only; write and edit operations remain restricted unless the directory is explicitly write-allowed.
file_read: Read file contents.file_write: Write new files.file_edit: Perform partial edits to existing files.glob: Match file patterns.grep: Search file contents.list_dir: List directory contents.shell: Execute commands directly. Prefercmdplusargsfor exact argv execution; legacycommandstrings support quote grouping but reject shell operators such as|,>,<,&&,||,$(), and backticks.
Orchestration Tools
Enabled via "orchestration": true. These allow a jockey agent to manage child agents:
spawn_agent: Create a new child agent session.spawn_agents: Create multiple child agent sessions in a single call for parallel execution.send_prompt: Send a follow-up message to a child.get_status: Check if a child is running or finished.wait_for_done: Block until a child agent completes its task.send_to_parent: From a child, ask the parent for clarification (emitschild.question).
When a child calls send_to_parent, the supervisor emits a child.question event to the parent subscription with this payload shape:
{
"event": "child.question",
"runtime_id": "<parent_runtime_id>",
"session_id": "<parent_session_id>",
"child_id": "<child_runtime_id>",
"message": "Which package should I use?",
"request_id": "cq_123"
}The parent should reply with send_prompt to child_id, and should include request_id to make duplicate reply handling idempotent.
This payload is defined as a shared runtime contract in internal/runtime/contracts.go (ChildQuestionPayload and EventChildQuestion) so other runtimes (including Mustang) can consume the same schema.
The Wire Format
Pony uses the OpenAI Chat Completions wire format. This means it is compatible with any provider that implements an OpenAI-compatible API, such as Ollama, LM Studio, OpenRouter, or vLLM.
To use a local endpoint like Ollama or LM Studio:
avenor --backend pony \
--pony-config pony.json \
--agent mule \
--prompt "Refactor the user module" \
--dir /repoSelect a different profile with --agent:
avenor --backend pony \
--pony-config pony.json \
--agent jockey \
--control-socket /tmp/avenor.sock \
--prompt "Review the codebase and fix lint errors" \
--dir /repoReasoning Blocks
When using models that support reasoning (like kimi-k2), Pony surfaces the internal thought process. Reasoning content is emitted as agent.thought_chunk events, allowing you to observe the agent's internal monologue before it produces tool calls or text.
Degenerate Stream Guard
Pony includes a defensive guard against degenerate reasoning streams. Some upstream providers emit internal control tokens (Llama 3.1+ channel markers like <|channel|> or tool markers like <tool_call|>) into the reasoning field, or stream encoded payloads as one character at a time. When this happens, the model is no longer making useful progress and the output budget would be consumed without producing a result.
The guard watches every agent.thought_chunk delta and aborts the session when one of three signals trips:
- Control-token leakage — a delta matches a known upstream control token (Llama 3.1 channels, Llama tool markers, end-of-text markers, etc., in canonical or truncated form). One occurrence is enough; legitimate reasoning never contains these sequences.
- Long hex run — 256 or more consecutive deltas are each a single hex/digit character.
- Window dominance — the trailing 2048-delta window is dominated by single hex deltas with no other progress (safety net for unusual patterns).
When the guard trips, the loop emits an avenor.error event with source: "loop", kind: "degenerate_reasoning_stream", the model name, and a diagnostics map (counters for the abort signal, observed total, hex totals, window state). The session ends with stop_reason: "degenerate_reasoning_stream", which surfaces into avenor.phase.end and avenor.team.end so failed team members are reported clearly while completed members' outputs are preserved.
The threshold constants live in internal/runtime/pony/degenerate.go and can be tuned if a new failure pattern emerges.
Boundaries
Pony is a lean execution loop. It does not currently handle:
- Vision/Images: It is a text-based backend.
- Prompt Caching: It does not manage provider-specific cache headers.
- Anthropic Native API: It currently only supports OpenAI-compatible adapters.
- Approval-Gated Tools: Tools run as soon as the model requests them.
- Multi-provider Fallback: If the specified endpoint fails, the session fails.
Roadmap
The following capabilities are planned:
- Skill Loading: Composable system prompts that load directly from
.skillfiles. - Parallel Subagents: Allowing a jockey to spawn multiple agents concurrently and collect their results via structured routing.