mirror of
https://github.com/ollama/ollama.git
synced 2026-10-09 01:16:44 +00:00
agent: consolidate compaction helpers and drop trace sink
This commit is contained in:
parent
e098a6afaa
commit
8f882b054a
7 changed files with 74 additions and 349 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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{
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
211
agent/trace.go
211
agent/trace.go
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue