Skip to content

Extending

Register custom tools, middlewares, agents, providers, and skills to shape the engine's behavior.

Tools

A tool implements four methods:

type Tool interface {
    Name() string
    Description() string
    Parameters() any
    Execute(ctx context.Context, args string) (string, error)
}

The ergonomic way is sdk.NewTool, which wraps a function and a JSON schema:

greet := sdk.NewTool(
    "greet",
    "greets the user",
    map[string]any{
        "type": "object",
        "properties": map[string]any{
            "name": map[string]any{"type": "string"},
        },
        "required": []string{"name"},
    },
    func(ctx context.Context, args string) (string, error) {
        return "hello, " + args, nil
    },
)

err := eng.RegisterTool(greet)

Parameters is a JSON schema describing the arguments. The model invokes the tool with a JSON string that your Execute function parses.

Registry operations:

eng.RegisterTool(greet)        // sdk.ErrToolNil / ErrToolEmpty / ErrToolDuplicate
eng.UnregisterTool("greet")    // bool
eng.Tools()                    // []Tool, sorted by name

Tools registered this way are available to agents that enable them by name (see ChatRequest.Tools).

Middlewares

Middlewares wrap the agent lifecycle. Embed sdk.BaseMiddleware to get no-op defaults for all seven hooks and override the ones you need:

type myMW struct {
    sdk.BaseMiddleware
}

func (m *myMW) BeforeLLM(ctx context.Context, req *sdk.Request) error {
    // inspect or mutate the outgoing request
    return nil
}

eng.RegisterMiddleware("my-mw", &myMW{})   // sdk.ErrMiddlewareNil / Duplicate
eng.UnregisterMiddleware("my-mw")           // bool

The full hook set: BeforeRun, AfterRun, BeforeLLM, AfterLLM, BeforeTool, AfterTool, OnError.

Agents

Register a factory for a new agent type. The factory is invoked immediately to learn the agent's name:

eng.RegisterAgent(func() (*sdk.Agent, error) {
    a := &sdk.Agent{
        Name:          "auditor",
        Description:   "Audits code for security issues",
        SystemPrompt:  "You are a security auditor...",
        MaxIterations: 30,
    }
    return a, nil
})

The built-in agent types are orchestrator, explorer, planner, and developer, and are what Chat and the task tool run. The sdk.Agent type exposes all its fields and run methods, but note that a fully wired agent also needs a populated tool registry and an event bus; the built-in agents come pre-wired through the engine. Registered agent types become available to the task tool and the TUI mention picker.

Providers

Register a new LLM provider factory:

eng.RegisterProvider("my-provider", func(cfg sdk.LLMConfig) (sdk.Provider, error) {
    // return a provider implementing Chat / ChatStream / ListModels
    return provider, nil
})

The provider becomes available to model resolution under its name.

Skills

Load SKILL.md-based skill directories or register skills directly:

eng.LoadSkills("~/skills", "./skills")

for _, sk := range eng.Skills() {
    fmt.Println(sk.Name, sk.Description)
}

eng.RegisterSkill(sk)   // name must match ^[a-z0-9]+(?:-[a-z0-9]+)*$

A *sdk.Skill has Name, Description, License, Compatibility, Metadata, AllowedTools, Instructions, Source, Path, and ScriptsPath. Loading a skill also wires /name expansion into agent inputs.

VFS access

The engine exposes its sandbox directly if you need lower-level control:

fs := eng.Workspace()          // *sdk.VFS
root := fs.Root()              // workspace root

See Files for the high-level operations.

Events

See Events & Errors for subscribing to agent and session events from custom components.