https://github.com/voocel/agentcore
A minimal, composable Go library for building AI agent applications.
https://github.com/voocel/agentcore
agent agentcore agents ai go llm multi-agent multi-agent-systems workflows
Last synced: 2 months ago
JSON representation
A minimal, composable Go library for building AI agent applications.
- Host: GitHub
- URL: https://github.com/voocel/agentcore
- Owner: voocel
- License: apache-2.0
- Created: 2025-03-15T13:18:31.000Z (over 1 year ago)
- Default Branch: main
- Last Pushed: 2026-04-11T10:50:53.000Z (2 months ago)
- Last Synced: 2026-04-11T12:25:00.494Z (2 months ago)
- Topics: agent, agentcore, agents, ai, go, llm, multi-agent, multi-agent-systems, workflows
- Language: Go
- Homepage:
- Size: 753 KB
- Stars: 32
- Watchers: 4
- Forks: 11
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# AgentCore
**AgentCore** is a minimal, composable Go library for building AI agent applications.
[English](README.md) | [中文](README_CN.md)
## Install
```bash
go get github.com/voocel/agentcore
```
## Design Philosophy
A restrained core with open extensibility tends to be more reliable than a complex all-in-one solution. Fewer built-ins, more possibilities.
## Stability
- Keep `Agent`, `AgentLoop`, `Event`, `Tool`, and `Message` stable first
- Behavioral changes should come with tests first; `examples/` and internal implementation details are not stable API
## Architecture
```
agentcore/ Agent core (types, loop, agent, events, subagent)
agentcore/llm/ LLM adapters (OpenAI, Anthropic, Gemini via litellm)
agentcore/tools/ Built-in tools: read, write, edit, bash
agentcore/context/ Context runtime — projection, rewrite, overflow recovery
```
Core design:
- **Standalone loop** (`loop.go`) — free function, all dependencies injected via parameters. Double loop: inner processes tool calls + steering, outer handles follow-up
- **Stateful Agent** (`agent.go`) — sole consumer of loop events, updates internal state then dispatches to external listeners
- **Event stream** — single `<-chan Event` output drives any UI (TUI, Web, Slack, logging)
- **Two-stage pipeline** — `TransformContext` (prune/inject) → `ConvertToLLM` (filter to LLM messages)
- **SubAgent tool** (`subagent.go`) — multi-agent via tool invocation, four modes: single, parallel, chain, background
- **Context runtime** (`context/`) — projection, committed rewrite, and overflow recovery near the context window limit
## Quick Start
### Single Agent
```go
package main
import (
"fmt"
"os"
"github.com/voocel/agentcore"
"github.com/voocel/agentcore/llm"
"github.com/voocel/agentcore/permission"
"github.com/voocel/agentcore/tools"
)
func main() {
model, err := llm.NewOpenAIModel("gpt-5-mini", os.Getenv("OPENAI_API_KEY"))
if err != nil {
panic(err)
}
agent := agentcore.NewAgent(
agentcore.WithModel(model),
agentcore.WithSystemPrompt("You are a helpful coding assistant."),
agentcore.WithTools(
tools.NewRead("."),
tools.NewWrite("."),
tools.NewEdit("."),
tools.NewBash("."),
),
agentcore.WithPermissionEngine(permission.NewEngine(permission.EngineConfig{
Workspace: ".",
Mode: permission.ModeBalanced,
})),
)
agent.Subscribe(func(ev agentcore.Event) {
if ev.Type == agentcore.EventMessageEnd {
if msg, ok := ev.Message.(agentcore.Message); ok && msg.Role == agentcore.RoleAssistant {
fmt.Println(msg.Content)
}
}
})
agent.Prompt("List the files in the current directory.")
agent.WaitForIdle()
}
```
For stricter control, pass a custom decision engine with `agentcore.WithPermissionEngine(...)`.
```go
engine := permission.NewEngine(permission.EngineConfig{
Workspace: ".",
Mode: permission.ModeStrict,
Roots: permission.FilesystemRoots{
ReadRoots: []string{"."},
WriteRoots: []string{"."},
},
})
```
### Multi-Agent (SubAgent Tool)
Sub-agents are invoked as regular tools with isolated contexts:
```go
model, _ := llm.NewOpenAIModel("gpt-5-mini", apiKey)
scout := agentcore.SubAgentConfig{
Name: "scout",
Description: "Fast codebase reconnaissance",
Model: model,
SystemPrompt: "Quickly explore and report findings. Be concise.",
Tools: []agentcore.Tool{tools.NewRead("."), tools.NewBash(".")},
MaxTurns: 5,
}
worker := agentcore.SubAgentConfig{
Name: "worker",
Description: "General-purpose executor",
Model: model,
SystemPrompt: "Implement tasks given to you.",
Tools: []agentcore.Tool{tools.NewRead("."), tools.NewWrite("."), tools.NewEdit("."), tools.NewBash(".")},
}
agent := agentcore.NewAgent(
agentcore.WithModel(model),
agentcore.WithTools(agentcore.NewSubAgentTool(scout, worker)),
)
```
Four execution modes via tool call:
```jsonc
// Single: one agent, one task
{"agent": "scout", "task": "Find all API endpoints"}
// Parallel: concurrent execution
{"tasks": [{"agent": "scout", "task": "Find auth code"}, {"agent": "scout", "task": "Find DB schema"}]}
// Chain: sequential with {previous} context passing
{"chain": [{"agent": "scout", "task": "Find auth code"}, {"agent": "worker", "task": "Refactor based on: {previous}"}]}
// Background: async execution, returns immediately, notifies on completion
{"agent": "worker", "task": "Run full test suite", "background": true, "description": "Running tests"}
```
### Steering & Follow-Up
```go
// Interrupt mid-run (delivered after current tool, remaining tools skipped)
agent.Steer(agentcore.UserMsg("Stop and focus on tests instead."))
// Queue for after the agent finishes
agent.FollowUp(agentcore.UserMsg("Now run the tests."))
// Cancel immediately
agent.Abort()
```
### Event Stream
All lifecycle events flow through a single channel — subscribe to drive any UI:
```go
agent.Subscribe(func(ev agentcore.Event) {
switch ev.Type {
case agentcore.EventMessageStart: // assistant starts streaming
case agentcore.EventMessageUpdate: // streaming token delta
case agentcore.EventMessageEnd: // message complete
case agentcore.EventToolExecStart: // tool execution begins
case agentcore.EventToolExecEnd: // tool execution ends
case agentcore.EventError: // error occurred
}
})
```
### Structured Tool Progress
Long-running tools can emit structured progress updates instead of ad-hoc JSON:
```go
agentcore.ReportToolProgress(ctx, agentcore.ProgressPayload{
Kind: agentcore.ProgressSummary,
Agent: "worker",
Tool: "bash",
Summary: "worker → bash",
})
```
Subscribers should read `ev.Progress` directly for tool progress updates:
```go
agent.Subscribe(func(ev agentcore.Event) {
if ev.Type == agentcore.EventToolExecUpdate && ev.Progress != nil {
fmt.Printf("[%s] %s\n", ev.Progress.Kind, ev.Progress.Summary)
}
})
```
### Swappable Models
When a model needs to change at runtime, wrap it with `SwappableModel`. The swap takes effect on the next call. `SubAgentConfig.Model` is resolved at the start of each sub-agent run, so the same wrapper also works for sub-agents.
```go
defaultModel, _ := llm.NewOpenAIModel("gpt-5-mini", apiKey)
sw := agentcore.NewSwappableModel(defaultModel)
agent := agentcore.NewAgent(agentcore.WithModel(sw))
nextModel, _ := llm.NewOpenAIModel("gpt-5", apiKey)
sw.Swap(nextModel) // next turn uses the new model
```
### Custom LLM (StreamFn)
Swap the LLM call with a proxy, mock, or custom implementation:
```go
agent := agentcore.NewAgent(
agentcore.WithStreamFn(func(ctx context.Context, req *agentcore.LLMRequest) (*agentcore.LLMResponse, error) {
// Route to your own proxy/gateway
return callMyProxy(ctx, req)
}),
)
```
### Context Compaction
Auto-summarize conversation history when approaching the context window limit. Use the built-in context manager:
```go
import (
"github.com/voocel/agentcore"
agentctx "github.com/voocel/agentcore/context"
)
engine := agentctx.NewDefaultEngine(model, 128000)
agent := agentcore.NewAgent(
agentcore.WithModel(model),
agentcore.WithContextManager(engine),
)
```
`NewAgent` auto-wires `ConvertToLLM`, token estimation, and context window from the context manager when available.
On each LLM call, the context manager first builds a projected prompt view for the next model request. When a rewrite should become the new runtime baseline, it can return `ShouldCommit=true` with `CommitMessages`, and the loop will replace the in-memory baseline before continuing.
When usage exceeds `ContextWindow - ReserveTokens` (default 16384), compaction:
1. Keeps recent messages (default 20000 tokens)
2. Summarizes older messages via LLM into a structured checkpoint (Goal / Progress / Key Decisions / Next Steps)
3. Tracks file operations (read/write/edit paths) across compacted messages
4. Supports incremental updates — subsequent compactions update the existing summary rather than re-summarizing
### Context Pipeline
For simpler transform-only pipelines, `WithContextPipeline` / `WithTransformContext` still work:
```go
agent := agentcore.NewAgent(
// Stage 1: prune old messages, inject external context
agentcore.WithTransformContext(func(ctx context.Context, msgs []agentcore.AgentMessage) ([]agentcore.AgentMessage, error) {
if len(msgs) > 100 {
msgs = msgs[len(msgs)-50:]
}
return msgs, nil
}),
// Stage 2: filter to LLM-compatible messages
agentcore.WithConvertToLLM(func(msgs []agentcore.AgentMessage) []agentcore.Message {
var out []agentcore.Message
for _, m := range msgs {
if msg, ok := m.(agentcore.Message); ok {
out = append(out, msg)
}
}
return out
}),
)
```
## Built-in Tools
| Tool | Description |
|------|-------------|
| `read` | Read file contents with head truncation (2000 lines / 50KB) |
| `write` | Write file with auto-mkdir |
| `edit` | Exact text replacement with fuzzy match, BOM/line-ending normalization, unified diff output |
| `bash` | Execute shell commands with tail truncation (2000 lines / 50KB) |
## Runtime Injection
Use `Inject(msg)` when the caller's intent is "deliver this as soon as the current
agent state allows" without manually branching on running vs idle state.
```go
result, err := agent.Inject(agentcore.UserMsg("Re-check unfinished tasks before stopping."))
if err != nil {
panic(err)
}
fmt.Println(result.Disposition)
```
`Inject` has three outcomes:
- `steered_current_run`: the agent is running, so the message was queued into the current run's steering path
- `resumed_idle_run`: the agent was idle with an assistant-tail conversation, so the message was queued and `Continue()` was started immediately
- `queued`: the message was queued, but no run was started
Use the lower-level APIs when you need stricter control:
- `Steer(msg)`: queue for the steering path without any idle auto-resume logic
- `FollowUp(msg)`: queue for after the current run stops
- prompt-side injection: keep this in the application layer if the message must be merged into the next explicit user prompt rather than the agent queues
## API Reference
### Agent
| Method | Description |
|--------|-------------|
| `NewAgent(opts...)` | Create agent with options |
| `Prompt(input)` | Start new conversation turn |
| `PromptMessages(msgs...)` | Start turn with arbitrary AgentMessages |
| `Continue()` | Resume from current context |
| `Inject(msg)` | Deliver message via steer / idle resume / queue, depending on current state |
| `Steer(msg)` | Inject steering message mid-run |
| `FollowUp(msg)` | Queue message for after completion |
| `Abort()` | Cancel current execution |
| `AbortSilent()` | Cancel without emitting abort marker |
| `WaitForIdle()` | Block until agent finishes |
| `Subscribe(fn)` | Register event listener |
| `State()` | Snapshot of current state |
| `ExportMessages()` | Export messages for serialization |
| `ImportMessages(msgs)` | Import deserialized messages |
## License
Apache License 2.0