Files
Cyrene_v2/internal/runtime/loop/step.go
T
AskaEth 5c28ace70d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(agent,loop): 静音路径工具广告面与流式路径统一(P0-12,审计 §4.3)
- 原缺陷:流式路径经 loop.toolSchemas(capacity 允许集 + restrict)过滤,而 agent 静音/内部路径
  (RunStep,被 services/automations 等使用)广告 dispatcher **全量** ⇒ 两条路径不一致
- 修法(方案 1):**过滤实现只留一份**(loop.toolSchemas);l.agentStep 调用同一个函数,结果经
  agent.WithToolSurface(ctx, …) 下传,agent 的 buildRequest 原样广告(取不到才回退全量,保持既有回退语义)
  · 按构造相同:同一次函数调用、同一份输入 ⇒ 不存在第二份可漂移的实现
  · 未选「抽共享函数两侧各调一次」:loop 的过滤输入含 loop 私有会话环境 ctx key 与 capacity 判定器,
    agent 侧凑齐等于再造一套 ctx 管线,漂移面更大
- 复核:agent/loop 包测试绿;新增 parity 用例(owner 与 group 两组,静音 == 流式);
  变异(撤掉下传行)→ parity 必红且精确复现「静音=全量 4 个」,已还原
- 附注:全仓仅剩 `services/automations` 的「无 Loop 回退」仍广告全量(没有循环就没有闸可言);
  子代理 ToolWhitelist 是执行期否决、本不参与 schema 过滤(未改)
2026-10-04 14:14:05 +08:00

87 lines
3.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package loop
import (
"context"
"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/trace"
)
// stepOutcome 是循环对单个模型步的内部视图,由
// agent 的 *agent.StepResult(非流式)或流式路径构建的等价物
// 归一化而来。
type stepOutcome struct {
final bool
text string
toolCalls []agent.ToolCall
stopReason string
// modelStopReason 是 provider 对该模型调用的原始 stop/finish
// 原因(Anthropic max_tokens / end_turn / tool_use),记录在 llm_call
// span 上,使即使该步本身正常结束,截断也可见。
modelStopReason string
// model 是服务该调用的路由模型,在流式路径上可知
//(该路径自行构建请求),在 agent 路径上为空。它
// 记录在 llm_call span 的日志事件上。
model string
}
// step 运行单次模型调用。配置了 OnStream 回调时,它
// 直接驱动适配器的 ChatStream,把增量串给 OnStream 并
// 应用循环的护栏;否则委托给 agent 的 RunStep,
// 由后者运行 agent 自身的护栏并记录自己的 CallRecord。整个
// 调用被包在 llm_call span 中,使 M9 面板能看到每次 LLM 调用。
func (l *Loop) step(ctx context.Context, sessionID string, messages []model.Message) (*stepOutcome, error) {
_, llmSpan := l.span(ctx, "llm.call", trace.SpanKindLLM)
var (
out *stepOutcome
err error
)
// 静音(内部自主流程)绕过流式:它既不流向
// UI,也不付流式路径的额外开销。正常聊天则流式。
if l.onStream != nil && !isMutedBroadcast(ctx) {
out, err = l.streamStep(ctx, messages)
} else {
out, err = l.agentStep(ctx, sessionID, messages)
}
if err != nil {
l.spanProp(llmSpan, "error", err.Error())
l.finishSpan(llmSpan, trace.SpanStatusError)
return nil, fmt.Errorf("loop: model step: %w", err)
}
l.spanProp(llmSpan, "stop_reason", out.stopReason)
if out.modelStopReason != "" {
l.spanProp(llmSpan, "model_stop_reason", out.modelStopReason)
}
l.spanProp(llmSpan, "tool_calls", len(out.toolCalls))
l.recordLLMDone(llmSpan, out, l.agentFor(ctx))
l.finishSpan(llmSpan, trace.SpanStatusOK)
return out, nil
}
// agentStep 把非流式步委托给该 turn 的 agent,由后者运行输入
// 与输出护栏、调用模型并解码所有工具调用。
//
// 工具面与流式路径**同源**(audit P0-12):两条路径都取这一处过滤结果
// (capacity 允许集 + design/11 §3 会话环境安全门),区别只在于流式路径在
// 循环里组装请求时直接把它写进 model.Request,而本路径经 ctx 下传给 agent
// 广告。agent 侧因此不需要、也不允许自行过滤,两份实现无从漂移;静音内部
// 流程(backthink、子代理)与前端聊天看到的是同一份工具面。
func (l *Loop) agentStep(ctx context.Context, sessionID string, messages []model.Message) (*stepOutcome, error) {
ctx = agent.WithToolSurface(ctx, l.toolSchemas(ctx, messages))
sr, err := l.agentFor(ctx).RunStep(ctx, sessionID, messages)
if err != nil {
return nil, fmt.Errorf("loop: agent step: %w", err)
}
return &stepOutcome{
final: sr.Final,
text: sr.Text,
toolCalls: sr.ToolCalls,
stopReason: sr.StopReason,
modelStopReason: sr.ModelStopReason,
}, nil
}