feat: implement tool registry and add various tools for HTTP, file operations, SSH, and Matrix messaging
This commit is contained in:
+128
-16
@@ -18,8 +18,11 @@ import (
|
||||
shelllm "github.com/enmanuel/agents/shell/llm"
|
||||
"github.com/enmanuel/agents/shell/matrix"
|
||||
"github.com/enmanuel/agents/shell/ssh"
|
||||
"github.com/enmanuel/agents/tools"
|
||||
)
|
||||
|
||||
const defaultMaxToolIterations = 5
|
||||
|
||||
// Agent is the assembled runtime: pure core + impure shell.
|
||||
type Agent struct {
|
||||
cfg *config.AgentConfig
|
||||
@@ -29,6 +32,7 @@ type Agent struct {
|
||||
matrix *matrix.Client
|
||||
runner *effects.Runner
|
||||
listener *matrix.Listener
|
||||
toolReg *tools.Registry
|
||||
logger *slog.Logger
|
||||
cryptoStore io.Closer // non-nil when E2EE is enabled; closed on shutdown
|
||||
}
|
||||
@@ -75,12 +79,16 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
|
||||
// Effects runner
|
||||
runner := effects.NewRunner(matrixClient, sshExec, logger)
|
||||
|
||||
// Tool registry — register tools enabled in config
|
||||
toolReg := buildToolRegistry(cfg, sshExec, matrixClient, logger)
|
||||
|
||||
a := &Agent{
|
||||
cfg: cfg,
|
||||
rules: rules,
|
||||
llm: llmFunc,
|
||||
matrix: matrixClient,
|
||||
runner: runner,
|
||||
toolReg: toolReg,
|
||||
logger: logger,
|
||||
cryptoStore: cryptoStore,
|
||||
}
|
||||
@@ -96,7 +104,11 @@ func (a *Agent) Run(ctx context.Context) error {
|
||||
if a.cryptoStore != nil {
|
||||
defer a.cryptoStore.Close()
|
||||
}
|
||||
a.logger.Info("agent starting", "id", a.cfg.Agent.ID, "name", a.cfg.Agent.Name)
|
||||
a.logger.Info("agent starting",
|
||||
"id", a.cfg.Agent.ID,
|
||||
"name", a.cfg.Agent.Name,
|
||||
"tools", a.toolReg.Names(),
|
||||
)
|
||||
return a.listener.Run(ctx)
|
||||
}
|
||||
|
||||
@@ -134,7 +146,7 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
|
||||
return
|
||||
}
|
||||
|
||||
// Expand LLM actions inline (simplified — real impl would maintain conversation state)
|
||||
// Expand LLM actions inline — with tool-use loop when enabled
|
||||
expanded := make([]decision.Action, 0, len(actions))
|
||||
for _, act := range actions {
|
||||
if act.Kind == decision.ActionKindLLM {
|
||||
@@ -164,20 +176,120 @@ func (a *Agent) runLLM(ctx context.Context, msgCtx decision.MessageContext) (str
|
||||
"model", a.cfg.LLM.Primary.Model,
|
||||
"provider", a.cfg.LLM.Primary.Provider,
|
||||
)
|
||||
req := coretypes.CompletionRequest{
|
||||
Model: a.cfg.LLM.Primary.Model,
|
||||
MaxTokens: a.cfg.LLM.Primary.MaxTokens,
|
||||
Temperature: a.cfg.LLM.Primary.Temperature,
|
||||
SystemPrompt: a.cfg.Agent.Description,
|
||||
Messages: []coretypes.Message{
|
||||
{Role: coretypes.RoleUser, Content: msgCtx.Content},
|
||||
},
|
||||
|
||||
// Load system prompt from file if configured, else use description
|
||||
systemPrompt := a.cfg.Agent.Description
|
||||
|
||||
messages := []coretypes.Message{
|
||||
{Role: coretypes.RoleUser, Content: msgCtx.Content},
|
||||
}
|
||||
resp, err := a.llm(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Error("LLM call failed", "model", req.Model, "err", err)
|
||||
return "", err
|
||||
|
||||
// Build tool specs for the LLM if tool_use is enabled
|
||||
var llmTools []coretypes.ToolSpec
|
||||
if a.cfg.LLM.ToolUse.Enabled && a.toolReg.Len() > 0 {
|
||||
llmTools = a.toolReg.ToLLMSpecs()
|
||||
a.logger.Debug("tools available for LLM", "count", len(llmTools))
|
||||
}
|
||||
a.logger.Debug("LLM responded", "content_len", len(resp.Content))
|
||||
return resp.Content, nil
|
||||
|
||||
maxIter := a.cfg.LLM.ToolUse.MaxIterations
|
||||
if maxIter <= 0 {
|
||||
maxIter = defaultMaxToolIterations
|
||||
}
|
||||
|
||||
// Tool-use loop: call LLM → execute tools → feed results back → repeat
|
||||
for i := 0; i < maxIter; i++ {
|
||||
req := coretypes.CompletionRequest{
|
||||
Model: a.cfg.LLM.Primary.Model,
|
||||
MaxTokens: a.cfg.LLM.Primary.MaxTokens,
|
||||
Temperature: a.cfg.LLM.Primary.Temperature,
|
||||
SystemPrompt: systemPrompt,
|
||||
Messages: messages,
|
||||
Tools: llmTools,
|
||||
}
|
||||
|
||||
resp, err := a.llm(ctx, req)
|
||||
if err != nil {
|
||||
a.logger.Error("LLM call failed", "model", req.Model, "err", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
a.logger.Debug("LLM responded",
|
||||
"content_len", len(resp.Content),
|
||||
"tool_calls", len(resp.ToolCalls),
|
||||
"finish_reason", resp.FinishReason,
|
||||
)
|
||||
|
||||
// No tool calls — return the text response
|
||||
if len(resp.ToolCalls) == 0 {
|
||||
return resp.Content, nil
|
||||
}
|
||||
|
||||
// Append assistant message with tool calls to conversation
|
||||
messages = append(messages, coretypes.Message{
|
||||
Role: coretypes.RoleAssistant,
|
||||
Content: resp.Content,
|
||||
ToolCalls: resp.ToolCalls,
|
||||
})
|
||||
|
||||
// Execute each tool and append results
|
||||
for _, tc := range resp.ToolCalls {
|
||||
a.logger.Info("executing tool",
|
||||
"tool", tc.Name,
|
||||
"call_id", tc.ID,
|
||||
)
|
||||
|
||||
result := a.toolReg.Execute(ctx, tc.Name, tc.Arguments)
|
||||
|
||||
output := result.Output
|
||||
if result.Err != nil {
|
||||
output = fmt.Sprintf("error: %s", result.Err)
|
||||
a.logger.Warn("tool execution error",
|
||||
"tool", tc.Name,
|
||||
"err", result.Err,
|
||||
)
|
||||
} else {
|
||||
a.logger.Debug("tool executed",
|
||||
"tool", tc.Name,
|
||||
"output_len", len(output),
|
||||
)
|
||||
}
|
||||
|
||||
messages = append(messages, coretypes.Message{
|
||||
Role: coretypes.RoleTool,
|
||||
Content: output,
|
||||
ToolCallID: tc.ID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Max iterations reached — return whatever we have
|
||||
a.logger.Warn("tool-use loop reached max iterations", "max", maxIter)
|
||||
return "I've reached the maximum number of tool iterations. Here's what I found so far.", nil
|
||||
}
|
||||
|
||||
// buildToolRegistry creates a Registry with tools enabled in the agent's config.
|
||||
func buildToolRegistry(cfg *config.AgentConfig, sshExec *ssh.Executor, matrixClient *matrix.Client, logger *slog.Logger) *tools.Registry {
|
||||
reg := tools.NewRegistry()
|
||||
|
||||
if cfg.Tools.HTTP.Enabled {
|
||||
reg.Register(tools.NewHTTPGet(cfg.Tools.HTTP))
|
||||
reg.Register(tools.NewHTTPPost(cfg.Tools.HTTP))
|
||||
logger.Debug("registered http tools")
|
||||
}
|
||||
|
||||
if cfg.Tools.SSH.Enabled {
|
||||
reg.Register(tools.NewSSHCommand(cfg.Tools.SSH, sshExec))
|
||||
logger.Debug("registered ssh tool")
|
||||
}
|
||||
|
||||
if cfg.Tools.FileOps.Enabled {
|
||||
reg.Register(tools.NewReadFile(cfg.Tools.FileOps))
|
||||
logger.Debug("registered file tool")
|
||||
}
|
||||
|
||||
// matrix_send is always available
|
||||
reg.Register(tools.NewMatrixSend(matrixClient))
|
||||
logger.Debug("registered matrix tool")
|
||||
|
||||
return reg
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user