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