resident_projection.go RawHistoryFolder
1package subagents
2
3import (
4 "sync"
5
6 "github.com/bnema/zut/packages/core"
7)
8
9const residentLiveArgumentBytes = 64 << 10
10
11// ResidentLiveTool is the bounded in-memory state of one active tool call.
12// It is never written to the journal; finalized calls and results are served
13// from the authoritative transcript instead.
14type ResidentLiveTool struct {
15 ID string
16 Name string
17 Args []byte
18 State ResidentLiveToolState
19}
20
21type ResidentLiveToolState string
22
23const (
24 ResidentLiveToolComposing ResidentLiveToolState = "composing"
25 ResidentLiveToolReady ResidentLiveToolState = "ready"
26 ResidentLiveToolRunning ResidentLiveToolState = "running"
27)
28
29// ResidentLiveSnapshot is an immutable copy of the unfinished visible turn.
30// Hidden reasoning and unbounded tool output deliberately do not belong here.
31type ResidentLiveSnapshot struct {
32 TurnID string
33 State ResidentState
34 Revision uint64
35 WaitingForModel bool
36 AssistantText string
37 Tools []ResidentLiveTool
38}
39
40type residentLiveProjection struct {
41 mu sync.RWMutex
42 snapshot ResidentLiveSnapshot
43}
44
45func newResidentLiveProjection() *residentLiveProjection { return &residentLiveProjection{} }
46
47func (p *residentLiveProjection) Start(turnID string) {
48 if p == nil {
49 return
50 }
51 p.mu.Lock()
52 p.snapshot.TurnID = turnID
53 p.snapshot.State = ResidentRunning
54 p.snapshot.WaitingForModel = true
55 p.snapshot.AssistantText = ""
56 p.snapshot.Tools = nil
57 p.snapshot.Revision++
58 p.mu.Unlock()
59}
60
61func (p *residentLiveProjection) Finish(state ResidentState) {
62 if p == nil {
63 return
64 }
65 p.mu.Lock()
66 p.snapshot.State = state
67 p.snapshot.WaitingForModel = false
68 p.snapshot.AssistantText = ""
69 p.snapshot.Tools = nil
70 p.snapshot.Revision++
71 p.mu.Unlock()
72}
73
74func (p *residentLiveProjection) Apply(event core.AgentEvent) {
75 if p == nil || event == nil {
76 return
77 }
78 p.mu.Lock()
79 defer p.mu.Unlock()
80 changed := true
81 switch value := event.(type) {
82 case core.EvRequestStarted:
83 p.snapshot.WaitingForModel = true
84 case core.EvAssistantStart:
85 p.snapshot.WaitingForModel = true
86 case core.EvTextDelta:
87 p.snapshot.WaitingForModel = false
88 if len(p.snapshot.AssistantText) < residentLiveArgumentBytes {
89 remaining := residentLiveArgumentBytes - len(p.snapshot.AssistantText)
90 if len(value.Delta) > remaining {
91 value.Delta = value.Delta[:remaining]
92 }
93 p.snapshot.AssistantText += value.Delta
94 }
95 case core.EvToolUseStart:
96 p.snapshot.WaitingForModel = false
97 p.upsertTool(value.ID, value.Name, ResidentLiveToolComposing)
98 case core.EvToolUseArgs:
99 p.snapshot.WaitingForModel = false
100 if tool := p.tool(value.ID); tool != nil && len(tool.Args) < residentLiveArgumentBytes {
101 remaining := residentLiveArgumentBytes - len(tool.Args)
102 if len(value.Delta) > remaining {
103 value.Delta = value.Delta[:remaining]
104 }
105 tool.Args = append(tool.Args, value.Delta...)
106 }
107 case core.EvToolCall:
108 p.snapshot.WaitingForModel = false
109 tool := p.upsertTool(value.ID, value.Name, ResidentLiveToolReady)
110 tool.Args = append(tool.Args[:0], value.Args...)
111 if len(tool.Args) > residentLiveArgumentBytes {
112 tool.Args = tool.Args[:residentLiveArgumentBytes]
113 }
114 case core.EvToolExecutionStarted:
115 p.snapshot.WaitingForModel = false
116 p.upsertTool(value.ID, value.Name, ResidentLiveToolRunning)
117 case core.EvToolResult:
118 p.snapshot.WaitingForModel = false
119 for index := range p.snapshot.Tools {
120 if p.snapshot.Tools[index].ID == value.ID {
121 p.snapshot.Tools = append(p.snapshot.Tools[:index], p.snapshot.Tools[index+1:]...)
122 break
123 }
124 }
125 case core.EvAssistantMessage:
126 p.snapshot.WaitingForModel = false
127 p.snapshot.AssistantText = ""
128 default:
129 changed = false
130 }
131 if changed {
132 p.snapshot.Revision++
133 }
134}
135
136func (p *residentLiveProjection) Snapshot() ResidentLiveSnapshot {
137 if p == nil {
138 return ResidentLiveSnapshot{}
139 }
140 p.mu.RLock()
141 snapshot := p.snapshot
142 snapshot.Tools = make([]ResidentLiveTool, len(p.snapshot.Tools))
143 for index, tool := range p.snapshot.Tools {
144 snapshot.Tools[index] = tool
145 snapshot.Tools[index].Args = append([]byte(nil), tool.Args...)
146 }
147 p.mu.RUnlock()
148 return snapshot
149}
150
151func (p *residentLiveProjection) tool(id string) *ResidentLiveTool {
152 for index := range p.snapshot.Tools {
153 if p.snapshot.Tools[index].ID == id {
154 return &p.snapshot.Tools[index]
155 }
156 }
157 return nil
158}
159
160func (p *residentLiveProjection) upsertTool(id, name string, state ResidentLiveToolState) *ResidentLiveTool {
161 if tool := p.tool(id); tool != nil {
162 if name != "" {
163 tool.Name = name
164 }
165 tool.State = state
166 return tool
167 }
168 p.snapshot.Tools = append(p.snapshot.Tools, ResidentLiveTool{ID: id, Name: name, State: state})
169 return &p.snapshot.Tools[len(p.snapshot.Tools)-1]
170}