Files
Cyrene_v2/internal/runtime/model/stream.go
T
AskaEth 6e7ec56f9a
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(loop,gateway): 事件批 — tool_execution 三事件 + streamId + turn.start/end(裁决 01.4.1/01.4.2/01.4.3/01.4.5/03.4.4/1.12)
执行方案阶段 2 · P2-3(`DevDocs/audit/_plan.md`)。契约 `Docs/api-client.md` → **v0.50**。

- **01.4.1 `tool_execution` 拆三事件**:`model/stream.go` 删旧 `StreamKindToolExecution`,新增
  `tool_execution.start/update/end` + `ToolPhaseRunning/Done/Error`(与 design/19 status 同值);
  `StreamEvent` 增 `Tool`/`Phase`;`loop/tools.go` 的 `emitToolExecution` 重写为结构化字段、
  **Delta 恒空**(断言锁死),`ID` 复用 provider tool call id(可与 `tool_call.*` 关联)。
  ⚠️ **诚实边界**:start/end 有真实生产者;`update` 的 wire 映射已实现并单测锁定,但**工具分发器
  无中途进度上报源 ⇒ loop 暂不产出 update 帧**(已在 api-client §3.1 如实登记)。
- **03.4.4 phase 驱动图节点状态**:`invokeOne` 用**单一 phase 决策**同时决定 end 帧 phase、
  span 的 `phase` prop 与终态;`trace/graph.go` 新增 `FlowStatusForPhase`,`graph_build.go` 的
  `AddSpan` 对 tool 节点**优先消费 phase prop**(缺省回落 span 终态 ⇒ 两路不可能矛盾)。
- **01.4.2 `streamId`**:`StreamEvent` 增 `StreamID`;`loop/stream.go` 每次 ChatStream 生成
  `stream-<hex>`(crypto/rand,降级时间戳),该流所有帧共享、多步工具轮取新 id;`ws_bridge`
  把它并进**模型流帧** `data`(`tool_execution.*`/`turn.*` 不带)。语义区分:`turnId` = 哪轮回答,
  `streamId` = 这条流式消息。
- **01.4.3 `turn.start`/`turn.end`**:`loop/run.go` 入口发 start、defer 发 end,**覆盖全部出口**
  (最终答案/安全网终止/预算耗尽/步骤错误);帧**无 data 载荷**;静音的内部 run(backthink/子代理)
  不发。顺带把 91 行的 `Run` 拆成 `driveTurn` + `settleAndReturn`(清掉一处既有 >80 行违规)。
- **01.4.5**:仅 `Docs/api-client.md` §3.5 补 `graph_update` 三范围(`web`/`web:<uuid>`/`sess-backthink`),
  与 `wire_runtime.go` 的 allow 谓词一致,**零代码变更**。

验证:`go build ./...` 绿;本批四包(loop/model/trace/gateway)vet + 测试绿(含 `-count=1`);
linux/amd64 + darwin/arm64 交叉编译绿;本批 16 文件 ≤500 行、函数 ≤80 行。
**真 WS 帧断言 8 条**:帧序 `turn.start → message.delta → tool_execution.start →
tool_execution.end → message.delta → turn.end`(+`stream_end`)、turn 边界无载荷且整轮 turnId 一致、
两条模型流 `streamId` 互异、tool 帧 `{tool,phase,id}` 且无 `delta`/`streamId` 键、
`turn.end` 在步骤出错时也发出、内部静音 run 不发边界帧。
**变异验证**:把 `ev.StreamID = streamID` 改为赋空 → 两条断言 FAIL(`streamId` 为空/两流同空),
还原后复绿(直接删行会先被编译期 `declared and not used` 拦截)。

未验证:前端未消费这些帧(本轮未动 `frontend/**`,「前端能渲染」待前端批);无真实模型端到端。
2026-10-04 11:26:35 +08:00

