92 lines
2.8 KiB
Go
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)
|
|
}
|
|
}
|