Files
Aska 44e7a5c2c5
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(flow): 实时全链路流程可视化 P1——trace→graph_update→前端全链路流程页(design/19)
后端: trace/graph.go(RunGraph 图构建器,kind/status 映射,父子优先连线,脱敏白名单+截断);
       loop/flow.go(FlowPublisher 按会话增量发布 ws.graph_update,仅主人web会话); loop span
       start/finish 折入图; run.go StartRound/EndRound; wire.go 组装+注入.
前端: Flow.tsx(全链路流程页:FlowGraphView+会话/节点/开始/耗时卡片+状态计数+节点点开日志);
       FlowGraph 组件加 onNodeClick; types/wsParse 扩展; router/sections/AdminApp 注册 flow;
       api-client.md §3.5 graph_update 契约.
验证: 后端 build/vet/test 全绿+flow_test.go 集成(span→graph→ws.graph_update主干); 前端 build 绿.
红线: 单二进制零新依赖; 工具参数/结果脱敏; 仅主人web会话; 身份靠actor元数据; 只读不虚构.
P2(并行子代理分支/泳道/节点实时日志)后置.
2026-09-07 19:10:57 +08:00

185 lines
6.0 KiB
Go

package trace
import (
"errors"
"sync"
"time"
)
// ErrSpanFinished is returned by Span.Record and Span.SetProp when called on a
// span that has already been closed by Finish. A closed span is immutable.
var ErrSpanFinished = errors.New("trace: span already finished")
// SpanKind classifies what a span represents so the M9 panel can group and color
// nodes by stage of the run. The values mirror the span kinds listed in
// DevDocs/design/04-运行时.md §9.
type SpanKind string
const (
// SpanKindLLM is a single LLM invocation.
SpanKindLLM SpanKind = "llm_call"
// SpanKindTool is a tool/function call.
SpanKindTool SpanKind = "tool_call"
// SpanKindOrchestration is an orchestration node, e.g. a plan step.
SpanKindOrchestration SpanKind = "orchestration"
// SpanKindStateTransition is a runtime state transition.
SpanKindStateTransition SpanKind = "state_transition"
// SpanKindSubagent is a spawned sub-agent.
SpanKindSubagent SpanKind = "subagent"
// SpanKindMemory is a memory read or write.
SpanKindMemory SpanKind = "memory"
// SpanKindGuardrail is a guardrail input/output check.
SpanKindGuardrail SpanKind = "guardrail"
)
// SpanStatus is the terminal outcome of a span.
type SpanStatus string
const (
// SpanStatusOK marks a successfully completed span.
SpanStatusOK SpanStatus = "ok"
// SpanStatusError marks a failed span.
SpanStatusError SpanStatus = "error"
)
// Event is a fine-grained record attached to a span, e.g. a token, retry or
// decision. Events enrich the chain-of-thought view.
type Event struct {
// Time is when the event happened. Record sets it to now when left zero.
Time time.Time `json:"time"`
// Type is a short label identifying the event kind.
Type string `json:"type"`
// Props carries event-specific structured data.
Props map[string]any `json:"props,omitempty"`
}
// Span is a single traced unit of work, safe for concurrent use.
//
// A span is created by (*Tracer).StartSpan, which also returns a context that
// carries the span as the current one. Runtime work then records detail with
// Record and SetProp, and closes the span once with Finish. ID, ParentID, Name
// and Kind are fixed at creation; Status, EndedAt and Duration are set when
// Finish runs. Reading a span's fields is safe after Finish returns.
type Span struct {
mu sync.RWMutex
tracer *Tracer
// ID is a span identifier unique across Tracers.
ID string `json:"id"`
// ParentID is the ID of the enclosing span, empty for a root span.
ParentID string `json:"parent_id,omitempty"`
// Name is a human-readable label for the work.
Name string `json:"name"`
// Kind classifies the span.
Kind SpanKind `json:"kind"`
// Status is the terminal outcome, valid after Finish.
Status SpanStatus `json:"status"`
// StartedAt is when the span began.
StartedAt time.Time `json:"started_at"`
// EndedAt is when Finish ran, zero until then.
EndedAt time.Time `json:"ended_at,omitempty"`
// Duration is EndedAt.Sub(StartedAt), zero until Finish.
Duration time.Duration `json:"duration_ns"`
// Props carries key/value metadata attached via SetProp.
Props map[string]any `json:"props,omitempty"`
// Events is the ordered list of events recorded via Record.
Events []Event `json:"events,omitempty"`
}
// Record appends an event to the span. A zero Timestamp is stamped with now.
// Record returns ErrSpanFinished when the span has already been closed.
func (s *Span) Record(evt Event) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.EndedAt.IsZero() {
return ErrSpanFinished
}
if evt.Time.IsZero() {
evt.Time = time.Now()
}
s.Events = append(s.Events, evt)
return nil
}
// SetProp attaches a key/value property to the span for structured output.
// SetProp returns ErrSpanFinished when the span has already been closed.
func (s *Span) SetProp(key string, value any) error {
s.mu.Lock()
defer s.mu.Unlock()
if !s.EndedAt.IsZero() {
return ErrSpanFinished
}
s.Props[key] = value
return nil
}
// Finish closes the span with the given status, stamping EndedAt and Duration
// and handing the completed span to the owning Tracer's Writer. Finish is
// idempotent: calls after the first are ignored.
func (s *Span) Finish(status SpanStatus) {
s.mu.Lock()
if !s.EndedAt.IsZero() {
s.mu.Unlock()
return
}
s.Status = status
s.EndedAt = time.Now()
s.Duration = s.EndedAt.Sub(s.StartedAt)
rec := s.recordLocked()
s.mu.Unlock()
if s.tracer != nil {
s.tracer.emit(rec)
}
}
// recordLocked snapshots the span as a SpanRecord. The caller must hold s.mu.
func (s *Span) recordLocked() SpanRecord {
rec := SpanRecord{
ID: s.ID,
ParentID: s.ParentID,
Name: s.Name,
Kind: s.Kind,
Status: s.Status,
StartedAt: s.StartedAt,
EndedAt: s.EndedAt,
Duration: s.Duration,
}
if len(s.Props) > 0 {
rec.Props = make(map[string]any, len(s.Props))
for k, v := range s.Props {
rec.Props[k] = v
}
}
if len(s.Events) > 0 {
rec.Events = make([]Event, len(s.Events))
copy(rec.Events, s.Events)
}
return rec
}
// Snapshot returns an immutable copy of a finished span as a SpanRecord. It is
// only safe to call after Finish returns (a closed span is immutable), and it
// lets a consumer read the span's final state without holding its lock. Calling
// it on a non-finished span returns a record with a zero Status/EndedAt.
func (s *Span) Snapshot() SpanRecord {
s.mu.RLock()
defer s.mu.RUnlock()
return s.recordLocked()
}
// SpanRecord is an immutable, lock-free snapshot of a completed span, produced
// by Finish and passed to a Writer for persistence. It is safe to copy.
type SpanRecord struct {
ID string `json:"id"`
ParentID string `json:"parent_id,omitempty"`
Name string `json:"name"`
Kind SpanKind `json:"kind"`
Status SpanStatus `json:"status"`
StartedAt time.Time `json:"started_at"`
EndedAt time.Time `json:"ended_at,omitempty"`
Duration time.Duration `json:"duration_ns"`
Props map[string]any `json:"props,omitempty"`
Events []Event `json:"events,omitempty"`
}