Files
Cyrene_v2/internal/runtime/loop/stream.go
T
Aska 33eeadfc38
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
fix(backthink): 后台思考静默广播——不再把内部推理泄到聊天思维链 + XML 动作/旁白解析
【静默广播修复(bug:思维链露出 backthink 推理)】
- 根因:LoopThinker.Think 复用同一 loop.Loop 且 loop 全局带 OnStream,backthink 后台思考时
  把 [background autonomous reflection] 的 thinking/流式事件广播到前端,被当聊天思维链显示。
- loop/mute_ctx.go:WithMutedBroadcast(ctx) + isMutedBroadcast(ctx)——context 上静默标志。
- loop 四处广播抑制(stream.go 流式 / step.go 静默走非流式 agentStep / request.go capacity 事件 /
  tools.go tool_execution),静默时跳过 UI 广播,推理照常返回结论。
- thinker.go LoopThinker.Think 用 loop.WithMutedBroadcast(ctx),backthink 不再泄 thinking。

【XML 动作/旁白结构化解析】
- cyrene.yaml systemPrompt 加「表达方式:动作与旁白」——动作 <action>…</action>、
  旁白 <narration>…</narration>;克制≤1-2个/段、贴合唤醒态、重要信息不加、朗读正文忽略标签。
- 前端 parseContent.ts:ContentPart[](text/action/narration),非贪婪+反向引用、跨行、非法/
  未闭合兜底纯文本不崩;summarizeContent 供思维导图/状态。
- 前端 renderContent.tsx:action/narration → 灰斜体(mu-text-lo italic)句中原位渲染。
- 应用到 chat.tsx UserBubble/AssistantBubble + messages 各 mode body。

go build/vet/test(loop/backthink)绿 + 前端 tsc/vite 绿。
2026-09-06 22:09:51 +08:00

60 lines
2.0 KiB
Go

package loop
import (
"context"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/agent"
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/model"
)
// streamStep drives a streaming model call. It is the streaming counterpart of
// agent.RunStep: it builds the request from the agent's public surface, runs the
// loop's input guardrail, streams deltas through OnStream, runs the output
// guardrail and decodes any tool calls. Because RunStep does not expose a stream
// path, this method is owned by the loop; it mirrors the agent's semantics so the
// two behave identically apart from the delta callback.
func (l *Loop) streamStep(ctx context.Context, messages []model.Message) (*stepOutcome, error) {
adapter := l.agent.Adapter()
if adapter == nil {
return nil, fmt.Errorf("loop: stream step: %w", ErrNoAdapter)
}
req := l.buildRequest(ctx, messages)
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) {
// Suppress UI broadcast for internal autonomous passes (backthink),
// whose intermediate reasoning must not leak into the chat stream.
if l.onStream != nil && !isMutedBroadcast(ctx) {
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}, nil
}
return &stepOutcome{final: true, text: resp.Content, stopReason: agent.StopReasonStop}, nil
}