- model.StreamEvent 增 SessionID/TurnID(适配器在 loop 之下,恒留零值) - loop 在回合边界注入;覆盖 stream.go 与 tools.go 的 tool_execution 发射点 - 网关 runTurn 每回合生成 turnID 并经 loop.WithTurnID 挂进运行上下文(编排层与 legacy 直连的唯一收口点);broadcastStream 填充 wsEvent - Docs/api-client v0.36 契约与变更日志;v0.10 条目标注为已被补齐 - 新增 loop/gateway 测试,含变异验证
66 lines
2.5 KiB
Go
66 lines
2.5 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) {
|
|
// Stamp the session/turn identity at the loop boundary: the adapter runs
|
|
// below the loop and cannot know which conversation the stream belongs to,
|
|
// so the loop fills it from the context it was driven with (design/16 §4.2)
|
|
// and the frontend can route the delta to the active session.
|
|
ev.SessionID = loopSession(ctx)
|
|
ev.TurnID = loopTurn(ctx)
|
|
// 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, modelStopReason: resp.StopReason}, nil
|
|
}
|
|
return &stepOutcome{final: true, text: resp.Content, stopReason: agent.StopReasonStop, modelStopReason: resp.StopReason}, nil
|
|
}
|