Files
Agent-Squad/squad/progress.go
T
weichen 676a1ede8c Carry tool-call identity and outcome through squad events
Why: F14 squad half - the event port dropped CallID/Status/ErrorReason,
so downstream hosts paired by tool name and showed failures as success.
Evidence: bridge and process-event tests pass (verify-u11b.json);
taps/wrappers preserve the fields; concurrent callbacks copy per-event.
Impact: new omitempty fields only; legacy events byte-identical.
Refs: feature/agent-stack-audit-remediation-b2 U11b
2026-10-22 03:25:28 +00:00

158 lines
4.1 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 squad
import (
"encoding/json"
"sync"
"time"
)
// Event 类型常量(SSE 事件名与此一致)。
const (
EvRunStarted = "run_started"
EvPhase = "phase"
EvStepStarted = "step_started"
EvStepCompleted = "step_completed"
EvStepFailed = "step_failed"
EvStepSkipped = "step_skipped"
EvDetail = "detail" // 成员执行过程帧(agent-native ProcessEvent 投影)
EvDeliverable = "deliverable"
EvHumanInputRequired = "human_input_required"
EvHumanResponded = "human_responded"
EvRunCompleted = "run_completed"
EvRunFailed = "run_failed"
EvRunCancelled = "run_cancelled"
)
// Event 引擎对外事件(可 JSON 序列化,SSE 直接下发)。
type Event struct {
Type string `json:"type"`
RunID string `json:"run_id"`
StepID string `json:"step_id,omitempty"`
Role string `json:"role,omitempty"`
Text string `json:"text,omitempty"`
Code string `json:"code,omitempty"`
Data json.RawMessage `json:"data,omitempty"`
At time.Time `json:"at"`
}
// ProgressSink 事件出口;实现方必须非阻塞或自行缓冲。
type ProgressSink interface {
Emit(ev Event)
}
// ProgressFunc 函数式 ProgressSink。
type ProgressFunc func(ev Event)
func (f ProgressFunc) Emit(ev Event) { f(ev) }
// FanoutSink 多路分发 + 环形缓冲(server 层 SSE 重放用)。
type FanoutSink struct {
mu sync.Mutex
buffer []Event
subs map[int]chan Event
nextSub int
limit int
}
// NewFanoutSink 创建分发器;limit 为事件缓冲上限(超过丢弃最旧)。
func NewFanoutSink(limit int) *FanoutSink {
if limit <= 0 {
limit = 4096
}
return &FanoutSink{subs: make(map[int]chan Event), limit: limit}
}
// Emit 分发事件并写入缓冲。
func (s *FanoutSink) Emit(ev Event) {
if ev.At.IsZero() {
ev.At = time.Now()
}
s.mu.Lock()
defer s.mu.Unlock()
s.buffer = append(s.buffer, ev)
if len(s.buffer) > s.limit {
s.buffer = s.buffer[len(s.buffer)-s.limit:]
}
for _, ch := range s.subs {
select {
case ch <- ev:
default: // 慢消费者丢帧,不阻塞引擎
}
}
}
// Subscribe 订阅后续事件;cursor 为已消费的缓冲长度(0 = 从头重放)。
// 返回事件通道与取消函数。重放部分一次性送入通道后进入实时流。
func (s *FanoutSink) Subscribe(cursor int) (<-chan Event, int, func()) {
ch := make(chan Event, 256)
s.mu.Lock()
id := s.nextSub
s.nextSub++
s.subs[id] = ch
if cursor < 0 {
cursor = 0
}
if cursor > len(s.buffer) {
cursor = len(s.buffer)
}
replay := make([]Event, len(s.buffer)-cursor)
copy(replay, s.buffer[cursor:])
next := len(s.buffer)
s.mu.Unlock()
done := make(chan struct{})
var once sync.Once
var replayDone sync.WaitGroup
replayDone.Add(1)
go func() {
defer replayDone.Done()
for _, ev := range replay {
select {
case <-done:
return
case ch <- ev:
}
}
}()
return ch, next, func() {
once.Do(func() { close(done) })
replayDone.Wait()
s.mu.Lock()
delete(s.subs, id)
s.mu.Unlock()
close(ch)
}
}
// Snapshot 返回缓冲快照与当前游标(重放起点)。
func (s *FanoutSink) Snapshot() ([]Event, int) {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]Event, len(s.buffer))
copy(out, s.buffer)
return out, len(s.buffer)
}
// NopSink 丢弃所有事件。
type NopSink struct{}
func (NopSink) Emit(Event) {}
// processEventData 把过程帧投影成 detail 事件的 Data(JSON 对象)。
//
// 工具调用身份与真实结局(call_id / status / error_reason)非空时随帧透传,
// 使 detail 事件流能按 CallID 配对交错的并行工具调用,而不是按到达顺序猜。
// 三者全空(legacy 上游未填充)时输出与既有投影逐字节一致,不引入新键。
func processEventData(ev ProcessEvent) json.RawMessage {
data := map[string]string{"process_type": ev.Type, "tool": ev.Tool, "detail": ev.Detail}
if ev.CallID != "" {
data["call_id"] = ev.CallID
}
if ev.Status != "" {
data["status"] = ev.Status
}
if ev.ErrorReason != "" {
data["error_reason"] = ev.ErrorReason
}
return marshalJSON(data)
}