Repo iniciado

This commit is contained in:
2026-03-03 23:19:23 +00:00
commit c126187c5a
32 changed files with 2719 additions and 0 deletions
+76
View File
@@ -0,0 +1,76 @@
package decision
import (
"strings"
)
// Rule maps a condition to a set of actions.
type Rule struct {
Name string
Match MatchFunc
Actions []Action
}
// MatchFunc is a pure predicate over a MessageContext.
type MatchFunc func(ctx MessageContext) bool
// Evaluate runs all rules against the context and returns the matching actions. Pure.
func Evaluate(ctx MessageContext, rules []Rule) []Action {
var actions []Action
for _, rule := range rules {
if rule.Match(ctx) {
actions = append(actions, rule.Actions...)
}
}
return actions
}
// MatchCommand returns a MatchFunc that matches when the command equals cmd.
func MatchCommand(cmd string) MatchFunc {
return func(ctx MessageContext) bool {
return strings.EqualFold(ctx.Command, cmd)
}
}
// MatchPrefix returns a MatchFunc that matches when content starts with prefix.
func MatchPrefix(prefix string) MatchFunc {
return func(ctx MessageContext) bool {
return strings.HasPrefix(ctx.Content, prefix)
}
}
// MatchAny returns a MatchFunc that matches any message.
func MatchAny() MatchFunc {
return func(_ MessageContext) bool { return true }
}
// MatchMinPowerLevel returns a MatchFunc that requires a minimum Matrix power level.
func MatchMinPowerLevel(level int) MatchFunc {
return func(ctx MessageContext) bool {
return ctx.PowerLevel >= level
}
}
// And composes multiple MatchFuncs with logical AND.
func And(fns ...MatchFunc) MatchFunc {
return func(ctx MessageContext) bool {
for _, fn := range fns {
if !fn(ctx) {
return false
}
}
return true
}
}
// Or composes multiple MatchFuncs with logical OR.
func Or(fns ...MatchFunc) MatchFunc {
return func(ctx MessageContext) bool {
for _, fn := range fns {
if fn(ctx) {
return true
}
}
return false
}
}
+64
View File
@@ -0,0 +1,64 @@
// Package decision implements the pure decision engine.
// Input: MessageContext. Output: []Action. Zero side effects.
package decision
import "github.com/enmanuel/agents/pkg/tools"
// MessageContext holds all the information about an incoming message.
type MessageContext struct {
SenderID string
SenderName string
RoomID string
Content string
Command string // parsed command name, e.g. "deploy"
Args []string // parsed arguments
PowerLevel int
IsDirectMsg bool
IsMention bool
ThreadID string
}
// ActionKind is the type of action to perform.
type ActionKind string
const (
ActionKindReply ActionKind = "reply"
ActionKindSSH ActionKind = "ssh"
ActionKindHTTP ActionKind = "http"
ActionKindScript ActionKind = "script"
ActionKindFileOps ActionKind = "file_ops"
ActionKindMCP ActionKind = "mcp"
ActionKindLLM ActionKind = "llm"
ActionKindDelegate ActionKind = "delegate"
)
// Action is a pure description of what the shell should do.
// It is a tagged union — only the field matching Kind is set.
type Action struct {
Kind ActionKind
Reply *ReplyAction
SSH *tools.SSHCommandSpec
HTTP *tools.HTTPCallSpec
Script *tools.ScriptSpec
FileOps *tools.FileOpsSpec
MCP *tools.MCPCallSpec
LLM *LLMAction
Delegate *DelegateAction
}
type ReplyAction struct {
Content string
ThreadID string // empty = new thread
Reaction string // optional Matrix reaction
}
type LLMAction struct {
ContextKey string // key to look up conversation history
ExtraTools []string // additional tool names to make available
}
type DelegateAction struct {
TargetAgentID string
Task string
Context map[string]string
}
+25
View File
@@ -0,0 +1,25 @@
package llm
import "strings"
// Route maps a model string to its provider. Pure function.
func Route(model string) ProviderID {
switch {
case strings.HasPrefix(model, "claude"):
return ProviderAnthropic
case strings.HasPrefix(model, "gpt"), strings.HasPrefix(model, "o1"), strings.HasPrefix(model, "o3"):
return ProviderOpenAI
case strings.HasPrefix(model, "ollama/"):
return ProviderOllama
default:
return ProviderOpenAI
}
}
// ModelName strips the provider prefix from a model string.
func ModelName(model string) string {
if after, ok := strings.CutPrefix(model, "ollama/"); ok {
return after
}
return model
}
+68
View File
@@ -0,0 +1,68 @@
// Package llm defines pure types for LLM provider communication.
// No side effects — only data and transformations.
package llm
import "context"
type Role string
const (
RoleSystem Role = "system"
RoleUser Role = "user"
RoleAssistant Role = "assistant"
RoleTool Role = "tool"
)
type ProviderID string
const (
ProviderAnthropic ProviderID = "anthropic"
ProviderOpenAI ProviderID = "openai"
ProviderOllama ProviderID = "ollama"
)
type Message struct {
Role Role
Content string
ToolCallID string
ToolCalls []ToolCall
}
type ToolCall struct {
ID string
Name string
Arguments string // JSON-encoded
}
type ToolSpec struct {
Name string
Description string
InputSchema map[string]any
}
type CompletionRequest struct {
Model string
Messages []Message
Tools []ToolSpec
MaxTokens int
Temperature float64
Stream bool
SystemPrompt string
}
type TokenUsage struct {
InputTokens int
OutputTokens int
TotalTokens int
}
type CompletionResponse struct {
Content string
ToolCalls []ToolCall
Usage TokenUsage
FinishReason string
}
// CompleteFunc is the single contract for LLM providers.
// Implementations live in shell/llm/.
type CompleteFunc func(ctx context.Context, req CompletionRequest) (CompletionResponse, error)
+28
View File
@@ -0,0 +1,28 @@
package message
import (
"bytes"
"text/template"
)
// Render executes a Go template string with the given data. Pure.
func Render(tmpl string, data any) (string, error) {
t, err := template.New("").Parse(tmpl)
if err != nil {
return "", err
}
var buf bytes.Buffer
if err := t.Execute(&buf, data); err != nil {
return "", err
}
return buf.String(), nil
}
// MustRender is like Render but panics on error. Use only in tests.
func MustRender(tmpl string, data any) string {
s, err := Render(tmpl, data)
if err != nil {
panic(err)
}
return s
}
+43
View File
@@ -0,0 +1,43 @@
// Package message provides pure parsing and formatting for Matrix messages.
package message
import (
"strings"
"github.com/enmanuel/agents/pkg/decision"
)
// ParseOptions configures how messages are parsed.
type ParseOptions struct {
CommandPrefix string // e.g. "!"
BotUserID string // for mention detection
}
// Parse converts a raw Matrix message body into a structured MessageContext. Pure.
func Parse(body, senderID, roomID string, powerLevel int, isDM bool, opts ParseOptions) decision.MessageContext {
ctx := decision.MessageContext{
SenderID: senderID,
RoomID: roomID,
Content: body,
PowerLevel: powerLevel,
IsDirectMsg: isDM,
}
// Detect mention
if opts.BotUserID != "" && strings.Contains(body, opts.BotUserID) {
ctx.IsMention = true
body = strings.ReplaceAll(body, opts.BotUserID, "")
body = strings.TrimSpace(body)
}
// Parse command
if opts.CommandPrefix != "" && strings.HasPrefix(body, opts.CommandPrefix) {
parts := strings.Fields(strings.TrimPrefix(body, opts.CommandPrefix))
if len(parts) > 0 {
ctx.Command = strings.ToLower(parts[0])
ctx.Args = parts[1:]
}
}
return ctx
}
+93
View File
@@ -0,0 +1,93 @@
// Package personality defines pure types for agent personality and behavior.
package personality
type Tone string
const (
ToneDirect Tone = "direct"
ToneFriendly Tone = "friendly"
ToneFormal Tone = "formal"
ToneCasual Tone = "casual"
ToneTechnical Tone = "technical"
)
type Verbosity string
const (
VerbosityMinimal Verbosity = "minimal"
VerbosityConcise Verbosity = "concise"
VerbosityDetailed Verbosity = "detailed"
VerbosityVerbose Verbosity = "verbose"
)
type EmojiStyle string
const (
EmojiNone EmojiStyle = "none"
EmojiMinimal EmojiStyle = "minimal"
EmojiModerate EmojiStyle = "moderate"
EmojiHeavy EmojiStyle = "heavy"
)
type ErrorStyle string
const (
ErrorTerse ErrorStyle = "terse"
ErrorHelpful ErrorStyle = "helpful"
ErrorDetailed ErrorStyle = "detailed"
)
type Templates struct {
Greeting string
UnknownCommand string
PermissionDenied string
Error string
Success string
Busy string
}
type Behavior struct {
Proactive bool
AskConfirmation bool
ShowReasoning bool
ThreadReplies bool
TypingIndicator bool
AcknowledgeReceipt bool
}
type Personality struct {
Tone Tone
Verbosity Verbosity
Language string
LanguagesSupported []string
EmojiStyle EmojiStyle
Prefix string
ErrorStyle ErrorStyle
Templates Templates
Behavior Behavior
}
// DefaultPersonality returns a sensible baseline.
func DefaultPersonality() Personality {
return Personality{
Tone: ToneFriendly,
Verbosity: VerbosityConcise,
Language: "en",
EmojiStyle: EmojiMinimal,
ErrorStyle: ErrorHelpful,
Templates: Templates{
Greeting: "Ready. What do you need?",
UnknownCommand: "Unknown command. Use `!help` for available commands.",
PermissionDenied: "You don't have permission for that.",
Error: "Something failed: {{.Error}}",
Success: "Done. {{.Summary}}",
Busy: "I'm busy with another task. Wait or use `!queue`.",
},
Behavior: Behavior{
AskConfirmation: true,
ThreadReplies: true,
TypingIndicator: true,
AcknowledgeReceipt: true,
},
}
}
+58
View File
@@ -0,0 +1,58 @@
// Package tools defines pure, declarative tool specifications.
// No execution happens here — only data describing what tools exist and their contracts.
package tools
// ToolKind identifies the category of a tool.
type ToolKind string
const (
ToolKindSSH ToolKind = "ssh"
ToolKindHTTP ToolKind = "http"
ToolKindScript ToolKind = "script"
ToolKindFileOps ToolKind = "file_ops"
ToolKindMCP ToolKind = "mcp"
)
// ToolSpec is a pure description of a tool — what it does and what it accepts.
// The actual execution lives in shell/effects/.
type ToolSpec struct {
Name string
Kind ToolKind
Description string
Parameters []ParameterSpec
}
type ParameterSpec struct {
Name string
Type string
Description string
Required bool
}
// Registry is a map of available tools, keyed by name.
type Registry map[string]ToolSpec
// Add returns a new Registry with the given spec added.
func (r Registry) Add(spec ToolSpec) Registry {
out := make(Registry, len(r)+1)
for k, v := range r {
out[k] = v
}
out[spec.Name] = spec
return out
}
// Get looks up a tool spec by name.
func (r Registry) Get(name string) (ToolSpec, bool) {
spec, ok := r[name]
return spec, ok
}
// Names returns all registered tool names.
func (r Registry) Names() []string {
names := make([]string, 0, len(r))
for k := range r {
names = append(names, k)
}
return names
}
+37
View File
@@ -0,0 +1,37 @@
package tools
// SSHCommandSpec describes an SSH command to execute. Pure data — no execution.
type SSHCommandSpec struct {
Target string // references a named target in ssh config
Command string
Timeout string
}
// HTTPCallSpec describes an HTTP call to make. Pure data.
type HTTPCallSpec struct {
Method string
URL string
Headers map[string]string
Body string
Timeout string
}
// ScriptSpec describes a script to run. Pure data.
type ScriptSpec struct {
Name string
Args []string
Timeout string
}
// FileOpsSpec describes a file operation. Pure data.
type FileOpsSpec struct {
Op string // read | write | list | delete
Path string
}
// MCPCallSpec describes a call to an MCP server. Pure data.
type MCPCallSpec struct {
ServerName string
ToolName string
Arguments map[string]any
}