- 重复调用安全网(critical-tool-loop):指纹=name+canonical JSON(args);批内重复第二次跳过回喂
「duplicate call」;跨轮关键重复(同指纹重现)→首次 block+criticalSeen,再次连续重复→终止整轮
StopReasonToolLoopTerminated(替代裸 8 轮针对同调用的硬截止)。
- 并行/串行混合:invokeParallel 默认并行,信号量(ToolParallelism 默认4)限并发,结果按调用索引
写回按原序渲染;串行触发=Options.ToolExecution:sequential 或 SetToolExecutionMode(name,sequential)
(对齐 OpenClaw「一个 sequential 标记→整组串行」);goroutine 不泄漏、ctx 取消快速返回。
- 工具钩子:ToolLoopHooks(BatchAdmit/CallAdmit/AfterCall/AfterOutcome) 经 Options.Hooks 注入,
均 nil-tolerant;供后续敏感操作代审复用(design/11)。terminate 用 every 语义。
- status 分类:ToolError.Kind() 区分 argument-validation(JSON-Schema 失败)/execution;
失败消息渲染 {kind,message} 让模型区分重发参数 vs 执行失败。
- tool-call-repair:LenientJSONUnmarshal 修 fence 包裹/尾逗号畸形;agent.extractToolCalls 与
loop.decodeToolCalls 走它;不可修复退化为文本不崩整轮。意图调用未区分回喂提示(后置)。
- tool_execution_update:流式路径 emitToolExecution 发 StreamKindToolExecution(start/end/end:error)。
go build/vet/test 全绿(46包)。TestRun_MaxRounds 改每轮不同参(原同 echo 被安全网拦,预期新行为)。
-race 受宿主 TSan VMA 限制无法运行(与代码无关)。
85 lines
2.8 KiB
Go
85 lines
2.8 KiB
Go
package loop
|
|
|
|
import (
|
|
"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"
|
|
)
|
|
|
|
// buildRequest assembles a model.Request from the agent's public surface,
|
|
// mirroring agent.buildRequest so the streaming path behaves identically to
|
|
// RunStep (leading instruction message, tool schemas, model routing).
|
|
func (l *Loop) buildRequest(messages []model.Message) model.Request {
|
|
req := model.Request{
|
|
Model: l.agent.Model(),
|
|
Messages: l.assembleMessages(messages),
|
|
Temperature: l.temperature(),
|
|
MaxTokens: l.agent.MaxTokens(),
|
|
Purpose: l.agent.Purpose(),
|
|
Tools: l.toolSchemas(),
|
|
}
|
|
if l.agent.Router() != nil {
|
|
if m, _ := l.agent.Router().RouteRequest(req); m != "" {
|
|
req.Model = m
|
|
}
|
|
}
|
|
return req
|
|
}
|
|
|
|
// assembleMessages prepends the agent's instructions as a leading system message
|
|
// when they are non-empty, otherwise returns the input slice unchanged.
|
|
func (l *Loop) assembleMessages(messages []model.Message) []model.Message {
|
|
instr := l.agent.Instructions()
|
|
if instr == "" {
|
|
return messages
|
|
}
|
|
out := make([]model.Message, 0, len(messages)+1)
|
|
out = append(out, model.TextMessage(model.RoleSystem, instr))
|
|
out = append(out, messages...)
|
|
return out
|
|
}
|
|
|
|
// temperature returns a pointer to the configured temperature, mapping zero to
|
|
// nil so the provider default applies (matching the agent package).
|
|
func (l *Loop) temperature() *float64 {
|
|
t := l.agent.Temperature()
|
|
if t == 0 {
|
|
return nil
|
|
}
|
|
return &t
|
|
}
|
|
|
|
// toolSchemas advertises the registered tool schemas to the model, or nil when no
|
|
// dispatcher is configured.
|
|
func (l *Loop) toolSchemas() []model.ToolSpec {
|
|
d := l.dispatcher()
|
|
if d == nil {
|
|
return nil
|
|
}
|
|
return d.ToolSchemas()
|
|
}
|
|
|
|
// decodeToolCalls decodes tool calls from a model.Response using the same two
|
|
// conventions as the agent package: the normalized RoleTool placeholder, then a
|
|
// JSON envelope of the form {"tool_calls":[...]} in Content. It reports ok=true
|
|
// (with a non-empty slice) when a call is decoded.
|
|
func decodeToolCalls(resp model.Response) ([]agent.ToolCall, bool) {
|
|
if resp.Message.Role == model.RoleAssistant && resp.Message.ToolCallName != "" {
|
|
return []agent.ToolCall{{Name: resp.Message.ToolCallName, Args: map[string]any{}}}, true
|
|
}
|
|
var payload struct {
|
|
ToolCalls []agent.ToolCall `json:"tool_calls"`
|
|
}
|
|
// Lenient decode repairs common model-emitted malformations (code fences,
|
|
// trailing commas) so a slightly broken tool-call envelope does not turn into
|
|
// a text answer; an unrecoverable payload still yields ok=false and the round
|
|
// proceeds as text.
|
|
if err := tool.LenientJSONUnmarshal([]byte(resp.Content), &payload); err != nil {
|
|
return nil, false
|
|
}
|
|
if len(payload.ToolCalls) == 0 {
|
|
return nil, false
|
|
}
|
|
return payload.ToolCalls, true
|
|
}
|