262 lines
7.9 KiB
Go
262 lines
7.9 KiB
Go
package sidekick
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
)
|
|
|
|
type Sidekick struct {
|
|
Config AgentConfig
|
|
Model ModelConfig
|
|
ToolMapping map[string]string
|
|
ToolRegistry map[string]ToolDefinition
|
|
StreamHandler func(string)
|
|
ReasoningHandler func(string)
|
|
IntermediateHandler func(string)
|
|
}
|
|
|
|
func NewSidekick(config AgentConfig, model ModelConfig, extTools []ToolDefinition) *Sidekick {
|
|
model.Resolve()
|
|
s := &Sidekick{
|
|
Config: config,
|
|
Model: model,
|
|
ToolMapping: make(map[string]string),
|
|
ToolRegistry: make(map[string]ToolDefinition),
|
|
}
|
|
for name, tool := range DefaultInternalTools() {
|
|
if name == "shell_exec" && !s.Config.EnableShellExec {
|
|
continue
|
|
}
|
|
s.registerTool(name, tool)
|
|
}
|
|
for _, tool := range extTools {
|
|
if len(s.Config.Toolsets) > 0 && !contains(s.Config.Toolsets, tool.Toolset) {
|
|
continue
|
|
}
|
|
s.registerTool(tool.Name, tool)
|
|
}
|
|
return s
|
|
}
|
|
|
|
func (s *Sidekick) registerTool(realName string, tool ToolDefinition) {
|
|
sanitized := SanitizeToolName(realName)
|
|
s.ToolMapping[sanitized] = realName
|
|
s.ToolRegistry[realName] = tool
|
|
}
|
|
|
|
func (s *Sidekick) constructSystemMessage(agentPool map[string]*Sidekick) string {
|
|
var sb strings.Builder
|
|
sb.WriteString(s.Config.SystemPrompt + "\n\n")
|
|
sb.WriteString(fmt.Sprintf("Role: %s\n\n", s.Config.Role))
|
|
if len(s.Config.Goals) > 0 {
|
|
sb.WriteString("Goals:\n")
|
|
for _, g := range s.Config.Goals {
|
|
sb.WriteString(fmt.Sprintf("- %s\n", g))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
if len(s.ToolRegistry) > 0 {
|
|
sb.WriteString("Available Tools:\n")
|
|
for _, t := range s.ToolRegistry {
|
|
sb.WriteString(fmt.Sprintf("- %s: %s\n", SanitizeToolName(t.Name), t.Description))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
if len(s.Config.Subagents) > 0 {
|
|
sb.WriteString("Available Subagents:\n")
|
|
for _, sid := range s.Config.Subagents {
|
|
if sub, exists := agentPool[sid]; exists {
|
|
sb.WriteString(fmt.Sprintf("- %s: %s\n", sid, sub.Config.Description))
|
|
}
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
sb.WriteString("Action Format:\nJSON object with action. Example:\n")
|
|
sb.WriteString(`{"action": "TOOL_CALL", "params": {"tool_name": "x", "arguments": {}}, "reasoning": "..."}` +
|
|
"\n")
|
|
return sb.String()
|
|
}
|
|
|
|
func (s *Sidekick) Run(ctx context.Context, inCtx []Message, pool map[string]*Sidekick) (string, error) {
|
|
history := make([]Message, len(inCtx))
|
|
copy(history, inCtx)
|
|
if len(history) == 0 || history[0].Role != MessageRoleSystem {
|
|
history = append([]Message{{Role: MessageRoleSystem, Content: s.constructSystemMessage(pool)}}, history...)
|
|
}
|
|
var tFiles []string
|
|
defer func() {
|
|
for _, f := range tFiles {
|
|
os.Remove(f)
|
|
}
|
|
}()
|
|
iters := s.Config.MaxIterations
|
|
if iters <= 0 {
|
|
iters = 10
|
|
}
|
|
for i := 0; i < iters; i++ {
|
|
done, res, err := s.runStep(ctx, &history, pool, &tFiles)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if done {
|
|
return res, nil
|
|
}
|
|
}
|
|
return "Max iterations reached.", nil
|
|
}
|
|
|
|
func (s *Sidekick) logIntermediate(msg Message) {
|
|
if !s.Config.LogIntermediate || s.IntermediateHandler == nil {
|
|
return
|
|
}
|
|
if msg.Content != "" {
|
|
s.IntermediateHandler(fmt.Sprintf("LLM Output: %s", msg.Content))
|
|
} else if len(msg.ToolCalls) > 0 {
|
|
s.IntermediateHandler(fmt.Sprintf("Tool Call: %s", msg.ToolCalls[0].Function.Name))
|
|
}
|
|
}
|
|
|
|
func (s *Sidekick) runStep(ctx context.Context, history *[]Message,
|
|
pool map[string]*Sidekick, tFiles *[]string) (bool, string, error) {
|
|
var sc, rc func(string)
|
|
if s.Config.Streaming {
|
|
if s.StreamHandler != nil {
|
|
sc = s.StreamHandler
|
|
}
|
|
if s.ReasoningHandler != nil {
|
|
rc = s.ReasoningHandler
|
|
}
|
|
}
|
|
msg, err := CallLLM(ctx, s.Model, *history, s.ToolRegistry, sc, rc)
|
|
if err != nil {
|
|
return false, "", fmt.Errorf("LLM call failed: %w", err)
|
|
}
|
|
s.logIntermediate(msg)
|
|
*history = append(*history, msg)
|
|
|
|
act, params, parsed, err := s.parseAction(msg)
|
|
if err != nil {
|
|
if !parsed {
|
|
act, params = ActionTypeRespond, map[string]interface{}{"response": msg.Content}
|
|
} else {
|
|
*history = append(*history, Message{Role: MessageRoleUser, Content: fmt.Sprintf("Error: %v", err)})
|
|
return false, "", nil
|
|
}
|
|
}
|
|
if act == ActionTypeRespond {
|
|
if r, ok := params["response"].(string); ok {
|
|
return true, r, nil
|
|
}
|
|
return true, msg.Content, nil
|
|
}
|
|
res, err := s.dispatch(ctx, act, params, pool, tFiles)
|
|
if err != nil {
|
|
res = fmt.Sprintf("Error: %v", err)
|
|
}
|
|
*history = append(*history, Message{Role: MessageRoleUser, Content: res})
|
|
return false, "", nil
|
|
}
|
|
|
|
func (s *Sidekick) dispatch(ctx context.Context, a ActionType, p map[string]interface{},
|
|
pl map[string]*Sidekick, tf *[]string) (string, error) {
|
|
if a == ActionTypeToolCall {
|
|
tName, _ := p["tool_name"].(string)
|
|
args, _ := p["arguments"].(map[string]interface{})
|
|
if aStr, ok := p["arguments"].(string); ok && args == nil {
|
|
args, _ = ParseArgs(aStr)
|
|
}
|
|
if s.Config.LogIntermediate && s.IntermediateHandler != nil {
|
|
argsJSON, _ := json.Marshal(args)
|
|
s.IntermediateHandler(fmt.Sprintf("Tool Call: %s(%s)", tName, string(argsJSON)))
|
|
}
|
|
res, err := s.executeTool(ctx, tName, args)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if s.Config.LogIntermediate && s.IntermediateHandler != nil {
|
|
truncRes := res
|
|
if len(truncRes) > 200 {
|
|
truncRes = truncRes[:200] + "..."
|
|
}
|
|
s.IntermediateHandler(fmt.Sprintf("Tool Result: %s", truncRes))
|
|
}
|
|
return BufferGate(res, tf, s.Config.ToolResponseThreshold)
|
|
}
|
|
if a == ActionTypeDelegate {
|
|
sid, _ := p["subagent_id"].(string)
|
|
task, _ := p["task"].(string)
|
|
return s.executeDelegation(ctx, sid, task, pl)
|
|
}
|
|
return "", fmt.Errorf("unknown action: %s", a)
|
|
}
|
|
|
|
func (s *Sidekick) parseAction(msg Message) (ActionType, map[string]interface{}, bool, error) {
|
|
if len(msg.ToolCalls) > 0 {
|
|
c := msg.ToolCalls[0]
|
|
return ActionTypeToolCall,
|
|
map[string]interface{}{"tool_name": c.Function.Name, "arguments": c.Function.Arguments}, true, nil
|
|
}
|
|
var env ActionEnvelope
|
|
c := strings.TrimSpace(msg.Content)
|
|
if strings.HasPrefix(c, "```json") && strings.HasSuffix(c, "```") {
|
|
c = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(c, "```json"), "```"))
|
|
}
|
|
if err := json.Unmarshal([]byte(c), &env); err != nil {
|
|
return "", nil, false, err
|
|
}
|
|
return env.Action, env.Params, true, nil
|
|
}
|
|
|
|
func (s *Sidekick) executeTool(ctx context.Context, sName string, args map[string]interface{}) (string, error) {
|
|
rName, ok := s.ToolMapping[sName]
|
|
if !ok {
|
|
return "", fmt.Errorf("tool %s not found", sName)
|
|
}
|
|
tDef, ok := s.ToolRegistry[rName]
|
|
if !ok {
|
|
return "", fmt.Errorf("tool %s not registered", rName)
|
|
}
|
|
if tDef.Internal {
|
|
if tDef.Handler == nil {
|
|
return "", fmt.Errorf("missing handler")
|
|
}
|
|
return tDef.Handler(args)
|
|
}
|
|
return CallExternalTool(ctx, tDef.Toolset, rName, args)
|
|
}
|
|
|
|
func (s *Sidekick) executeDelegation(ctx context.Context, sid string,
|
|
task string, pool map[string]*Sidekick) (string, error) {
|
|
if !contains(s.Config.Subagents, sid) {
|
|
return "", fmt.Errorf("subagent %s not allowed", sid)
|
|
}
|
|
sub, ok := pool[sid]
|
|
if !ok {
|
|
return "", fmt.Errorf("subagent %s not found", sid)
|
|
}
|
|
if s.Config.LogIntermediate && s.IntermediateHandler != nil {
|
|
s.IntermediateHandler(fmt.Sprintf("Delegating to %s: %s", sid, task))
|
|
}
|
|
res, err := sub.Run(ctx, []Message{{Role: MessageRoleUser, Content: task}}, pool)
|
|
if err == nil && s.Config.LogIntermediate && s.IntermediateHandler != nil {
|
|
truncRes := res
|
|
if len(truncRes) > 200 {
|
|
truncRes = truncRes[:200] + "..."
|
|
}
|
|
s.IntermediateHandler(fmt.Sprintf("Subagent %s Result: %s", sid, truncRes))
|
|
}
|
|
return res, err
|
|
}
|
|
|
|
func contains(s []string, v string) bool {
|
|
for _, i := range s {
|
|
if i == v {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|