Documentation
¶
Index ¶
- Constants
- Variables
- func BuildAgentMessagesFromEntries(entries []ConversationEntry) []sdk.Message
- func CreateTitleFromMessage(content string) string
- func FormatChannelSessionID(channel, senderID string) string
- func FormatModelPricingLabel(pricingService PricingService, model string) string
- func LastAssistantText(entries []ConversationEntry) string
- func NewToolCallEntries(toolCall sdk.ChatCompletionMessageToolCall, ...) (ConversationEntry, ConversationEntry)
- func ParseChannelSessionID(sessionID string) (channel, recipientID string, ok bool)
- func ToolOutcomes(entries []ConversationEntry) (succeeded, failed int)
- func ToolResultOutput(result *agentdomain.ToolExecutionResult, ...) string
- type ConversationEntry
- type ConversationMetadata
- type ConversationOptimizer
- type ConversationRepository
- type ConversationSummary
- type ExportFormat
- type MessageQueue
- type ModelCostStats
- type ModelService
- type PersistentConversationRepository
- type PlanApprovalStatus
- type PricingService
- type QueuedMessage
- type QueuedMessageSource
- type SessionCostStats
- type SessionID
- type SessionRollover
- type SessionTokenStats
- type SyntheticToolResponse
- type TokenEstimator
- type ToolApprovalStatus
Constants ¶
const CancelledToolResponseContent = "[Tool execution cancelled by user before completion]"
CancelledToolResponseContent is the sentinel body used for synthetic tool responses inserted by EnsureToolCallsClosed. The wording is surfaced to the model in the next turn and to the user via the conversation view, so it must be self-explanatory and stable.
Variables ¶
var ErrConversationNotFound = errors.New("conversation not found")
ErrConversationNotFound is returned by conversation storage when a requested conversation ID does not exist. Callers use errors.Is to tell an expected miss (e.g. the first use of a caller-chosen --session-id, before anything is saved) apart from a real load failure such as a corrupt file or an unreachable store.
Functions ¶
func BuildAgentMessagesFromEntries ¶ added in v0.207.0
func BuildAgentMessagesFromEntries(entries []ConversationEntry) []sdk.Message
BuildAgentMessagesFromEntries converts conversation entries into the flat slice of SDK messages sent to the model, dropping the three UI-only classes: plan entries, user-typed `!command` entries, and pending-approval placeholders. Plan and bash entries carry no reasoning_content, which thinking-mode providers reject with HTTP 400; a placeholder between an assistant tool_calls message and its tool response breaks provider adjacency.
func CreateTitleFromMessage ¶
CreateTitleFromMessage creates a short title from message content (fallback title)
func FormatChannelSessionID ¶
FormatChannelSessionID builds the session ID the daemon's channel manager uses for a channel/sender pair, the inverse of ParseChannelSessionID. ponytail: channel names must not contain '-' (they're a fixed enum today; sender IDs may contain dashes and are parsed back as the tail).
func FormatModelPricingLabel ¶
func FormatModelPricingLabel(pricingService PricingService, model string) string
FormatModelPricingLabel builds a human-readable pricing/availability label for a model, combining the per-token price with a subscription marker. A subscription model has no per-token price ($0/$0 → "free"), which would be misleading, so the bare "free" token is replaced by "subscription". A model that is both priced and subscription-gated keeps its price and gains the marker. Returns "" when there is nothing to show (pricing disabled, no entry, and not subscription-gated).
func LastAssistantText ¶ added in v0.207.0
func LastAssistantText(entries []ConversationEntry) string
LastAssistantText returns the trimmed content of the last non-empty assistant message in entries, or "" if there is none.
func NewToolCallEntries ¶ added in v0.182.0
func NewToolCallEntries(toolCall sdk.ChatCompletionMessageToolCall, result *agentdomain.ToolExecutionResult, content string, now time.Time) (ConversationEntry, ConversationEntry)
NewToolCallEntries builds the assistant tool_call entry and its paired tool result entry for a single directly-executed tool call, as persisted by the TUI direct-exec path and the browser extension bridge. content is what the LLM sees as the tool result on later turns.
func ParseChannelSessionID ¶
ParseChannelSessionID extracts the channel name and recipient ID from a session ID created by the daemon's channel manager. The channel manager builds session IDs as "channel-<name>-<sender_id>" (see channel_manager.go).
Returns ok=false when the session ID does not match this format (e.g. for chat-mode or generic agent sessions). Channel names cannot contain a '-'; recipient IDs may.
func ToolOutcomes ¶ added in v0.220.0
func ToolOutcomes(entries []ConversationEntry) (succeeded, failed int)
ToolOutcomes counts the executed tool calls in entries by outcome. A rejected call counts as failed.
func ToolResultOutput ¶ added in v0.200.0
func ToolResultOutput(result *agentdomain.ToolExecutionResult, formatForLLM func(*agentdomain.ToolExecutionResult) string) string
ToolResultOutput extracts the human-facing output of a tool result: combined stdout/stderr for Bash, formatForLLM otherwise, or the marshaled data when formatForLLM is nil.
Types ¶
type ConversationEntry ¶
type ConversationEntry struct {
// Core message fields
Message sdk.Message `json:"message"`
Model string `json:"model,omitempty"`
Time time.Time `json:"time"`
Hidden bool `json:"hidden,omitempty"`
Images []agentdomain.ImageAttachment `json:"images,omitempty"`
ReasoningContent string `json:"reasoning_content,omitempty"`
// Tool-related fields
ToolExecution *agentdomain.ToolExecutionResult `json:"tool_execution,omitempty"`
PendingToolCall *sdk.ChatCompletionMessageToolCall `json:"pending_tool_call,omitempty"`
ToolApprovalStatus ToolApprovalStatus `json:"tool_approval_status,omitempty"`
// Plan mode fields
Rejected bool `json:"rejected,omitempty"`
IsPlan bool `json:"is_plan,omitempty"`
PlanApprovalStatus PlanApprovalStatus `json:"plan_approval_status,omitempty"`
}
ConversationEntry represents a message in the conversation with metadata
type ConversationMetadata ¶
type ConversationMetadata struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MessageCount int `json:"message_count"`
TokenStats SessionTokenStats `json:"token_stats"`
CostStats SessionCostStats `json:"cost_stats,omitempty"`
Model string `json:"model,omitempty"`
Tags []string `json:"tags,omitempty"`
TitleGenerated bool `json:"title_generated,omitempty"`
TitleInvalidated bool `json:"title_invalidated,omitempty"`
TitleGenerationTime *time.Time `json:"title_generation_time,omitempty"`
ContextID string `json:"context_id,omitempty"`
// ActiveDuration is how long the agent has worked in this session, summed
// over its runs. Time spent idle between runs does not count.
ActiveDuration time.Duration `json:"active_duration,omitempty"`
// ParentSessionID is the orchestrator session that spawned this one
// (e.g. a subagent's parent). Empty for top-level sessions.
ParentSessionID string `json:"parent_session_id"`
// InvokedBy indicates who started the session: "human" or "agent".
// Defaults to "human" for sessions with no explicit invoker.
InvokedBy string `json:"invoked_by"`
// Project is the absolute path of the working directory the session
// runs in. Storage backends group conversations by it so listing can
// be scoped to the current project.
Project string `json:"project"`
}
ConversationMetadata contains metadata about a conversation
type ConversationOptimizer ¶
type ConversationOptimizer interface {
OptimizeMessages(messages []sdk.Message, model string, force bool) []sdk.Message
}
ConversationOptimizer optimizes conversation history to reduce token usage
type ConversationRepository ¶
type ConversationRepository interface {
AddMessage(msg ConversationEntry) error
GetMessages() []ConversationEntry
Clear() error
ClearExceptFirstUserMessage() error
GetMessageCount() int
UpdateLastMessage(content string) error
UpdateLastMessageToolCalls(toolCalls *[]sdk.ChatCompletionMessageToolCall) error
DeleteMessagesAfterIndex(index int) error
AddTokenUsage(model string, inputTokens, outputTokens, totalTokens, cachedTokens, cacheWriteTokens int) error
AddCachedTokens(tokens int)
GetSessionTokens() SessionTokenStats
GetSessionCostStats() SessionCostStats
StartWorking()
StopWorking()
GetActiveDuration() time.Duration
FormatToolResultForLLM(result *agentdomain.ToolExecutionResult) string
FormatToolResultForUI(result *agentdomain.ToolExecutionResult, terminalWidth int) string
FormatToolResultExpanded(result *agentdomain.ToolExecutionResult, terminalWidth int) string
RemovePendingToolCallByID(toolCallID string)
StartNewConversation(title string) error
LoadConversation(ctx context.Context, conversationID string) error
GetCurrentConversationTitle() string
GetCurrentConversationID() string
Export(format ExportFormat) ([]byte, error)
}
ConversationRepository is the composed interface for all conversation storage and retrieval operations. New code should depend on the narrower sub-interfaces above.
type ConversationSummary ¶
type ConversationSummary struct {
ID string `json:"id"`
Title string `json:"title"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
MessageCount int `json:"message_count"`
TokenStats SessionTokenStats `json:"token_stats"`
CostStats SessionCostStats `json:"cost_stats,omitempty"`
Model string `json:"model,omitempty"`
Tags []string `json:"tags,omitempty"`
Summary string `json:"summary,omitempty"`
TitleGenerated bool `json:"title_generated,omitempty"`
TitleInvalidated bool `json:"title_invalidated,omitempty"`
TitleGenerationTime *time.Time `json:"title_generation_time,omitempty"`
// ParentSessionID is the orchestrator session that spawned this one.
ParentSessionID string `json:"parent_session_id"`
// InvokedBy indicates who started the session: "human" or "agent".
InvokedBy string `json:"invoked_by"`
// Project is the absolute path of the working directory the session runs
// in; empty for legacy records.
Project string `json:"project,omitempty"`
}
ConversationSummary contains summary information about a conversation
type ExportFormat ¶
type ExportFormat string
ExportFormat defines the format for exporting conversations
const ( ExportMarkdown ExportFormat = "markdown" ExportJSON ExportFormat = "json" ExportText ExportFormat = "text" )
type MessageQueue ¶
type MessageQueue interface {
// Enqueue adds a message to the queue, tagged with where it came from
Enqueue(message sdk.Message, source QueuedMessageSource, requestID string)
// Dequeue removes and returns the next message from the queue
// Returns nil if the queue is empty
Dequeue() *QueuedMessage
// Peek returns the next message without removing it
// Returns nil if the queue is empty
Peek() *QueuedMessage
// Size returns the number of messages in the queue
Size() int
// IsEmpty returns true if the queue has no messages
IsEmpty() bool
// Clear removes all messages from the queue
Clear()
// GetAll returns all messages in the queue without removing them
GetAll() []QueuedMessage
}
MessageQueue handles centralized message queuing for all components
type ModelCostStats ¶
type ModelCostStats struct {
Model string
InputTokens int
OutputTokens int
InputCost float64
OutputCost float64
TotalCost float64
RequestCount int
}
ModelCostStats tracks cost statistics for a specific model within a session. This allows detailed breakdown when multiple models are used in the same conversation.
type ModelService ¶
type ModelService interface {
ListModels(ctx context.Context) ([]string, error)
SelectModel(modelID string) error
GetCurrentModel() string
IsModelAvailable(modelID string) bool
ValidateModel(modelID string) error
}
ModelService handles model selection and information
type PersistentConversationRepository ¶ added in v0.207.0
type PersistentConversationRepository interface {
ListSavedConversations(ctx context.Context, limit, offset int) ([]ConversationSummary, error)
LoadConversation(ctx context.Context, conversationID string) error
GetCurrentConversationMetadata() ConversationMetadata
SaveConversation(ctx context.Context) error
StartNewConversation(title string) error
GetCurrentConversationID() string
SetConversationID(id string)
SetConversationTitle(title string)
SetAutoSave(enabled bool)
DeleteSavedConversation(ctx context.Context, conversationID string) error
}
PersistentConversationRepository is the storage-backed side of a conversation: list, load, save, delete and re-point at stored conversations. The in-memory repository does not implement it, so callers type-assert a ConversationRepository to find out whether persistence is available.
type PlanApprovalStatus ¶
type PlanApprovalStatus int
PlanApprovalStatus represents the approval status of a plan
const ( PlanApprovalPending PlanApprovalStatus = iota PlanApprovalAccepted PlanApprovalRejected )
type PricingService ¶
type PricingService interface {
// IsEnabled returns whether pricing is enabled in the configuration.
IsEnabled() bool
// GetInputPrice retrieves the input price per million tokens for a specific model.
// Returns 0.0 for unknown models (e.g., Ollama, custom models).
GetInputPrice(model string) float64
// GetOutputPrice retrieves the output price per million tokens for a specific model.
// Returns 0.0 for unknown models (e.g., Ollama, custom models).
GetOutputPrice(model string) float64
// CalculateCost computes the total cost for a given number of input and
// output tokens. cachedTokens and cacheWriteTokens are the cache-read and
// cache-creation subsets of inputTokens, billed at the gateway's
// cache-read/cache-write rates when known (full input rate otherwise).
CalculateCost(model string, inputTokens, outputTokens, cachedTokens, cacheWriteTokens int) (inputCost, outputCost, totalCost float64)
// RequiresPro reports whether the model is gated behind a paid Pro
// subscription (e.g. some Ollama Cloud models). Resolves custom prices
// first, then defaults. Returns false when pricing is disabled or the
// model has no entry.
RequiresPro(model string) bool
// FormatModelPricing returns a formatted string describing the model's pricing.
// Returns empty string if pricing is disabled or the model has no pricing entry.
// Returns "free" only when an explicit pricing entry sets both prices to 0.0.
// Returns "$X.XX/$Y.YY per MTok" for paid models.
FormatModelPricing(model string) string
}
PricingService provides pricing information and cost calculation for different models. Note: This interface returns float64 for pricing to avoid import cycles. The actual ModelPricing struct is defined in the config package.
type QueuedMessage ¶
type QueuedMessage struct {
Message sdk.Message
QueuedAt time.Time
RequestID string
Source QueuedMessageSource
}
QueuedMessage represents a message in the input queue
type QueuedMessageSource ¶ added in v0.213.0
type QueuedMessageSource string
QueuedMessageSource identifies where a queued message came from. It is set when the entry is enqueued so the UI can tell a typed message from a background job result without guessing from the message text.
const ( QueueSourceComposer QueuedMessageSource = "composer" QueueSourceStdin QueuedMessageSource = "stdin" QueueSourceA2A QueuedMessageSource = "a2a" QueueSourceShell QueuedMessageSource = "shell" QueueSourceSubagent QueuedMessageSource = "subagent" QueueSourceJob QueuedMessageSource = "job" )
type SessionCostStats ¶
type SessionCostStats struct {
TotalCost float64
TotalInputCost float64
TotalOutputCost float64
PerModelStats map[string]*ModelCostStats
Currency string
}
SessionCostStats aggregates cost information for an entire session. It provides both total costs and per-model breakdowns.
type SessionID ¶
type SessionID string
SessionID represents a unique identifier for a chat session. Format: {unix-timestamp}-{8-char-random-hex} Example: 1733678400-a3f2bc8d
func GenerateSessionID ¶
func GenerateSessionID() SessionID
GenerateSessionID creates a new unique session identifier. The ID combines a Unix timestamp (for temporal uniqueness) with random hex characters (for collision resistance).
func (SessionID) Age ¶
Age returns the duration since the session was created. Returns 0 if the session ID format is invalid.
type SessionRollover ¶ added in v0.207.0
type SessionRollover interface {
// ResolveSessionID maps a raw (possibly group) id to the group's current
// session id; returns (sessionID, groupKey, error).
ResolveSessionID(rawID string) (string, string, error)
ShouldRollover(model string) bool
// MaybeRollover rolls over when ShouldRollover fires and returns the new
// session id and true; failures are logged and reported as no rollover.
MaybeRollover(ctx context.Context, model, groupKey string) (string, bool)
PerformRollover(ctx context.Context, model, groupKey string) (string, error)
}
SessionRollover continues a conversation in a fresh session (summary preserved) once it crosses the compact threshold, keeping a stable group key across the chain.
type SessionTokenStats ¶
type SessionTokenStats struct {
TotalInputTokens int `json:"total_input_tokens"`
TotalOutputTokens int `json:"total_output_tokens"`
TotalTokens int `json:"total_tokens"`
RequestCount int `json:"request_count"`
LastInputTokens int `json:"last_input_tokens"`
TotalCachedTokens int `json:"total_cached_tokens"`
TotalCacheWriteTokens int `json:"total_cache_write_tokens"`
}
SessionTokenStats tracks accumulated token usage across a session
type SyntheticToolResponse ¶ added in v0.207.0
SyntheticToolResponse pairs a synthesized Tool-role message with the metadata of the assistant tool_call it closes. Callers persist Message to the conversation repository and use ToolCallID / ToolName when emitting UI events.
func EnsureToolCallsClosed ¶ added in v0.207.0
func EnsureToolCallsClosed(conv []sdk.Message) ([]sdk.Message, []SyntheticToolResponse)
EnsureToolCallsClosed enforces the OpenAI-style invariant that every assistant message with tool_calls is followed by a Tool-role message for each tool_call_id. For every unmatched tool_call_id it inserts a synthetic Tool-role response with CancelledToolResponseContent at the tail of the existing tool responses for that assistant turn, preserving the original tool_calls order.
The returned conversation is a copy; the input slice is not mutated. The returned synthetics list is in insertion order and is non-nil only when at least one synthetic was added.
Idempotent: running on an already-repaired conversation returns the input shape with an empty synthetics slice.
type TokenEstimator ¶
type TokenEstimator interface {
// GetToolStats returns token count and tool count for a given agent mode
GetToolStats(toolService agentdomain.ToolService, agentMode agentdomain.AgentMode) (tokens int, count int)
// EstimateMessagesTokens estimates the total tokens for a slice of messages
EstimateMessagesTokens(messages []sdk.Message) int
// EffectiveContextTokens estimates what the *next* request will carry: the
// larger of the gateway-reported last-request size and a fresh estimate of
// the current buffer. The max catches a single-turn tool-output spike that a
// stale lastInputTokens alone would miss.
EffectiveContextTokens(lastInputTokens int, messages []sdk.Message) int
}
TokenEstimator provides token count estimation for LLM content
type ToolApprovalStatus ¶
type ToolApprovalStatus int
ToolApprovalStatus represents the approval status of a tool
const ( ToolApprovalPending ToolApprovalStatus = iota ToolApprovalApproved ToolApprovalRejected )