Agent Package¶
The internal/agent package defines the capabilities and environment for the Autonomous Repair Agent. This agent is responsible for the self-healing code generation feature, where AI-generated code is iteratively fixed until it compiles and passes tests.
Overview¶
When the user runs generate command --script or --prompt, an AI loop is initiated. This loop allows the LLM (Large Language Model) to act as a "developer" exploring a sandbox environment. The agent package defines the Tools available to this developer.
Available Tools¶
The agent does not have unrestricted shell access. It operates through a strictly defined set of tools defined in tools.go to ensure safety and determinism.
| Tool Name | Purpose | Implementation Details |
|---|---|---|
list_dir |
Explore the project structure | Lists files in the current sandbox. |
read_file |
Read code content | Returns file contents to the LLM context. |
write_file |
Create or update code | Writes code to the in-memory or on-disk filesystem. |
go_build |
Verify compilation | Runs go build ./... and captures stderr. |
go_test |
Verify functionality | Runs go test ./... and captures output. |
go_get |
Fix dependencies | Runs go get <package> to resolve missing imports. |
golangci_lint |
Enforce standards | Runs the project's linter configuration. |
The Repair Loop¶
The repair process follows a ReAct (Reason + Act) pattern:
- Draft: The initial generation is written to the filesystem.
- Verify: The agent runs
go_buildandgo_test. - Analyze: If verification fails, the agent reads the error logs (compiler errors, test failures).
- Reason: The agent determines the root cause (e.g., "undefined variable", "import missing").
- Act: The agent calls
write_fileto patch the code orgo_getto add a dependency. - Loop: Steps 2-5 repeat until success or the maximum step limit (default: 15) is reached.
Integration with Generator¶
The internal/generator package orchestrates this agent.
generator.goinitializes the project structure.- It delegates to
ai.goto create aChatClientand anAgent. - The
Agentis initialized with theprops.FS(Filesystem) andprops.Logger. - The loop executes, modifying the implementation files (
main.go) directly.
Security Strategy: Sandboxing¶
The agent's file operations are strictly confined to a basePath (typically the project root). This is enforced by the ensurePathAllowed helper, which prevents directory traversal attacks (../).
func ensurePathAllowed(basePath, targetPath string) error {
// changing /abs/base/../etc/passwd -> /etc/passwd
absReq, _ := filepath.Abs(targetPath)
// Check prefix
if !strings.HasPrefix(absReq, absBase) {
return ErrPathInvalid
}
return nil
}
Tutorial: Building a New Agent Tool¶
This tutorial demonstrates how to add a grep_search tool to the agent, allowing the LLM to search for patterns in files.
1. Define the Tool Factory¶
Create a new file internal/agent/search_tool.go or add to tools.go.
package agent
import (
"context"
"encoding/json"
"fmt"
"os/exec"
"github.com/phpboyscout/gtb/pkg/chat"
"github.com/invopop/jsonschema"
)
// GrepTool returns a tool for searching file contents
func GrepTool(basePath string) chat.Tool {
return chat.Tool{
Name: "grep_search",
Description: "Search for a string pattern in files recursively.",
// 1. Define Parameters Schema using reflection
Parameters: jsonschema.Reflect(struct {
Pattern string `json:"pattern" jsonschema:"description=Regex pattern to search for"`
Path string `json:"path" jsonschema:"description=Directory to search in"`
}{}),
// 2. Implement the Handler
Handler: func(ctx context.Context, args json.RawMessage) (interface{}, error) {
// A. Parse Arguments
var params struct {
Pattern string `json:"pattern"`
Path string `json:"path"`
}
if err := json.Unmarshal(args, ¶ms); err != nil {
return nil, fmt.Errorf("invalid args: %w", err)
}
// B. Enforce Sandbox
if err := ensurePathAllowed(basePath, params.Path); err != nil {
return nil, err
}
// C. Execute Logic (ripgrep / grep)
cmd := exec.CommandContext(ctx, "grep", "-r", params.Pattern, params.Path)
output, err := cmd.CombinedOutput()
if err != nil {
// Return output even on failure (grep returns non-zero if not found)
return fmt.Sprintf("Search completed (err=%v):\n%s", err, string(output)), nil
}
return string(output), nil
},
}
}
2. Register Your Tool¶
Update internal/agent/ai.go (or wherever the agent is initialized) to include your new tool in the definition list.
// internal/agent/ai.go
func NewAgent(prop *props.Props) *chat.Agent {
basePath := prop.Cwd
tools := []chat.Tool{
ListDirTool(basePath),
ReadFileTool(basePath),
GrepTool(basePath), // <--- Register here
}
return chat.NewAgent(client, tools)
}
Architecture for Custom Agents¶
If you are building your own agent-based tool using this library as a foundation, follow these design patterns:
1. The Context-Tool-loop¶
The core loop is managed by the chat.Client (pkg/chat), but the semantics are defined here.
- State: Maintain a conversation history (
[]chat.Message). - Iteration:
- Send history to LLM.
- LLM responds with a
ToolCall. - Execute the tool locally.
- Append the
ToolResultto history. - Repeat.
2. Stateless vs Stateful Tools¶
- Stateless: Most tools should be stateless functions (like
read_file). - Stateful: If you need state (e.g., a "current selection"), pass a state object into the tool factory closure.
3. Error Handling as Feedback¶
Don't just log errors; return them to the LLM. The error message is often the most valuable feedback for the model to self-correct.
if err != nil {
// Return the error string so the LLM sees it
return nil, fmt.Errorf("compilation failed: %w", err)
}
By treating errors as data, you enable the ReAct loop to actually work.
Extending the Agent¶
To add new capabilities to the autonomous agent:
- Define the Tool: Add a new function definition in
internal/agent/tools.go. - ** Implement Logic**: Implement the handler that executes the tool (e.g., executing a new CLI command).
- Register: Add the tool definition to the tool registry passed to the LLM provider.
Sandboxing
While the agent runs locally, it is constrained by the tools provided. It cannot arbitrarily delete files outside the project scope or execute dangerous system commands unless explicitly exposed as a tool.