turn_classification.go RawHistoryFolder
1package subagents
2
3import "fmt"
4
5// turnEventClass identifies the boundary that owns a turn event. The worker
6// daemon's turn.started event is the delegated message boundary; provider
7// turn_start events are nested model-loop activity. New workers carry stable
8// lifetime/current counters on the delegated boundary and nested_turn on the
9// provider boundary. The fallbacks preserve protocol-v1 logs, which used
10// turn.started without counters for delegated turns and turn_start for nested
11// turns.
12type turnEventClass uint8
13
14const (
15 turnEventUnknown turnEventClass = iota
16 turnEventDelegated
17 turnEventNested
18)
19
20func classifyTurnEvent(ev Event) turnEventClass {
21 if nested, ok := ev.Data["nested_turn"].(bool); ok && nested {
22 return turnEventNested
23 }
24 switch ev.Type {
25 case "turn_start":
26 return turnEventNested
27 case EventTurnStarted:
28 // v1 turn.started without counters is the delegated boundary. A
29 // current worker marks nested canonical events explicitly above.
30 return turnEventDelegated
31 case "turn_end":
32 if _, ok := eventCounter(ev.Data, "step"); ok {
33 return turnEventDelegated
34 }
35 return turnEventNested
36 }
37 return turnEventUnknown
38}
39
40func isDelegatedTurnStart(ev Event) bool {
41 return ev.Type == EventTurnStarted && classifyTurnEvent(ev) == turnEventDelegated
42}
43
44// IsDelegatedTurnStart is shared with the worker's replay bootstrap so live,
45// replayed, and protocol-v1 event handling use the same boundary rules.
46func IsDelegatedTurnStart(ev Event) bool {
47 return isDelegatedTurnStart(ev)
48}
49
50func isDelegatedTurnEnd(ev Event) bool {
51 return ev.Type == "turn_end" && classifyTurnEvent(ev) == turnEventDelegated
52}
53
54func eventTurnID(ev Event) string {
55 if ev.TurnID != "" {
56 return ev.TurnID
57 }
58 if step, ok := eventCounter(ev.Data, "step"); ok {
59 return fmt.Sprintf("turn-%d", step)
60 }
61 return ""
62}
63
64func userMessageText(ev Event) string {
65 if ev.Type != "user_message" {
66 return ""
67 }
68 content := ev.Data["content"]
69 var text string
70 appendBlock := func(fields map[string]any) {
71 if typ, _ := fields["type"].(string); typ == "text" {
72 if value, _ := fields["text"].(string); value != "" {
73 if text != "" {
74 text += "\n"
75 }
76 text += value
77 }
78 }
79 }
80 switch blocks := content.(type) {
81 case []any:
82 for _, block := range blocks {
83 if fields, ok := block.(map[string]any); ok {
84 appendBlock(fields)
85 }
86 }
87 case []map[string]any:
88 for _, fields := range blocks {
89 appendBlock(fields)
90 }
91 }
92 return text
93}