feat(loop): agent loop(8 轮 + checkpoint + 流式 + 记忆注入/提取 + trace + 工具超时回喂)
This commit is contained in:
1 parent
01a4e29f84
commit
71697ca4f8
12 files changed
+1292
No files matched your search
@@ -0,0 +1,91 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user