Skip to content

Commit 86991f8

Browse files
fix[plugins](socai): added context window compactor
1 parent e413cac commit 86991f8

3 files changed

Lines changed: 111 additions & 12 deletions

File tree

plugins/soc-ai/config/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ type Config struct {
3434
AuthHeaderName string
3535
CustomHeaders map[string]string
3636
MaxTokens int
37+
ContextWindow int
3738

3839
// Agent behavior (from YAML)
3940
MaxToolIterations int
@@ -53,6 +54,7 @@ type fileConfig struct {
5354
AuthHeaderName string `yaml:"auth_header_name"`
5455
CustomHeaders map[string]string `yaml:"custom_headers"`
5556
MaxTokens int `yaml:"max_tokens"`
57+
ContextWindow int `yaml:"context_window"`
5658
MaxToolIterations int `yaml:"max_tool_iterations"`
5759
AutoAnalyze bool `yaml:"auto_analyze"`
5860
Capabilities []string `yaml:"capabilities"`
@@ -242,6 +244,7 @@ func readConfig(path, encKey, backend, internalKey string) *Config {
242244
c.AuthHeaderName = fc.AuthHeaderName
243245
c.CustomHeaders = customHeaders
244246
c.MaxTokens = maxTokens
247+
c.ContextWindow = fc.ContextWindow
245248
c.MaxToolIterations = maxIters
246249
c.AutoAnalyze = fc.AutoAnalyze
247250
c.Capabilities = fc.Capabilities

plugins/soc-ai/internal/agent/loop.go

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,18 +4,49 @@ import (
44
"context"
55
"encoding/json"
66
"fmt"
7+
"strings"
78
"sync/atomic"
9+
10+
"github.com/threatwinds/go-sdk/catcher"
11+
)
12+
13+
const (
14+
defaultMaxIters = 12
15+
compactionThreshold = 0.80
16+
summaryMaxTokens = 400 // ~200 words + slack
817
)
918

10-
const defaultMaxIters = 12
19+
// ponytail: prefix map, kept short. Add entries when a new model family lands.
20+
var modelContextWindow = []struct {
21+
prefix string
22+
window int
23+
}{
24+
{"claude", 200000},
25+
{"gpt-5", 400000},
26+
{"gpt-4o", 128000},
27+
{"gpt-4-turbo", 128000},
28+
{"gpt-4", 8192},
29+
{"gpt-3.5", 16385},
30+
}
31+
32+
func resolveContextWindow(model string) int {
33+
m := strings.ToLower(model)
34+
for _, e := range modelContextWindow {
35+
if strings.HasPrefix(m, e.prefix) {
36+
return e.window
37+
}
38+
}
39+
return 128000
40+
}
1141

1242
type EventKind string
1343

1444
const (
15-
EventToolCall EventKind = "tool_call"
16-
EventToolResult EventKind = "tool_result"
17-
EventFinal EventKind = "final"
18-
EventError EventKind = "error"
45+
EventToolCall EventKind = "tool_call"
46+
EventToolResult EventKind = "tool_result"
47+
EventFinal EventKind = "final"
48+
EventError EventKind = "error"
49+
EventCompaction EventKind = "compaction"
1950
)
2051

2152
type Event struct {
@@ -51,14 +82,21 @@ type RunResult struct {
5182
}
5283

5384
type Agent struct {
54-
llm LLMClient
55-
broker *ToolBroker
56-
model string
57-
maxTokens int
85+
llm LLMClient
86+
broker *ToolBroker
87+
model string
88+
maxTokens int
89+
contextWindow int // 0 = compaction disabled
5890
}
5991

60-
func New(llm LLMClient, broker *ToolBroker, model string, maxTokens int) *Agent {
61-
return &Agent{llm: llm, broker: broker, model: model, maxTokens: maxTokens}
92+
func New(llm LLMClient, broker *ToolBroker, model string, maxTokens, contextWindow int) *Agent {
93+
cw := contextWindow
94+
if cw == 0 {
95+
cw = resolveContextWindow(model)
96+
} else if cw < 0 {
97+
cw = 0
98+
}
99+
return &Agent{llm: llm, broker: broker, model: model, maxTokens: maxTokens, contextWindow: cw}
62100
}
63101

64102
func (a *Agent) Broker() *ToolBroker { return a.broker }
@@ -85,6 +123,20 @@ func (a *Agent) Run(ctx context.Context, task RunTask, sink EventSink) (RunResul
85123

86124
for step := 1; step <= maxIters; step++ {
87125
result.Steps = step
126+
127+
if a.contextWindow > 0 && len(msgs) > 1 &&
128+
estimateTokens(task.System, msgs) >= int(compactionThreshold*float64(a.contextWindow)) {
129+
newMsgs, cErr := a.compact(ctx, task.Input, msgs)
130+
if cErr != nil {
131+
_ = catcher.Error("context compaction failed, continuing with full history", cErr, map[string]any{
132+
"process": "plugin_com.utmstack.soc-ai",
133+
})
134+
} else {
135+
msgs = newMsgs
136+
sink.emit(Event{Kind: EventCompaction, Step: step})
137+
}
138+
}
139+
88140
resp, err := a.llm.Complete(ctx, CompletionRequest{
89141
System: task.System,
90142
Messages: msgs,
@@ -154,6 +206,50 @@ func filterTools(specs []ToolSpec, task RunTask) []ToolSpec {
154206
return out
155207
}
156208

209+
// ponytail: chars/4 estimate; swap for a real tokenizer only if it misdetects
210+
func estimateTokens(system string, msgs []Message) int {
211+
n := len(system)
212+
for _, m := range msgs {
213+
n += len(m.Content)
214+
for _, tc := range m.ToolCalls {
215+
n += len(tc.Name) + len(tc.Args)
216+
}
217+
if m.ToolResult != nil {
218+
n += len(m.ToolResult.Content) + len(m.ToolResult.Name)
219+
}
220+
}
221+
return n / 4
222+
}
223+
224+
func (a *Agent) compact(ctx context.Context, userInput string, msgs []Message) ([]Message, error) {
225+
var b strings.Builder
226+
for _, m := range msgs {
227+
fmt.Fprintf(&b, "[%s] %s\n", m.Role, m.Content)
228+
for _, tc := range m.ToolCalls {
229+
fmt.Fprintf(&b, " tool_call %s(%s)\n", tc.Name, string(tc.Args))
230+
}
231+
if m.ToolResult != nil {
232+
fmt.Fprintf(&b, " tool_result %s: %s\n", m.ToolResult.Name, m.ToolResult.Content)
233+
}
234+
}
235+
resp, err := a.llm.Complete(ctx, CompletionRequest{
236+
System: "Summarize the following SOC analyst conversation in ~200 words. Preserve key facts, tool outputs, decisions, and unresolved next steps. Do not use tools.",
237+
Messages: []Message{{Role: RoleUser, Content: b.String()}},
238+
Model: a.model,
239+
MaxTokens: summaryMaxTokens,
240+
})
241+
if err != nil {
242+
return msgs, err
243+
}
244+
if strings.TrimSpace(resp.Content) == "" {
245+
return msgs, fmt.Errorf("empty summary")
246+
}
247+
return []Message{{
248+
Role: RoleUser,
249+
Content: "Original task:\n" + userInput + "\n\nProgress so far (summary of prior context):\n" + resp.Content + "\n\nContinue the task.",
250+
}}, nil
251+
}
252+
157253
var current atomic.Pointer[Agent]
158254

159255
func SetCurrent(a *Agent) {

plugins/soc-ai/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ func applyConfigUpdates() {
5555
CustomHeaders: cfg.CustomHeaders,
5656
})
5757
broker := agent.NewToolBroker(cfg.Backend, cfg.InternalKey)
58-
agent.SetCurrent(agent.New(llm, broker, cfg.Model, cfg.MaxTokens))
58+
agent.SetCurrent(agent.New(llm, broker, cfg.Model, cfg.MaxTokens, cfg.ContextWindow))
5959
catcher.Info("SOC-AI agent configured", map[string]any{
6060
"process": "plugin_com.utmstack.soc-ai",
6161
"provider": cfg.Provider,

0 commit comments

Comments
 (0)