feat(gateway,loop): A1 流式帧附加 sessionId/turnId — 多会话并发流式可路由
- 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 测试,含变异验证
This commit is contained in:
1 parent
29aa60d0eb
commit
61cc32c44b
9 files changed
+332
-10
No files matched your search
@@ -80,6 +80,12 @@ func (h *hub) streamChat(ctx context.Context, payload any) {
|
||||
// emits a single whole-text chunk to hold the stream contract.
|
||||
func (h *hub) runTurn(ctx context.Context, sessionID string, msgs []model.Message, env loop.SessionEnv) (*loop.Result, error) {
|
||||
turnID := newTurnID()
|
||||
// Thread the turn identity into the run context so the loop stamps it on every
|
||||
// streamed event it emits (message.*/thinking.*/tool_call.*); the hub then puts
|
||||
// it on the outbound frame. Every turn takes a fresh id — a session has at most
|
||||
// one in-flight turn — so a client can correlate one burst of deltas to one
|
||||
// assistant turn (design/16 §4.2).
|
||||
ctx = loop.WithTurnID(ctx, turnID)
|
||||
if h.runner == nil || len(msgs) == 0 {
|
||||
h.broadcast(wsEvent{Type: "stream_end", TurnID: turnID, Seq: int(h.streamSeq.Add(1)), SessionID: sessionID, Data: map[string]any{"stopReason": "idle"}})
|
||||
return &loop.Result{StopReason: "idle"}, nil
|
||||
@@ -117,16 +123,17 @@ func (h *hub) handleInterrupt(payload json.RawMessage) {
|
||||
// broadcastStream maps a streamed model.StreamEvent to its downstream WS event
|
||||
// type and fans it out to every client. It runs on the bus subscription worker;
|
||||
// seq is assigned atomically so a burst stays ordered even if the loop emits
|
||||
// events from more than one goroutine. TurnId is left empty here because the
|
||||
// OnStream bus payload does not carry the turn identity (future work would
|
||||
// thread a turn id through loop → streamChunk → payload).
|
||||
// events from more than one goroutine. SessionID/TurnID ride on the event itself
|
||||
// (the loop stamps them at the turn boundary from the run context), so the frame
|
||||
// carries the scope a client needs to route it to the active session and to one
|
||||
// assistant turn.
|
||||
func (h *hub) broadcastStream(payload any) {
|
||||
ev, ok := payload.(model.StreamEvent)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
typ, data := streamEventData(ev)
|
||||
h.broadcast(wsEvent{Type: typ, Seq: int(h.streamSeq.Add(1)), Data: data})
|
||||
h.broadcast(wsEvent{Type: typ, SessionID: ev.SessionID, TurnID: ev.TurnID, Seq: int(h.streamSeq.Add(1)), Data: data})
|
||||
}
|
||||
|
||||
// chatStreamRequest is the decoded ws.chat payload.
|
||||
|
||||
Reference in new issue
Block a user