diff --git a/agent/compactor.go b/agent/compactor.go index f843d612d..aee9ef62d 100644 --- a/agent/compactor.go +++ b/agent/compactor.go @@ -11,18 +11,25 @@ import ( "github.com/ollama/ollama/api" ) +// Compaction wire-format. These constants and helpers are the single canonical +// definition of how a compacted turn is represented in message history; both +// the in-memory compactor (this package) and the on-disk chat store +// (package store) build and detect summaries through them. +const ( + CompactionSummaryMessagePrefix = "Conversation summary:\n" + CompactionToolName = "summary" + CompactionToolCallID = "ollama_compaction" + CompactionContinueInstruction = "continue the task in progress. the history has been compacted, do not mention compaction to the user" +) + const ( defaultCompactionContextWindowTokens = 32768 defaultCompactionKeepUserTurns = 3 defaultCompactionThreshold = 0.8 compactOnlySummaryContextTokens = 16000 - compactionSummaryMessagePrefix = "Conversation summary:\n" - compactionToolName = "summary" - compactionToolCallID = "ollama_compaction" - maxCompactionSummaryBytes = 16 * 1024 - compactionSummaryTruncated = "\n\n[summary truncated]" - compactionContinueInstruction = "continue the task in progress. the history has been compacted, do not mention compaction to the user" + maxCompactionSummaryBytes = 16 * 1024 + compactionSummaryTruncated = "\n\n[summary truncated]" compactionSystemPrompt = "Summarize the archived part of an Ollama CLI agent conversation. Preserve user goals, decisions, files, commands, tool results, and unresolved tasks needed to continue. Omit private reasoning and return only the summary." ) @@ -32,14 +39,7 @@ type Compactor interface { } type CompactionStore interface { - ArchiveForCompaction(context.Context, string, int, string) error -} - -// CompactionStoreWithContinuation persists the model-facing continuation hint -// for automatic compactions while preserving the legacy store method for manual -// compactions and older store implementations. -type CompactionStoreWithContinuation interface { - ArchiveForCompactionWithContinuation(context.Context, string, int, string, bool) error + ArchiveForCompaction(context.Context, string, int, string, bool) error } type CompactionOptions struct { @@ -134,13 +134,7 @@ func (c *SimpleCompactor) MaybeCompact(ctx context.Context, req CompactionReques } if c.Store != nil && req.ChatID != "" { - var err error - if store, ok := c.Store.(CompactionStoreWithContinuation); ok { - err = store.ArchiveForCompactionWithContinuation(ctx, req.ChatID, keptUserTurns, summary, req.ContinueTask) - } else { - err = c.Store.ArchiveForCompaction(ctx, req.ChatID, keptUserTurns, summary) - } - if err != nil { + if err := c.Store.ArchiveForCompaction(ctx, req.ChatID, keptUserTurns, summary, req.ContinueTask); err != nil { result.Reason = err.Error() return result, err } @@ -148,7 +142,7 @@ func (c *SimpleCompactor) MaybeCompact(ctx context.Context, req CompactionReques compacted := make([]api.Message, 0, len(prefix)+len(suffix)+2) compacted = append(compacted, prefix...) - compacted = append(compacted, compactionSummaryMessagesForTask(summary, req.ContinueTask)...) + compacted = append(compacted, CompactionSummaryMessages(summary, req.ContinueTask)...) compacted = append(compacted, suffix...) result.Messages = compacted result.Compacted = true @@ -285,37 +279,35 @@ func isUnsupportedCompactionThinkError(err error) bool { return strings.Contains(text, "does not support") || strings.Contains(text, "not supported") || strings.Contains(text, "unsupported") } -func compactionSummaryMessage(summary string) string { - return compactionSummaryMessageForTask(summary, false) -} - +// compactionSummaryMessageForTask renders a compaction summary as the content +// string stored on the synthetic tool-result message. func compactionSummaryMessageForTask(summary string, continueTask bool) string { - content := compactionSummaryMessagePrefix + strings.TrimSpace(summary) + content := CompactionSummaryMessagePrefix + strings.TrimSpace(summary) if continueTask { - content = strings.TrimSpace(content) + "\n\n" + compactionContinueInstruction + content = strings.TrimSpace(content) + "\n\n" + CompactionContinueInstruction } return content } -func compactionSummaryMessages(summary string) []api.Message { - return compactionSummaryMessagesForTask(summary, false) -} - -func compactionSummaryMessagesForTask(summary string, continueTask bool) []api.Message { +// CompactionSummaryMessages renders a compaction summary as the assistant +// tool-call plus tool-result pair that represents a compacted turn in the +// message history. This is the canonical builder used by both the compactor +// and the chat store. +func CompactionSummaryMessages(summary string, continueTask bool) []api.Message { return []api.Message{ { Role: "assistant", ToolCalls: []api.ToolCall{{ - ID: compactionToolCallID, + ID: CompactionToolCallID, Function: api.ToolCallFunction{ - Name: compactionToolName, + Name: CompactionToolName, }, }}, }, { Role: "tool", - ToolName: compactionToolName, - ToolCallID: compactionToolCallID, + ToolName: CompactionToolName, + ToolCallID: CompactionToolCallID, Content: compactionSummaryMessageForTask(summary, continueTask), }, } @@ -533,12 +525,12 @@ func splitCompactionMessages(messages []api.Message, keepUserTurns int) (prefix for i := start; i < len(messages); i++ { msg := messages[i] if isCompactionSummary(msg) { - previousSummary = compactionSummaryText(msg.Content) + previousSummary = CompactionSummaryText(msg.Content) continue } if isCompactionToolCall(msg) { if i+1 < len(messages) && isCompactionSummary(messages[i+1]) { - previousSummary = compactionSummaryText(messages[i+1].Content) + previousSummary = CompactionSummaryText(messages[i+1].Content) i++ } continue @@ -572,12 +564,12 @@ func splitCompactionMessages(messages []api.Message, keepUserTurns int) (prefix } func isCompactionToolName(name string) bool { - return name == compactionToolName + return name == CompactionToolName } func isCompactionSummary(msg api.Message) bool { return (msg.Role == "user" || msg.Role == "system" || (msg.Role == "tool" && isCompactionToolName(msg.ToolName))) && - strings.HasPrefix(msg.Content, compactionSummaryMessagePrefix) + strings.HasPrefix(msg.Content, CompactionSummaryMessagePrefix) } func isCompactionToolCall(msg api.Message) bool { @@ -592,17 +584,14 @@ func isCompactionToolCall(msg api.Message) bool { return false } -func compactionSummaryText(content string) string { - return strings.TrimSpace(strings.TrimSuffix( - strings.TrimSpace(strings.TrimPrefix(content, compactionSummaryMessagePrefix)), - compactionContinueInstruction, - )) -} - -// CompactionSummaryText returns the user-visible summary text from a compaction -// tool result. +// CompactionSummaryText reverses CompactionSummaryMessages, returning the +// user-visible summary text with the prefix and any continuation instruction +// removed. func CompactionSummaryText(content string) string { - return compactionSummaryText(content) + return strings.TrimSpace(strings.TrimSuffix( + strings.TrimSpace(strings.TrimPrefix(content, CompactionSummaryMessagePrefix)), + CompactionContinueInstruction, + )) } func intOption(options map[string]any, key string) int { diff --git a/agent/compactor_test.go b/agent/compactor_test.go index 028bd7c06..fe3522a16 100644 --- a/agent/compactor_test.go +++ b/agent/compactor_test.go @@ -16,11 +16,7 @@ type compactionStore struct { continueTask bool } -func (s *compactionStore) ArchiveForCompaction(ctx context.Context, chatID string, keepUserTurns int, summary string) error { - return s.ArchiveForCompactionWithContinuation(ctx, chatID, keepUserTurns, summary, false) -} - -func (s *compactionStore) ArchiveForCompactionWithContinuation(_ context.Context, chatID string, keepUserTurns int, summary string, continueTask bool) error { +func (s *compactionStore) ArchiveForCompaction(_ context.Context, chatID string, keepUserTurns int, summary string, continueTask bool) error { s.chatID = chatID s.keepUserTurns = keepUserTurns s.summary = summary @@ -55,16 +51,16 @@ func assertCompactionSummaryPair(t *testing.T, messages []api.Message) { if len(messages) != 2 { t.Fatalf("compaction summary pair len = %d, want 2: %#v", len(messages), messages) } - if messages[0].Role != "assistant" || len(messages[0].ToolCalls) != 1 || messages[0].ToolCalls[0].Function.Name != compactionToolName { + if messages[0].Role != "assistant" || len(messages[0].ToolCalls) != 1 || messages[0].ToolCalls[0].Function.Name != CompactionToolName { t.Fatalf("compaction assistant message = %#v", messages[0]) } if messages[0].ToolCalls[0].Function.Arguments.Len() != 0 { t.Fatalf("compaction summary tool call should not have arguments: %#v", messages[0].ToolCalls[0].Function.Arguments.ToMap()) } - if messages[1].Role != "tool" || messages[1].ToolName != compactionToolName || messages[1].ToolCallID != messages[0].ToolCalls[0].ID { + if messages[1].Role != "tool" || messages[1].ToolName != CompactionToolName || messages[1].ToolCallID != messages[0].ToolCalls[0].ID { t.Fatalf("compaction tool result = %#v", messages[1]) } - if !strings.HasPrefix(messages[1].Content, compactionSummaryMessagePrefix) { + if !strings.HasPrefix(messages[1].Content, CompactionSummaryMessagePrefix) { t.Fatalf("compaction tool result missing summary prefix: %#v", messages[1]) } } @@ -169,7 +165,7 @@ func TestSimpleCompactorKeepsOnlySummaryForSmallContext(t *testing.T) { t.Fatalf("leading system message not kept: %#v", result.Messages) } assertCompactionSummaryPair(t, result.Messages[1:]) - if !strings.Contains(result.Messages[2].Content, compactionContinueInstruction) { + if !strings.Contains(result.Messages[2].Content, CompactionContinueInstruction) { t.Fatalf("tool result missing continue instruction: %q", result.Messages[2].Content) } } @@ -205,10 +201,10 @@ func TestSimpleCompactorAddsContinueTaskInstructionOnlyToToolResult(t *testing.T t.Fatalf("result summary = %q", result.Summary) } content := result.Messages[1].Content - if !strings.Contains(content, compactionContinueInstruction) { + if !strings.Contains(content, CompactionContinueInstruction) { t.Fatalf("tool result missing continue instruction: %q", content) } - if got := compactionSummaryText(content); got != "summary" { + if got := CompactionSummaryText(content); got != "summary" { t.Fatalf("visible summary text = %q", got) } if !store.continueTask || store.summary != "summary" { @@ -621,18 +617,18 @@ func TestCompactionPromptRetruncatesAlreadyTruncatedToolOutput(t *testing.T) { } func TestCompactionSummaryTextStripsPrefix(t *testing.T) { - content := compactionSummaryMessage("worked on branch changes") - if got := compactionSummaryText(content); got != "worked on branch changes" { + content := compactionSummaryMessageForTask("worked on branch changes", false) + if got := CompactionSummaryText(content); got != "worked on branch changes" { t.Fatalf("summary text = %q", got) } } func TestCompactionSummaryCanTellModelToContinueTask(t *testing.T) { content := compactionSummaryMessageForTask("worked on branch changes", true) - if !strings.Contains(content, compactionContinueInstruction) { + if !strings.Contains(content, CompactionContinueInstruction) { t.Fatalf("summary message missing continue instruction: %q", content) } - if got := compactionSummaryText(content); got != "worked on branch changes" { + if got := CompactionSummaryText(content); got != "worked on branch changes" { t.Fatalf("summary text = %q", got) } } @@ -760,7 +756,7 @@ func TestSimpleCompactorCarriesPreviousSummary(t *testing.T) { result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{ Model: "model", Messages: []api.Message{ - {Role: "system", Content: compactionSummaryMessagePrefix + "old summary"}, + {Role: "system", Content: CompactionSummaryMessagePrefix + "old summary"}, {Role: "user", Content: "old"}, {Role: "assistant", Content: "old answer"}, {Role: "user", Content: "recent"}, @@ -792,8 +788,8 @@ func TestSimpleCompactorCarriesPreviousToolSummaryAndPlacesNewSummaryBeforeKeptS messages := []api.Message{ {Role: "user", Content: "kept before old summary"}, - compactionSummaryMessages("old summary")[0], - compactionSummaryMessages("old summary")[1], + CompactionSummaryMessages("old summary", false)[0], + CompactionSummaryMessages("old summary", false)[1], {Role: "user", Content: "latest request"}, } result, err := compactor.MaybeCompact(context.Background(), CompactionRequest{ diff --git a/agent/session_test.go b/agent/session_test.go index 9c49251c7..36af3f7aa 100644 --- a/agent/session_test.go +++ b/agent/session_test.go @@ -179,7 +179,7 @@ func (c *recordingCompactor) MaybeCompact(_ context.Context, req CompactionReque c.requests = append(c.requests, req) result := CompactionResult{Messages: req.Messages, Due: true} if len(req.Messages) > 0 && req.Messages[len(req.Messages)-1].Role == "tool" { - result.Messages = compactionSummaryMessages("tool result summarized") + result.Messages = CompactionSummaryMessages("tool result summarized", false) result.Compacted = true result.Summary = "tool result summarized" } @@ -190,7 +190,7 @@ func (c *oversizedCompactor) MaybeCompact(_ context.Context, req CompactionReque c.requests = append(c.requests, req) summary := strings.Repeat("oversized summary ", 300) return CompactionResult{ - Messages: compactionSummaryMessagesForTask(summary, req.ContinueTask), + Messages: CompactionSummaryMessages(summary, req.ContinueTask), Compacted: true, Due: true, Summary: summary, @@ -1365,10 +1365,10 @@ func TestSessionCompactsThenReattachesFullyOmittedToolResult(t *testing.T) { if len(nextRequestMessages) != 4 { t.Fatalf("next model request messages = %#v, want summary pair plus tool call/result", nextRequestMessages) } - if nextRequestMessages[0].Role != "assistant" || len(nextRequestMessages[0].ToolCalls) != 1 || nextRequestMessages[0].ToolCalls[0].Function.Name != compactionToolName { + if nextRequestMessages[0].Role != "assistant" || len(nextRequestMessages[0].ToolCalls) != 1 || nextRequestMessages[0].ToolCalls[0].Function.Name != CompactionToolName { t.Fatalf("first message should be compaction summary tool call: %#v", nextRequestMessages[0]) } - if nextRequestMessages[1].Role != "tool" || nextRequestMessages[1].ToolName != compactionToolName || !strings.Contains(nextRequestMessages[1].Content, "older history summarized") { + if nextRequestMessages[1].Role != "tool" || nextRequestMessages[1].ToolName != CompactionToolName || !strings.Contains(nextRequestMessages[1].Content, "older history summarized") { t.Fatalf("second message should be compaction summary result: %#v", nextRequestMessages[1]) } if nextRequestMessages[2].Role != "assistant" || len(nextRequestMessages[2].ToolCalls) != 1 || nextRequestMessages[2].ToolCalls[0].ID != "call-1" { diff --git a/agent/trace.go b/agent/trace.go deleted file mode 100644 index 17243fc08..000000000 --- a/agent/trace.go +++ /dev/null @@ -1,211 +0,0 @@ -package agent - -import ( - "encoding/json" - "os" - "path/filepath" - "sort" - "strings" - "sync" - "time" - "unicode/utf8" -) - -const agentTraceEnv = "OLLAMA_AGENT_TRACE" - -type JSONLTraceSink struct { - mu sync.Mutex - file *os.File -} - -func NewJSONLTraceSinkFromEnv() (*JSONLTraceSink, error) { - path := strings.TrimSpace(os.Getenv(agentTraceEnv)) - if path == "" { - return nil, nil - } - return NewJSONLTraceSink(path) -} - -func NewJSONLTraceSink(path string) (*JSONLTraceSink, error) { - path = strings.TrimSpace(path) - if path == "" { - return nil, nil - } - if dir := filepath.Dir(path); dir != "." && dir != "" { - if err := os.MkdirAll(dir, 0o755); err != nil { - return nil, err - } - } - file, err := os.OpenFile(path, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644) - if err != nil { - return nil, err - } - return &JSONLTraceSink{file: file}, nil -} - -func (s *JSONLTraceSink) Close() error { - if s == nil || s.file == nil { - return nil - } - return s.file.Close() -} - -func (s *JSONLTraceSink) Emit(event Event) error { - if s == nil || s.file == nil { - return nil - } - record := traceRecordFromEvent(event) - data, err := json.Marshal(record) - if err != nil { - return err - } - s.mu.Lock() - defer s.mu.Unlock() - _, err = s.file.Write(append(data, '\n')) - return err -} - -type traceRecord struct { - Time time.Time `json:"time"` - Type EventType `json:"type"` - RunID string `json:"run_id,omitempty"` - ChatID string `json:"chat_id,omitempty"` - Model string `json:"model,omitempty"` - Status string `json:"status,omitempty"` - ToolName string `json:"tool_name,omitempty"` - ToolCallID string `json:"tool_call_id,omitempty"` - WorkingDir string `json:"working_dir,omitempty"` - Args map[string]string `json:"args,omitempty"` - PromptTokens int `json:"prompt_tokens,omitempty"` - ContextWindowTokens int `json:"context_window_tokens,omitempty"` - CompactionThresholdTokens int `json:"compaction_threshold_tokens,omitempty"` - MessageCount int `json:"message_count,omitempty"` - UserMessageCount int `json:"user_message_count,omitempty"` - AssistantMessageCount int `json:"assistant_message_count,omitempty"` - ToolMessageCount int `json:"tool_message_count,omitempty"` - SystemMessageCount int `json:"system_message_count,omitempty"` - ToolCallCount int `json:"tool_call_count,omitempty"` - ToolCount int `json:"tool_count,omitempty"` - ToolNames []string `json:"tool_names,omitempty"` - ToolRound int `json:"tool_round,omitempty"` - ToolRoundLimit int `json:"tool_round_limit,omitempty"` - ContentApproxTokens int `json:"content_approx_tokens,omitempty"` - ContentRunes int `json:"content_runes,omitempty"` - ThinkingApproxTokens int `json:"thinking_approx_tokens,omitempty"` - ThinkingRunes int `json:"thinking_runes,omitempty"` - Response *traceMetrics `json:"response,omitempty"` - Error string `json:"error,omitempty"` -} - -type traceMetrics struct { - TotalDurationMS float64 `json:"total_duration_ms,omitempty"` - LoadDurationMS float64 `json:"load_duration_ms,omitempty"` - PromptEvalCount int `json:"prompt_eval_count,omitempty"` - PromptEvalDurationMS float64 `json:"prompt_eval_duration_ms,omitempty"` - EvalCount int `json:"eval_count,omitempty"` - EvalDurationMS float64 `json:"eval_duration_ms,omitempty"` - PromptEvalRate float64 `json:"prompt_eval_rate,omitempty"` - EvalRate float64 `json:"eval_rate,omitempty"` -} - -func traceRecordFromEvent(event Event) traceRecord { - record := traceRecord{ - Time: time.Now(), - Type: event.Type, - RunID: event.RunID, - ChatID: event.ChatID, - Model: event.Model, - Status: event.Status, - ToolName: event.ToolName, - ToolCallID: event.ToolCallID, - WorkingDir: event.WorkingDir, - Args: traceArgs(event.Args), - PromptTokens: event.PromptTokens, - ContextWindowTokens: event.ContextWindowTokens, - CompactionThresholdTokens: event.CompactionThresholdTokens, - MessageCount: event.MessageCount, - UserMessageCount: event.UserMessageCount, - AssistantMessageCount: event.AssistantMessageCount, - ToolMessageCount: event.ToolMessageCount, - SystemMessageCount: event.SystemMessageCount, - ToolCallCount: event.ToolCallCount, - ToolCount: event.ToolCount, - ToolNames: event.ToolNames, - ToolRound: event.ToolRound, - ToolRoundLimit: event.ToolRoundLimit, - ContentRunes: utf8.RuneCountInString(event.Content), - ThinkingRunes: utf8.RuneCountInString(event.Thinking), - Error: event.Error, - } - if event.Content != "" { - record.ContentApproxTokens = estimateCompactionTokens(event.Content) - } - if event.Thinking != "" { - record.ThinkingApproxTokens = estimateCompactionTokens(event.Thinking) - } - if event.Response != nil { - metrics := event.Response.Metrics - record.Response = &traceMetrics{ - TotalDurationMS: durationMS(metrics.TotalDuration), - LoadDurationMS: durationMS(metrics.LoadDuration), - PromptEvalCount: metrics.PromptEvalCount, - PromptEvalDurationMS: durationMS(metrics.PromptEvalDuration), - EvalCount: metrics.EvalCount, - EvalDurationMS: durationMS(metrics.EvalDuration), - } - if metrics.PromptEvalCount > 0 && metrics.PromptEvalDuration > 0 { - record.Response.PromptEvalRate = float64(metrics.PromptEvalCount) / metrics.PromptEvalDuration.Seconds() - } - if metrics.EvalCount > 0 && metrics.EvalDuration > 0 { - record.Response.EvalRate = float64(metrics.EvalCount) / metrics.EvalDuration.Seconds() - } - } - return record -} - -func durationMS(d time.Duration) float64 { - if d <= 0 { - return 0 - } - return float64(d) / float64(time.Millisecond) -} - -func traceArgs(args map[string]any) map[string]string { - if len(args) == 0 { - return nil - } - keys := make([]string, 0, len(args)) - for key := range args { - keys = append(keys, key) - } - sort.Strings(keys) - out := make(map[string]string, len(keys)) - for _, key := range keys { - out[key] = traceString(args[key]) - } - return out -} - -func traceString(value any) string { - data, err := json.Marshal(value) - if err != nil { - data = []byte(strings.TrimSpace(strings.ReplaceAll(strings.ReplaceAll(strings.TrimSpace(toString(value)), "\n", " "), "\t", " "))) - } - text := strings.TrimSpace(string(data)) - runes := []rune(text) - if len(runes) > 240 { - return string(runes[:240]) + "..." - } - return text -} - -func toString(value any) string { - if value == nil { - return "" - } - if text, ok := value.(string); ok { - return text - } - data, _ := json.Marshal(value) - return string(data) -} diff --git a/app/store/agent.go b/app/store/agent.go index cdef3cd52..c796b506b 100644 --- a/app/store/agent.go +++ b/app/store/agent.go @@ -9,16 +9,10 @@ import ( "strings" "time" + "github.com/ollama/ollama/agent" "github.com/ollama/ollama/api" ) -const ( - compactionSummaryMessagePrefix = "Conversation summary:\n" - compactionToolName = "summary" - compactionToolCallID = "ollama_compaction" - compactionContinueInstruction = "continue the task in progress. the history has been compacted, do not mention compaction to the user" -) - type AgentChat struct { ID string Title string @@ -222,7 +216,7 @@ func (s *Store) AgentChat(ctx context.Context, id string) (*AgentChat, error) { return nil, err } if summary != "" && !messagesContainCompactionSummary(chat.Messages) { - chat.Messages = insertCompactionSummaryAfterLeadingSystemMessages(chat.Messages, compactionSummaryMessages(summary, false)) + chat.Messages = insertCompactionSummaryAfterLeadingSystemMessages(chat.Messages, agent.CompactionSummaryMessages(summary, false)) } else { chat.Messages = moveCompactionSummaryBeforeKeptMessages(chat.Messages) } @@ -353,7 +347,7 @@ func (s *Store) ListUserMessages(ctx context.Context, limit int) ([]string, erro LIMIT ? ) ORDER BY id ASC - `, compactionSummaryMessagePrefix+"%", limit) + `, agent.CompactionSummaryMessagePrefix+"%", limit) if err != nil { return nil, fmt.Errorf("list user messages: %w", err) } @@ -373,11 +367,7 @@ func (s *Store) ListUserMessages(ctx context.Context, limit int) ([]string, erro return messages, nil } -func (s *Store) ArchiveForCompaction(ctx context.Context, chatID string, keepUserTurns int, summary string) error { - return s.archiveForCompaction(ctx, chatID, keepUserTurns, summary, false) -} - -func (s *Store) ArchiveForCompactionWithContinuation(ctx context.Context, chatID string, keepUserTurns int, summary string, continueTask bool) error { +func (s *Store) ArchiveForCompaction(ctx context.Context, chatID string, keepUserTurns int, summary string, continueTask bool) error { return s.archiveForCompaction(ctx, chatID, keepUserTurns, summary, continueTask) } @@ -440,7 +430,7 @@ func (s *Store) archiveForCompaction(ctx context.Context, chatID string, keepUse ) ) ORDER BY id ASC - `, chatID, keepStartID, compactionToolName, compactionToolName) + `, chatID, keepStartID, agent.CompactionToolName, agent.CompactionToolName) if err != nil { return fmt.Errorf("list archived messages: %w", err) } @@ -485,11 +475,11 @@ func (s *Store) archiveForCompaction(ctx context.Context, chatID string, keepUse WHERE tool_calls.message_id = messages.id AND tool_calls.function_name = ? ) ) - `, chatID, keepStartID, compactionToolName, compactionToolName); err != nil { + `, chatID, keepStartID, agent.CompactionToolName, agent.CompactionToolName); err != nil { return fmt.Errorf("archive messages: %w", err) } - for _, msg := range compactionSummaryMessages(summary, continueTask) { + for _, msg := range agent.CompactionSummaryMessages(summary, continueTask) { messageID, err := insertAgentMessage(ctx, tx, chatID, msg, "") if err != nil { return err @@ -556,36 +546,12 @@ func currentAgentModelSelectExpr(chatAlias string) string { )`, chatAlias) } -func compactionSummaryMessages(summary string, continueTask bool) []api.Message { - content := compactionSummaryMessagePrefix + strings.TrimSpace(summary) - if continueTask { - content = strings.TrimSpace(content) + "\n\n" + compactionContinueInstruction - } - return []api.Message{ - { - Role: "assistant", - ToolCalls: []api.ToolCall{{ - ID: compactionToolCallID, - Function: api.ToolCallFunction{ - Name: compactionToolName, - }, - }}, - }, - { - Role: "tool", - ToolName: compactionToolName, - ToolCallID: compactionToolCallID, - Content: content, - }, - } -} - func messagesContainCompactionSummary(messages []api.Message) bool { for _, msg := range messages { - if msg.Role == "tool" && msg.ToolName == compactionToolName && strings.HasPrefix(msg.Content, compactionSummaryMessagePrefix) { + if msg.Role == "tool" && msg.ToolName == agent.CompactionToolName && strings.HasPrefix(msg.Content, agent.CompactionSummaryMessagePrefix) { return true } - if (msg.Role == "user" || msg.Role == "system") && strings.HasPrefix(msg.Content, compactionSummaryMessagePrefix) { + if (msg.Role == "user" || msg.Role == "system") && strings.HasPrefix(msg.Content, agent.CompactionSummaryMessagePrefix) { return true } } @@ -598,10 +564,10 @@ func moveCompactionSummaryBeforeKeptMessages(messages []api.Message) []api.Messa for i, msg := range messages { if msg.Role == "assistant" { for _, call := range msg.ToolCalls { - if call.Function.Name == compactionToolName { + if call.Function.Name == agent.CompactionToolName { start = i end = i + 1 - if end < len(messages) && messages[end].Role == "tool" && messages[end].ToolName == compactionToolName { + if end < len(messages) && messages[end].Role == "tool" && messages[end].ToolName == agent.CompactionToolName { end++ } break diff --git a/app/store/agent_test.go b/app/store/agent_test.go index 9b4862e59..fd61e7b81 100644 --- a/app/store/agent_test.go +++ b/app/store/agent_test.go @@ -11,6 +11,7 @@ import ( "strings" "testing" + "github.com/ollama/ollama/agent" "github.com/ollama/ollama/api" ) @@ -190,7 +191,7 @@ func TestAgentStoreListUserMessages(t *testing.T) { {Role: "assistant", Content: "not user"}, {Role: "user", Content: "middle prompt"}, {Role: "user", Content: " "}, - {Role: "user", Content: compactionSummaryMessagePrefix + "old context"}, + {Role: "user", Content: agent.CompactionSummaryMessagePrefix + "old context"}, {Role: "user", Content: "new prompt"}, } { if err := store.AppendAgentMessage(ctx, "chat-1", msg, ""); err != nil { @@ -222,7 +223,7 @@ func TestAgentStoreArchivesCompactedMessages(t *testing.T) { } } - if err := store.ArchiveForCompactionWithContinuation(ctx, "chat-1", 1, "summary", true); err != nil { + if err := store.ArchiveForCompaction(ctx, "chat-1", 1, "summary", true); err != nil { t.Fatal(err) } @@ -233,11 +234,11 @@ func TestAgentStoreArchivesCompactedMessages(t *testing.T) { if len(agentChat.Messages) != 3 { t.Fatalf("active messages = %#v, want compaction pair plus latest request", agentChat.Messages) } - if agentChat.Messages[0].Role != "assistant" || len(agentChat.Messages[0].ToolCalls) != 1 || agentChat.Messages[0].ToolCalls[0].Function.Name != compactionToolName { + if agentChat.Messages[0].Role != "assistant" || len(agentChat.Messages[0].ToolCalls) != 1 || agentChat.Messages[0].ToolCalls[0].Function.Name != agent.CompactionToolName { t.Fatalf("summary tool call = %#v", agentChat.Messages[0]) } content := agentChat.Messages[1].Content - if !strings.Contains(content, compactionContinueInstruction) { + if !strings.Contains(content, agent.CompactionContinueInstruction) { t.Fatalf("summary tool result missing continuation instruction: %q", content) } if agentChat.Messages[2].Content != "recent request" { diff --git a/cmd/agent_tui.go b/cmd/agent_tui.go index 0cc98937e..fd9885027 100644 --- a/cmd/agent_tui.go +++ b/cmd/agent_tui.go @@ -224,13 +224,6 @@ func GenerateAgentTUI(cmd *cobra.Command, opts AgentTUIOptions) error { defer setup.close() opts = setup.opts - traceSink, err := coreagent.NewJSONLTraceSinkFromEnv() - if err != nil { - return err - } - if traceSink != nil { - defer traceSink.Close() - } _, err = tui.RunAgentChat(cmd.Context(), tui.ChatOptions{ Model: opts.Model, @@ -252,7 +245,6 @@ func GenerateAgentTUI(cmd *cobra.Command, opts AgentTUIOptions) error { return agentSystemPrompt(model, setup.skills, registry != nil && registry.Has("skill"), "") }, Approval: setup.approval, - EventSink: traceSink, AutoApproveTools: opts.AutoApproveTools, Skills: setup.skills, SystemPrompt: agentSystemPrompt(opts.Model, setup.skills, setup.registry != nil && setup.registry.Has("skill"), ""), @@ -347,14 +339,6 @@ func GenerateAgentHeadless(cmd *cobra.Command, opts AgentTUIOptions) error { headlessSink := &agentHeadlessEventSink{hideThinking: opts.HideThinking} eventSink := coreagent.EventSink(headlessSink) - traceSink, err := coreagent.NewJSONLTraceSinkFromEnv() - if err != nil { - return err - } - if traceSink != nil { - defer traceSink.Close() - eventSink = coreagent.MultiEventSink{eventSink, traceSink} - } session := &coreagent.Session{ Client: setup.client, Store: setup.store,