Documentation
¶
Overview ¶
Package agentkit is an agent loop as a library: a provider, Go tools and MCP servers, with no files and no global state. The Agent holds what is costly; each Conversation holds its own history.
Example ¶
An agent with one Go tool. A real program sets Provider and Model (for instance "gemini" and "gemini-2.5-flash-lite") instead of ProviderImpl.
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ThiraSoft/agentkit"
"github.com/ThiraSoft/agentkit/llm"
)
// scripted stands in for a model in these examples: it asks for the clock
// tool, then answers with what the tool returned.
type scripted struct{}
func (scripted) Name() string { return "scripted" }
func (scripted) ModelName() string { return "scripted" }
func (s scripted) Chat(ctx context.Context, msgs []llm.Message, tools []llm.Tool) (*llm.Message, error) {
return s.Stream(ctx, msgs, tools, func(string) error { return nil })
}
func (scripted) Stream(_ context.Context, msgs []llm.Message, _ []llm.Tool, onChunk func(string) error) (*llm.Message, error) {
last := msgs[len(msgs)-1]
if last.Role != "tool" {
return &llm.Message{Role: "assistant", ToolCalls: []llm.ToolCall{{
ID: "1",
Type: "function",
Function: llm.FunctionCall{Name: "clock", Arguments: json.RawMessage(`{}`)},
}}}, nil
}
text := "It is " + last.Content + "."
if err := onChunk(text); err != nil {
return nil, err
}
return &llm.Message{Role: "assistant", Content: text}, nil
}
var clock = agentkit.Tool{
Name: "clock",
Description: "Tells the time.",
Parameters: llm.ToolParams{Type: "object", Properties: llm.ToolProperties{}},
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return "12:00", nil
},
}
func main() {
ctx := context.Background()
agent, err := agentkit.New(ctx, agentkit.Config{
ProviderImpl: scripted{},
Tools: []agentkit.Tool{clock},
})
if err != nil {
log.Fatal(err)
}
defer agent.Close()
conv := agent.NewConversation("Answer in one sentence.")
turn, err := conv.Send(ctx, "What time is it?", agentkit.Hooks{
OnText: func(chunk string) { fmt.Println("text:", chunk) },
})
if err != nil {
log.Fatal(err)
}
fmt.Println("steps:", turn.Steps)
fmt.Println("reply:", turn.Text)
}
Output: text: It is 12:00. steps: 2 reply: It is 12:00.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrMaxSteps = errors.New("agentkit: maximum number of steps reached")
ErrMaxSteps is returned when a turn reaches MaxSteps model calls without finishing.
Functions ¶
This section is empty.
Types ¶
type Agent ¶
type Agent struct {
// contains filtered or unexported fields
}
Agent brings together a provider and tools. It holds no conversation state and serves multiple Conversations concurrently.
func New ¶
New builds the provider, connects MCP servers and validates tools. It fails rather than falling back to something else.
func (*Agent) NewConversation ¶
func (a *Agent) NewConversation(system string, history ...llm.Message) *Conversation
NewConversation starts a conversation, optionally with existing history (without system prompt).
func (*Agent) Tools ¶
Tools returns the tool names seen by the model, in the order it sees them: Go tools first, then MCP servers.
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ThiraSoft/agentkit"
"github.com/ThiraSoft/agentkit/llm"
)
// scripted stands in for a model in these examples: it asks for the clock
// tool, then answers with what the tool returned.
type scripted struct{}
func (scripted) Name() string { return "scripted" }
func (scripted) ModelName() string { return "scripted" }
func (s scripted) Chat(ctx context.Context, msgs []llm.Message, tools []llm.Tool) (*llm.Message, error) {
return s.Stream(ctx, msgs, tools, func(string) error { return nil })
}
func (scripted) Stream(_ context.Context, msgs []llm.Message, _ []llm.Tool, onChunk func(string) error) (*llm.Message, error) {
last := msgs[len(msgs)-1]
if last.Role != "tool" {
return &llm.Message{Role: "assistant", ToolCalls: []llm.ToolCall{{
ID: "1",
Type: "function",
Function: llm.FunctionCall{Name: "clock", Arguments: json.RawMessage(`{}`)},
}}}, nil
}
text := "It is " + last.Content + "."
if err := onChunk(text); err != nil {
return nil, err
}
return &llm.Message{Role: "assistant", Content: text}, nil
}
var clock = agentkit.Tool{
Name: "clock",
Description: "Tells the time.",
Parameters: llm.ToolParams{Type: "object", Properties: llm.ToolProperties{}},
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return "12:00", nil
},
}
func clockAgent() *agentkit.Agent {
agent, err := agentkit.New(context.Background(), agentkit.Config{
ProviderImpl: scripted{},
Tools: []agentkit.Tool{clock},
})
if err != nil {
log.Fatal(err)
}
return agent
}
func main() {
agent := clockAgent()
defer agent.Close()
fmt.Println(agent.Tools())
}
Output: [clock]
type Config ¶
type Config struct {
Provider string // "gemini", "openai-compat", "anthropic", "llamacpp"...
Model string
BaseURL string // full base URL of provider, e.g. https://generativelanguage.googleapis.com/v1beta for Gemini, http://host:port/v1 for an OpenAI-compatible server (empty = default)
APIKey string // empty = provider environment variable
ProviderImpl llm.Provider
Tools []Tool
MCP []mcp.ServerConfig
MaxSteps int // model calls per Send, 20 by default
MaxToolResult int // bytes kept from a tool result, 32 KB by default
}
Config describes an agent. ProviderImpl, if non-nil, overrides Provider, Model, BaseURL and APIKey.
type Conversation ¶
type Conversation struct {
// contains filtered or unexported fields
}
Conversation holds history, including the system prompt, and has only one active turn: a Send during another interrupts the first.
func (*Conversation) Messages ¶
func (c *Conversation) Messages() []llm.Message
Messages returns a copy of the history.
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/ThiraSoft/agentkit"
"github.com/ThiraSoft/agentkit/llm"
)
// scripted stands in for a model in these examples: it asks for the clock
// tool, then answers with what the tool returned.
type scripted struct{}
func (scripted) Name() string { return "scripted" }
func (scripted) ModelName() string { return "scripted" }
func (s scripted) Chat(ctx context.Context, msgs []llm.Message, tools []llm.Tool) (*llm.Message, error) {
return s.Stream(ctx, msgs, tools, func(string) error { return nil })
}
func (scripted) Stream(_ context.Context, msgs []llm.Message, _ []llm.Tool, onChunk func(string) error) (*llm.Message, error) {
last := msgs[len(msgs)-1]
if last.Role != "tool" {
return &llm.Message{Role: "assistant", ToolCalls: []llm.ToolCall{{
ID: "1",
Type: "function",
Function: llm.FunctionCall{Name: "clock", Arguments: json.RawMessage(`{}`)},
}}}, nil
}
text := "It is " + last.Content + "."
if err := onChunk(text); err != nil {
return nil, err
}
return &llm.Message{Role: "assistant", Content: text}, nil
}
var clock = agentkit.Tool{
Name: "clock",
Description: "Tells the time.",
Parameters: llm.ToolParams{Type: "object", Properties: llm.ToolProperties{}},
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return "12:00", nil
},
}
func clockAgent() *agentkit.Agent {
agent, err := agentkit.New(context.Background(), agentkit.Config{
ProviderImpl: scripted{},
Tools: []agentkit.Tool{clock},
})
if err != nil {
log.Fatal(err)
}
return agent
}
func main() {
agent := clockAgent()
defer agent.Close()
conv := agent.NewConversation("Answer in one sentence.")
if _, err := conv.Send(context.Background(), "What time is it?", agentkit.Hooks{}); err != nil {
log.Fatal(err)
}
var roles []string
for _, m := range conv.Messages() {
roles = append(roles, m.Role)
}
fmt.Println(strings.Join(roles, " "))
}
Output: system user assistant tool assistant
func (*Conversation) Reset ¶
func (c *Conversation) Reset()
Reset interrupts the active turn if any, then clears all messages except the system prompt.
Calling Send or Reset from within a hook of the active turn will deadlock; Messages and SetSystem remain permitted.
func (*Conversation) Send ¶
Send appends text to the history and runs the model and its tools until a response without tool calls is produced. Any active Send is interrupted first, and the new turn sees whatever remains of it.
Calling Send or Reset from within a hook of the active turn will deadlock; Messages and SetSystem remain permitted.
func (*Conversation) SetSystem ¶
func (c *Conversation) SetSystem(system string)
SetSystem replaces the system prompt without modifying the rest.
type Hooks ¶
type Hooks struct {
OnText func(chunk string)
OnToolCall func(ToolCall)
OnToolResult func(ToolResult)
// OnStepEnd: the model finished a message requesting tool calls.
OnStepEnd func()
// OnFinish: the model finished its response, the turn is about to close.
// It may block (e.g. voice finishing speech); if the turn is interrupted
// during this time, it is treated as an interruption and OnInterrupt decides
// what text is kept.
OnFinish func()
// Approve: returning false rejects the call; the model receives a rejection.
Approve func(ToolCall) bool
// Prepare receives a copy of the history before each model call
// and returns what will be sent. The copy is shallow: mutating in place
// nested slice contents (Media, ToolCalls) modifies history; replacing
// a field or an entire slice has no effect.
Prepare func(msgs []llm.Message) []llm.Message
// OnInterrupt receives the text written by the model during an interrupted
// turn and returns what should be kept in history.
OnInterrupt func(written string) (kept string)
}
Hooks are called during a Send, within its goroutine. All are optional. A hook that blocks (OnFinish waiting for voice output, Approve waiting for user confirmation) does not see the cancellation triggered by another Send or by Reset: it is up to the caller to unblock whatever it is waiting on when canceling.
Calling Send or Reset from within a hook of the active turn will deadlock; Messages and SetSystem remain permitted.
Example ¶
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/ThiraSoft/agentkit"
"github.com/ThiraSoft/agentkit/llm"
)
// scripted stands in for a model in these examples: it asks for the clock
// tool, then answers with what the tool returned.
type scripted struct{}
func (scripted) Name() string { return "scripted" }
func (scripted) ModelName() string { return "scripted" }
func (s scripted) Chat(ctx context.Context, msgs []llm.Message, tools []llm.Tool) (*llm.Message, error) {
return s.Stream(ctx, msgs, tools, func(string) error { return nil })
}
func (scripted) Stream(_ context.Context, msgs []llm.Message, _ []llm.Tool, onChunk func(string) error) (*llm.Message, error) {
last := msgs[len(msgs)-1]
if last.Role != "tool" {
return &llm.Message{Role: "assistant", ToolCalls: []llm.ToolCall{{
ID: "1",
Type: "function",
Function: llm.FunctionCall{Name: "clock", Arguments: json.RawMessage(`{}`)},
}}}, nil
}
text := "It is " + last.Content + "."
if err := onChunk(text); err != nil {
return nil, err
}
return &llm.Message{Role: "assistant", Content: text}, nil
}
var clock = agentkit.Tool{
Name: "clock",
Description: "Tells the time.",
Parameters: llm.ToolParams{Type: "object", Properties: llm.ToolProperties{}},
Run: func(ctx context.Context, args json.RawMessage) (string, error) {
return "12:00", nil
},
}
func clockAgent() *agentkit.Agent {
agent, err := agentkit.New(context.Background(), agentkit.Config{
ProviderImpl: scripted{},
Tools: []agentkit.Tool{clock},
})
if err != nil {
log.Fatal(err)
}
return agent
}
func main() {
agent := clockAgent()
defer agent.Close()
_, err := agent.NewConversation("").Send(context.Background(), "What time is it?", agentkit.Hooks{
OnToolCall: func(c agentkit.ToolCall) { fmt.Println("call:", c.Name, string(c.Arguments)) },
Approve: func(c agentkit.ToolCall) bool { return c.Name == "clock" },
OnToolResult: func(r agentkit.ToolResult) { fmt.Println("result:", r.Content) },
OnFinish: func() { fmt.Println("finished") },
})
if err != nil {
log.Fatal(err)
}
}
Output: call: clock {} result: 12:00 finished
type Tool ¶
type Tool struct {
Name string
Description string
Parameters llm.ToolParams
Run func(ctx context.Context, args json.RawMessage) (string, error)
}
Tool is a tool executed in the caller's process. Run must respect ctx cancellation: cancellation waits for tools to finish. A panic in Run crashes the process (the tool runs in a goroutine).
type ToolCall ¶
type ToolCall struct {
ID, Name string
Arguments json.RawMessage
}
ToolCall is a tool call requested by the model.
type ToolResult ¶
ToolResult is what the model receives in response. Err is empty if and only if the tool succeeded.
Directories
¶
| Path | Synopsis |
|---|---|
|
internal
|
|
|
testmcp
command
Command testmcp is a minimal stdio MCP server for agentkit tests, with a single tool, get_time.
|
Command testmcp is a minimal stdio MCP server for agentkit tests, with a single tool, get_time. |
|
Package llm talks to language models behind one interface, Provider (Chat and Stream), with the message and tool types they share.
|
Package llm talks to language models behind one interface, Provider (Chat and Stream), with the message and tool types they share. |
|
Package mcp connects to Model Context Protocol servers over stdio, SSE or streamable HTTP, and exposes their tools in the llm.Tool format so an agent can call them next to its own tools.
|
Package mcp connects to Model Context Protocol servers over stdio, SSE or streamable HTTP, and exposes their tools in the llm.Tool format so an agent can call them next to its own tools. |