117 lines
5.9 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 model
// StreamKind enumerates the kinds of structured events produced while a
// completion streams. It mirrors OpenClaw's AssistantMessageEvent segments
// (text / thinking / tool_call) plus a tool-execution progress event, so the
// frontend can render each segment into its own surface (body, collapsed
// thinking block, tool card) rather than accumulating a single opaque token
// stream.
type StreamKind string
const (
// StreamKindTextStart opens the body-text segment. The single text segment
// of a response is bracketed by TextStart/TextEnd; deltas fill it.
StreamKindTextStart StreamKind = "text_start"
// StreamKindTextDelta is one body-text token fragment.
StreamKindTextDelta StreamKind = "text_delta"
// StreamKindTextEnd closes the body-text segment.
StreamKindTextEnd StreamKind = "text_end"
// StreamKindThinkingStart opens a chain-of-thought segment (frontend
// collapsed block). Reserved: adapters that report reasoning emit it.
StreamKindThinkingStart StreamKind = "thinking_start"
// StreamKindThinkingDelta is one reasoning token fragment.
StreamKindThinkingDelta StreamKind = "thinking_delta"
// StreamKindThinkingEnd closes a reasoning segment.
StreamKindThinkingEnd StreamKind = "thinking_end"
// StreamKindToolCallStart opens a tool invocation's streaming form.
StreamKindToolCallStart StreamKind = "tool_call_start"
// StreamKindToolCallDelta is a fragment of the tool arguments (e.g. the
// incremental input_json of an Anthropic tool_use block).
StreamKindToolCallDelta StreamKind = "tool_call_delta"
// StreamKindToolCallEnd closes a tool invocation block.
StreamKindToolCallEnd StreamKind = "tool_call_end"
// StreamKindToolExecutionStart opens the execution of a tool the model
// requested (design/04 §1.1: entering execution). It is emitted by the tool
// loop, not the model adapter; Phase carries the running label.
StreamKindToolExecutionStart StreamKind = "tool_execution_start"
// StreamKindToolExecutionUpdate reports intermediate progress or partial
// output while a requested tool runs (design/04 §1.1). Multiple frames per
// execution are allowed; the wire mapping is defined, and a producer appears
// once tools can report mid-execution progress.
StreamKindToolExecutionUpdate StreamKind = "tool_execution_update"
// StreamKindToolExecutionEnd closes a tool execution with its terminal
// phase (done or error). It is emitted by the tool loop after Invoke
// returns.
StreamKindToolExecutionEnd StreamKind = "tool_execution_end"
// StreamKindTurnStart opens an assistant turn at the loop's run boundary
// (design/04 §1.1 turn.start). It carries no payload: the frame shell's
// session/turn identity brackets the turn on the client.
StreamKindTurnStart StreamKind = "turn_start"
// StreamKindTurnEnd closes an assistant turn at the loop's run boundary,
// however the run settled (final answer, guardrail stop, terminated tool
// loop, exhausted rounds or an error).
StreamKindTurnEnd StreamKind = "turn_end"
)
// 工具执行 phase 标签(design/04 §1.1 细粒度 phase)。取值与 design/19 §1
// 的节点 status 枚举(running/done/error)一致:start/update 帧带 running,
// end 帧带 done 或 error,前端与实时流程图据此驱动工具节点的状态。
const (
// ToolPhaseRunning 标记工具进入执行或仍在执行中。
ToolPhaseRunning = "running"
// ToolPhaseDone 标记工具执行成功结束。
ToolPhaseDone = "done"
// ToolPhaseError 标记工具执行失败结束。
ToolPhaseError = "error"
)
// StreamEvent is a single structured event in a streamed completion. It replaces
// the former plain text delta as the unit delivered by Adapter.ChatStream, so a
// consumer can distinguish body text from tool calls (and, in time, reasoning
// segments) and track each by Index/ID.
type StreamEvent struct {
// Kind classifies the event (see StreamKind).
Kind StreamKind
// Delta carries the incremental payload: body text for TextDelta, reasoning
// text for ThinkingDelta, or a partial_argument fragment for ToolCallDelta.
// Tool-execution events never pack their phase or tool name in here —
// those live in the structured Phase/Tool fields.
Delta string
// Index is the zero-based content-block index the event belongs to, when the
// provider reports one. The frontend uses it to correlate a segment's
// start/delta/end events.
Index int
// ID is the provider block/index identifier for a tool call (e.g. Anthropic's
// tool_use block id). Empty for text segments. Tool-execution events reuse it
// so the client can correlate an execution with its tool_call.* frames.
ID string
// Tool is the invoked tool's name on a tool_execution.* event. Empty on
// every other kind.
Tool string
// Phase is the fine-grained execution label (ToolPhase*) on a
// tool_execution.* event: running on start/update, done or error on end.
// Empty on every other kind.
Phase string
// StreamID identifies the single model stream (one ChatStream call) a frame
// belongs to. The loop stamps it at the stream boundary so every frame of
// the same streamed message shares it, while a multi-step turn's later
// model calls get fresh ids; consumers distinguish it from TurnID, which
// stays constant across the whole turn.
StreamID string
// SessionID is the conversation the streamed event belongs to. It is stamped
// by the loop at the turn boundary (loop.streamStep), which reads the session
// the Run was started for; a model adapter, running below the loop and with no
// notion of a session, leaves it zero. A consumer that routes the event (the
// WS hub) reads it to target the right conversation.
SessionID string
// TurnID is the assistant turn the event belongs to. Like SessionID it is
// stamped by the loop, which reads the turn identity the caller attached with
// loop.WithTurnID; it is zero when no turn id was attached, so a consumer must
// treat both fields as best-effort correlation rather than a hard guarantee.
TurnID string
}