146 lines
4.1 KiB
Go
146 lines
4.1 KiB
Go
// Package agents defines the Agent runtime that ties core and shell together.
|
|
package agents
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
|
|
"maunium.net/go/mautrix/event"
|
|
|
|
"github.com/enmanuel/agents/internal/config"
|
|
"github.com/enmanuel/agents/pkg/decision"
|
|
coretypes "github.com/enmanuel/agents/pkg/llm"
|
|
"github.com/enmanuel/agents/pkg/personality"
|
|
"github.com/enmanuel/agents/shell/effects"
|
|
shelllm "github.com/enmanuel/agents/shell/llm"
|
|
"github.com/enmanuel/agents/shell/matrix"
|
|
"github.com/enmanuel/agents/shell/ssh"
|
|
)
|
|
|
|
// Agent is the assembled runtime: pure core + impure shell.
|
|
type Agent struct {
|
|
cfg *config.AgentConfig
|
|
personality personality.Personality
|
|
rules []decision.Rule
|
|
llm coretypes.CompleteFunc
|
|
matrix *matrix.Client
|
|
runner *effects.Runner
|
|
listener *matrix.Listener
|
|
logger *slog.Logger
|
|
}
|
|
|
|
// New assembles an Agent from its config, rules, and logger.
|
|
func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*Agent, error) {
|
|
// Matrix client
|
|
matrixClient, err := matrix.New(cfg.Matrix)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("matrix client: %w", err)
|
|
}
|
|
|
|
// SSH executor
|
|
sshExec := ssh.NewExecutor(cfg.SSH)
|
|
|
|
// LLM client
|
|
primaryLLM, err := shelllm.FromConfig(cfg.LLM.Primary)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("primary LLM: %w", err)
|
|
}
|
|
|
|
var llmFunc coretypes.CompleteFunc = primaryLLM
|
|
if cfg.LLM.Fallback.Provider != "" {
|
|
fallbackLLM, err := shelllm.FromConfig(cfg.LLM.Fallback)
|
|
if err != nil {
|
|
logger.Warn("fallback LLM config error", "err", err)
|
|
} else {
|
|
llmFunc = shelllm.WithFallback(primaryLLM, fallbackLLM)
|
|
}
|
|
}
|
|
|
|
// Effects runner
|
|
runner := effects.NewRunner(matrixClient, sshExec, logger)
|
|
|
|
a := &Agent{
|
|
cfg: cfg,
|
|
rules: rules,
|
|
llm: llmFunc,
|
|
matrix: matrixClient,
|
|
runner: runner,
|
|
logger: logger,
|
|
}
|
|
|
|
// Matrix event listener
|
|
a.listener = matrix.NewListener(matrixClient, cfg.Matrix, a.handleEvent, logger)
|
|
|
|
return a, nil
|
|
}
|
|
|
|
// Run starts the agent sync loop. Blocks until ctx is cancelled.
|
|
func (a *Agent) Run(ctx context.Context) error {
|
|
a.logger.Info("agent starting", "id", a.cfg.Agent.ID, "name", a.cfg.Agent.Name)
|
|
return a.listener.Run(ctx)
|
|
}
|
|
|
|
// handleEvent is called by the matrix Listener for each filtered incoming event.
|
|
func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext, evt *event.Event) {
|
|
if a.cfg.Personality.Behavior.TypingIndicator {
|
|
_ = a.matrix.SendTyping(ctx, evt.RoomID.String(), true)
|
|
defer a.matrix.SendTyping(ctx, evt.RoomID.String(), false)
|
|
}
|
|
|
|
actions := decision.Evaluate(msgCtx, a.rules)
|
|
|
|
// If no rules matched and the message mentions the bot or is a DM, use LLM.
|
|
if len(actions) == 0 && (msgCtx.IsMention || msgCtx.IsDirectMsg) {
|
|
actions = []decision.Action{{
|
|
Kind: decision.ActionKindLLM,
|
|
LLM: &decision.LLMAction{ContextKey: msgCtx.RoomID},
|
|
}}
|
|
}
|
|
|
|
if len(actions) == 0 {
|
|
return
|
|
}
|
|
|
|
// Expand LLM actions inline (simplified — real impl would maintain conversation state)
|
|
expanded := make([]decision.Action, 0, len(actions))
|
|
for _, act := range actions {
|
|
if act.Kind == decision.ActionKindLLM {
|
|
reply, err := a.runLLM(ctx, msgCtx)
|
|
if err != nil {
|
|
a.logger.Error("llm error", "err", err)
|
|
expanded = append(expanded, decision.Action{
|
|
Kind: decision.ActionKindReply,
|
|
Reply: &decision.ReplyAction{Content: "Sorry, I encountered an error."},
|
|
})
|
|
} else {
|
|
expanded = append(expanded, decision.Action{
|
|
Kind: decision.ActionKindReply,
|
|
Reply: &decision.ReplyAction{Content: reply},
|
|
})
|
|
}
|
|
} else {
|
|
expanded = append(expanded, act)
|
|
}
|
|
}
|
|
|
|
a.runner.Execute(ctx, evt.RoomID.String(), expanded)
|
|
}
|
|
|
|
func (a *Agent) runLLM(ctx context.Context, msgCtx decision.MessageContext) (string, error) {
|
|
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},
|
|
},
|
|
}
|
|
resp, err := a.llm(ctx, req)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return resp.Content, nil
|
|
}
|