Files
Cyrene_v2/internal/runtime/loop/request.go
T
Aska 35cb2d3942
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(loop): Top5#4 工具循环增强(对齐 OpenClaw agent-loop)——重复检测/并行串行/钩子/errorKind/repair
- 重复调用安全网(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 限制无法运行(与代码无关)。
2026-09-06 17:57:18 +08:00

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
}