Files
Cyrene_v2/internal/runtime/loop/tools.go
T

92 lines
2.8 KiB
Go

package loop
import (
"context"
"encoding/json"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/tool"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/trace"
)
// runToolCalls executes each requested tool call, appending a RoleTool result
// message (success or failure) so the model can continue. It returns the updated
// message slice and the number of tool calls invoked. Invocation is delegated to
// the dispatched tool, which applies the per-tool deadline; a missing dispatcher
// is fed back as an error so the loop never panics or crashes on an unhandled
// call.
func (l *Loop) runToolCalls(ctx context.Context, messages []model.Message, calls []agent.ToolCall) ([]model.Message, int, error) {
d := l.dispatcher()
out := messages
executed := 0
for _, tc := range calls {
var (
result any
invokeErr error
)
if d == nil {
invokeErr = fmt.Errorf("no tool dispatcher configured for %q", tc.Name)
} else {
_, toolSpan := l.span(ctx, "tool."+tc.Name, trace.SpanKindTool)
result, invokeErr = d.Invoke(ctx, tc.Name, tc.Args)
if invokeErr != nil {
l.spanProp(toolSpan, "error", invokeErr.Error())
l.finishSpan(toolSpan, trace.SpanStatusError)
} else {
l.spanProp(toolSpan, "result", result)
l.finishSpan(toolSpan, trace.SpanStatusOK)
}
}
executed++
out = append(out, toolResultMessage(tc, result, invokeErr))
}
return out, executed, nil
}
// dispatcher returns the loop's tool dispatcher, falling back to the agent's own
// when the loop was not given one.
func (l *Loop) dispatcher() *tool.Dispatcher {
if l.tools != nil {
return l.tools
}
if l.agent != nil {
return l.agent.Tools()
}
return nil
}
// toolResultMessage renders a tool call's outcome as a RoleTool message ready to
// be fed back to the model. Errors are included in the text so the LLM can
// react to a failed or timed-out call.
func toolResultMessage(tc agent.ToolCall, result any, invokeErr error) model.Message {
text := fmt.Sprintf("tool %s result: %s", tc.Name, formatToolResult(result))
if invokeErr != nil {
text = fmt.Sprintf("tool %s failed: %v", tc.Name, invokeErr)
}
return model.Message{Role: model.RoleTool, ToolCallName: tc.Name,
Content: []model.Content{{Type: model.ContentTypeText, Text: text}}}
}
// formatToolResult renders a tool return value as text for the model. Strings are
// passed through (an empty string becomes "empty"); everything else is JSON
// encoded, falling back to a plain %v render on an encoding error.
func formatToolResult(result any) string {
switch v := result.(type) {
case nil:
return "null"
case string:
if v == "" {
return "empty"
}
return v
default:
b, err := json.Marshal(v)
if err != nil {
return fmt.Sprintf("%v", v)
}
return string(b)
}
}