How I Built a Go-Based Unified Agent Orchestration Framework Wrapping Claude Code, Codex, and Pi
The author describes creating a Go library called agent‑wrapper that unifies multiple coding agents—Claude Code, Codex, Pi, OpenCode—by providing a single registration interface, built‑in approval, budget control, context compression, session resume, and command‑line tools, turning disparate agents into a stable, production‑ready orchestration layer.
Background and Motivation
Earlier work experimented with langchain / langgraph in Go (project langgraphgo). Most community agents used langchain or Crew AI. A newer trend is to use coding agents such as Claude Code, Codex, OpenCode, and Pi directly, wrapping them to build higher‑level agents.
These coding agents follow a general‑purpose agent architecture, not just code generation, and providers supply SDKs for secondary development. Industry examples include Baidu’s “dodo” application and OpenClaw built on Pi, both created by quickly wrapping the underlying agents.
Problem: Repetitive Glue Code
Each CLI‑based coding agent requires separate glue code for:
Launching a subprocess
Parsing its output protocol (NDJSON, SSE, JSONL, JSON‑RPC, etc.)
Managing lifecycle, timeouts, and retries
Compressing context when token limits are reached
Writing and maintaining this glue code for every new provider is error‑prone and hinders production use.
Solution: agent-wrapper Library
agent-wrapperprovides a unified registration and orchestration layer for multiple coding agents. Providers are registered once, and the active agent can be swapped by changing a string.
registry := agentwrapper.NewRegistry()
claude.RegisterIn(registry)
codex.RegisterIn(registry)
pi.RegisterIn(registry)
agent, _ := registry.Get("codex", nil) // switch provider by name
orch := agentwrapper.NewOrchestrator(agent)
result, _ := orch.RunSync(context.Background(), types.RunInput{Prompt: "帮我重构这段代码"})
fmt.Println(result.Text)Changing the provider only requires updating the provider name, eliminating duplicated glue code.
Safety Layers Added by the Orchestrator
The orchestrator inserts three guards that native CLI agents lack.
Approval interception : Every tool call passes through an ApprovalHandler. Read‑only operations (e.g., read, ls, grep) are allowed; other calls are denied and a synthetic ToolResult with the value “DENIED” is injected so the agent continues without crashing.
orch := agentwrapper.NewOrchestrator(agent,
agentwrapper.WithApprovalHandler(func(ctx context.Context, call agentwrapper.ToolCall) (*agentwrapper.Decision, error) {
switch call.Name {
case "read", "ls", "grep":
return &agentwrapper.Decision{Action: agentwrapper.ActionAllow}, nil
default:
return &agentwrapper.Decision{Action: agentwrapper.ActionDeny, Reason: "只读模式"}, nil
}
}),
)Budget control : A BudgetHandler receives token‑usage information after each turn. If total tokens exceed a configured limit (e.g., 50 000), the run is aborted with an error.
agentwrapper.WithBudgetHandler(func(ctx context.Context, usage types.TokenUsage) error {
if usage.TotalTokens > 50000 {
return fmt.Errorf("预算超支: %d tokens", usage.TotalTokens)
}
return nil
}),Context compression & automatic retry : When the LLM returns “context length exceeded”, the orchestrator compresses the message history using a sliding‑window → summarization → chain‑of‑thought pipeline and retries up to three times, invisible to the caller.
None of these three features are provided by the underlying agent CLIs.
Session Resume
Agents normally lose state between runs. agent-wrapper returns a SessionID on the first RunSync. Supplying the same SessionID on subsequent calls restores the full conversation context, enabling cross‑day, cross‑process, and cross‑machine continuity.
// First round
r1, _ := orch.RunSync(ctx, types.RunInput{Prompt: "这个项目的目录结构是什么?"})
fmt.Println(r1.SessionID) // store
// Next round, full context restored
r2, _ := orch.RunSync(ctx, types.RunInput{Prompt: "刚才你提到有一个潜在的性能问题,展开说说", SessionID: r1.SessionID})Command‑Line Demonstration
With a single command the wrapper can invoke any provider and choose output format:
# Stream output (text→stdout, metadata→stderr)
agent-wrapper run --provider claude-code "解释这段代码"
# JSON aggregation (CI‑friendly)
agent-wrapper run --provider codex "fix the bug" --json
# With approval and budget limits
agent-wrapper run --provider claude-code "重构本项目" --approve-all --budget-tokens 50000
# NDJSON pipeline
agent-wrapper run --provider claude-code "hello" --json --stream | jq .
# Resume a previous session
agent-wrapper run --provider claude-code --session-id abc123 "继续"Library Usage
When imported via go get, the orchestrator runs without spawning extra processes or handling external protocols, offering a single interface for five providers (Claude Code, Codex, Pi, OpenCode, etc.).
Value of Wrapping
Wrapping itself is not a drawback; the real cost lies in repeatedly writing glue code, manually managing agent lifecycles, and uncontrolled token consumption. agent-wrapper addresses these issues by providing safety, budget enforcement, session persistence, and a uniform API, turning experimental usage into production‑ready tooling.
Project repository: https://github.com/smallnest/agent-wrapper
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
BirdNest Tech Talk
Author of the rpcx microservice framework, original book author, and chair of Baidu's Go CMC committee.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
