diff --git a/pkg/agent/core.go b/pkg/agent/core.go index 94cbcfd..6a29106 100644 --- a/pkg/agent/core.go +++ b/pkg/agent/core.go @@ -34,6 +34,7 @@ type AgentEnv struct { tools []backend.Tool exec toolExecutor classifyTool toolClassifier + classifyTier func(name string) ResultTier Hooks Hooks preamble string genParams backend.GenerationParams @@ -138,6 +139,22 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv } } + var tierFn func(string) ResultTier + if srv, ok := d.GetServer("execute"); ok { + if tc, ok := srv.(tools.TierClassifier); ok { + tierFn = func(name string) ResultTier { + switch tc.ResultTier(name) { + case "cold": + return TierCold + case "warm": + return TierWarm + default: + return TierHot + } + } + } + } + var backendName, modelName string if cfg != nil { backendName = cfg.Backend @@ -149,6 +166,7 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv tools: allTools, exec: exec, classifyTool: classify, + classifyTier: tierFn, Hooks: hooks, preamble: preamble, genParams: genParams, @@ -352,6 +370,7 @@ func NewAgentCore(cfg AgentCoreConfig) Core { Tools: cfg.Env.tools, Exec: cfg.Env.exec, ClassifyTool: cfg.Env.classifyTool, + ClassifyTier: cfg.Env.classifyTier, ToolResultMaxBytes: defaultToolResultMaxBytes, GenerationParams: cfg.Env.genParams, MaxSteps: cfg.Env.maxSteps, diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index cee06e6..bdd1a62 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -38,8 +38,9 @@ type agentConfig struct { Backend backend.Backend Tools []backend.Tool Exec toolExecutor - ClassifyTool func(name string) bool // nil=treat all as serial; true=parallel-read-safe - ToolResultMaxBytes int // 0=unlimited; truncate tool results beyond this size + ClassifyTool func(name string) bool // nil=treat all as serial; true=parallel-read-safe + ClassifyTier func(name string) ResultTier // nil=TierHot for all; tools self-classify retention + ToolResultMaxBytes int // 0=unlimited; truncate tool results beyond this size Output EventHandler preamble string // compiled system+agent prompt sent as the system role GenerationParams backend.GenerationParams @@ -332,7 +333,11 @@ func run(ctx context.Context, cfg agentConfig, state state) error { resultCache.Store(tc.Name+"\x00"+string(tc.Arguments), result) } emit(cfg, Event{Role: "tool", Name: tc.Name, Content: result}) - return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: result, IsError: isErr}, false + tier := TierHot + if !isErr && cfg.ClassifyTier != nil { + tier = cfg.ClassifyTier(tc.Name) + } + return toolResult{ToolCallID: tc.ID, Name: tc.Name, Content: result, IsError: isErr, Tier: tier}, false } isParallelSafe := func(name string) bool { diff --git a/pkg/agent/session.go b/pkg/agent/session.go index be6f0b8..b86495f 100644 --- a/pkg/agent/session.go +++ b/pkg/agent/session.go @@ -216,14 +216,28 @@ func (s *Session) recordTurnCost(model string) { func (s *Session) update(assistant backend.Message, results []toolResult) { s.messages = append(s.messages, assistant) for _, r := range results { + content := r.Content + if r.Tier == TierCold && !r.IsError { + // Immediately collapse cold results to a one-line summary. + content = summarizeToolResult(r.Name, content) + } s.messages = append(s.messages, backend.Message{ Role: "tool", - Content: r.Content, + Content: content, ToolCallID: r.ToolCallID, }) } } +// summarizeToolResult produces a compact one-line summary of a tool result +// for cold-tier results that don't need to stay verbatim. +func summarizeToolResult(name, content string) string { + if len(content) <= 200 { + return content + } + return fmt.Sprintf("[%s: %d bytes, first 200: %s…]", name, len(content), content[:200]) +} + // removeCancelledToolResults filters out tool results that were cancelled due // to interrupt, keeping completed work. Also removes the corresponding tool // calls from assistant messages to maintain a valid message sequence. diff --git a/pkg/agent/state.go b/pkg/agent/state.go index b87c67d..33148d5 100644 --- a/pkg/agent/state.go +++ b/pkg/agent/state.go @@ -2,12 +2,22 @@ package agent import "ollie/pkg/backend" +// ResultTier classifies how long a tool result stays in the hot message list. +type ResultTier int + +const ( + TierHot ResultTier = iota // stays verbatim in messages (default) + TierWarm // summarized on next compaction pass + TierCold // immediately summarized at update time +) + // toolResult holds the output of a single tool call. type toolResult struct { ToolCallID string // may be empty (Ollama does not always set this) Name string Content string IsError bool + Tier ResultTier // default TierHot; tools may self-classify } // state is the interface both ephemeral and bead-backed state must satisfy. diff --git a/pkg/tools/execute/tier.go b/pkg/tools/execute/tier.go new file mode 100644 index 0000000..f85e308 --- /dev/null +++ b/pkg/tools/execute/tier.go @@ -0,0 +1,40 @@ +package execute + +import "strings" + +// ResultTier implements tools.TierClassifier. It reads the tool script's +// "ollie:tier" annotation to determine result retention. Returns "hot" if +// no annotation is found. +func (e *Server) ResultTier(name string) string { + code, err := ReadTool(name) + if err != nil { + return "hot" + } + return detectTier(code) +} + +// detectTier scans the first 10 lines of a tool script for an +// "ollie:tier" annotation. Matches "ollie:tier hot", "ollie:tier warm", +// or "ollie:tier cold" anywhere in the line. +func detectTier(code string) string { + lines := strings.SplitN(code, "\n", 11) + if len(lines) > 10 { + lines = lines[:10] + } + for _, line := range lines { + idx := strings.Index(line, "ollie:tier") + if idx < 0 { + continue + } + rest := strings.TrimSpace(line[idx+len("ollie:tier"):]) + switch { + case rest == "cold" || strings.HasPrefix(rest, "cold "): + return "cold" + case rest == "warm" || strings.HasPrefix(rest, "warm "): + return "warm" + case rest == "hot" || strings.HasPrefix(rest, "hot "): + return "hot" + } + } + return "hot" +} diff --git a/pkg/tools/tools.go b/pkg/tools/tools.go index 8df4fcf..aaf308d 100644 --- a/pkg/tools/tools.go +++ b/pkg/tools/tools.go @@ -126,3 +126,10 @@ type ToolRestrictionSetter interface { type ParallelClassifier interface { IsParallelRead(name string) bool } + +// TierClassifier is implemented by tool servers that can report the retention +// tier of a tool's results: "hot" (verbatim), "warm" (summarized on compaction), +// or "cold" (immediately summarized). Returns "hot" for unknown tools. +type TierClassifier interface { + ResultTier(name string) string +}