improved tool debugging

This commit is contained in:
Luxferre
2026-03-22 12:01:29 +02:00
parent 77d0d17c89
commit cd2de78c2c
16 changed files with 986 additions and 592 deletions
+129 -37
View File
@@ -9,10 +9,13 @@ import (
)
type Sidekick struct {
Config AgentConfig
Model ModelConfig
ToolMapping map[string]string
ToolRegistry map[string]ToolDefinition
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 {
@@ -72,7 +75,8 @@ func (s *Sidekick) constructSystemMessage(agentPool map[string]*Sidekick) string
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")
sb.WriteString(`{"action": "TOOL_CALL", "params": {"tool_name": "x", "arguments": {}}, "reasoning": "..."}` +
"\n")
return sb.String()
}
@@ -84,42 +88,101 @@ func (s *Sidekick) Run(ctx context.Context, inCtx []Message, pool map[string]*Si
}
var tFiles []string
defer func() {
for _, f := range tFiles { os.Remove(f) }
for _, f := range tFiles {
os.Remove(f)
}
}()
iters := s.Config.MaxIterations
if iters <= 0 { iters = 10 }
if iters <= 0 {
iters = 10
}
for i := 0; i < iters; i++ {
msg, err := CallLLM(ctx, s.Model, history, s.ToolRegistry)
if err != nil { return "", fmt.Errorf("LLM call failed: %w", err) }
history = append(history, msg)
act, params, parsed, err := s.parseAction(msg)
done, res, err := s.runStep(ctx, &history, pool, &tFiles)
if err != nil {
if !parsed {
act = ActionTypeRespond
params = map[string]interface{}{"response": msg.Content}
} else {
history = append(history, Message{Role: MessageRoleUser, Content: fmt.Sprintf("Error: %v", err)})
continue
}
return "", err
}
if act == ActionTypeRespond {
if r, ok := params["response"].(string); ok { return r, nil }
return msg.Content, nil
if done {
return res, 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 "Max iterations reached.", nil
}
func (s *Sidekick) dispatch(ctx context.Context, a ActionType, p map[string]interface{}, pl map[string]*Sidekick, tf *[]string) (string, error) {
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 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 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 {
@@ -133,37 +196,66 @@ func (s *Sidekick) dispatch(ctx context.Context, a ActionType, p map[string]inte
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
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 }
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) }
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 !ok {
return "", fmt.Errorf("tool %s not registered", rName)
}
if tDef.Internal {
if tDef.Handler == nil { return "", fmt.Errorf("missing handler") }
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) }
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) }
return sub.Run(ctx, []Message{{Role: MessageRoleUser, Content: task}}, pool)
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 } }
for _, i := range s {
if i == v {
return true
}
}
return false
}