package loop import ( "context" "fmt" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent" "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model" ) // streamStep 驱动一次流式模型调用。它是 // agent.RunStep 的流式对应物:它从 agent 的公开表面构建请求、 // 运行循环的输入护栏、把增量流经 OnStream、运行输出 // 护栏并解码所有工具调用。由于 RunStep 不暴露流式 // 路径,该方法归循环所有;它镜像 agent 的语义,使两者 // 除增量回调外行为一致。 func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepOutcome, error) { ag := l.agentFor(ctx) adapter := ag.Adapter() if adapter == nil { return nil, fmt.Errorf("loop: stream step: %w", ErrNoAdapter) } // 每个 step 解析一次该角色的 agent 与推理旋钮:同一对取值 // 既用于请求,也用于逐增量的广播门,因此角色切换 // 绝不会在流中途用旧的广播开关套用新的提示词。 knobs := l.knobsFor(ctx) req := l.buildRequest(ctx, messages, ag, knobs) if l.guards != nil { verdict, err := l.guards.CheckInput(ctx, &req) if err != nil { return nil, fmt.Errorf("loop: check input: %w", err) } if verdict != nil && !verdict.Allow { return &stepOutcome{final: true, text: verdict.Reason, stopReason: agent.StopReasonGuardrailInput}, nil } } resp, err := adapter.ChatStream(ctx, req, func(ev model.StreamEvent) { // 在循环边界打上 session/turn 身份:适配器运行在 // 循环之下,无法知道该流属于哪个会话, // 因此循环用它被驱动的 context 填充(design/16 §4.2), // 前端便能把增量路由到活跃会话。 ev.SessionID = loopSession(ctx) ev.TurnID = loopTurn(ctx) // 为内部自主流程(backthink)抑制 UI 广播, // 其中间推理不得泄露进聊天流;当该 turn 的角色 // 关闭思考帧时(showToUser: false)也抑制。两处门 // 都只影响广播:模型产出的响应 // 不变,因此展示与持久化保持解耦。 if l.onStream != nil && !isMutedBroadcast(ctx) && broadcasts(ev, knobs.ThinkingShowToUser) { l.onStream(ev) } }) if err != nil { return nil, fmt.Errorf("loop: chat stream: %w", err) } if l.guards != nil { verdict, err := l.guards.CheckOutput(ctx, &resp) if err != nil { return nil, fmt.Errorf("loop: check output: %w", err) } if verdict != nil && !verdict.Allow { return &stepOutcome{final: true, text: verdict.Reason, stopReason: agent.StopReasonGuardrailOutput}, nil } } if calls, ok := decodeToolCalls(resp); ok { return &stepOutcome{final: false, toolCalls: calls, stopReason: agent.StopReasonToolCalls, modelStopReason: resp.StopReason, model: resp.Model}, nil } return &stepOutcome{final: true, text: resp.Content, stopReason: agent.StopReasonStop, modelStopReason: resp.StopReason, model: resp.Model}, nil } // broadcasts 报告某流式事件是否可推给 UI。只有 // 思考帧会被扣下,且仅当该 turn 的角色把 // showToUser 解析为 false 时;文本、工具调用与工具进度帧 // 从不受影响,因此关闭思考不会改变客户端所见 // 的答案本身。 func broadcasts(ev model.StreamEvent, showToUser *bool) bool { if !isThinkingKind(ev.Kind) { return true } return showToUser == nil || *showToUser } // isThinkingKind 报告 kind 是否为推理分段事件之一。 func isThinkingKind(kind model.StreamKind) bool { switch kind { case model.StreamKindThinkingStart, model.StreamKindThinkingDelta, model.StreamKindThinkingEnd: return true default: return false } }