mirror of
https://github.com/docker/docker-agent.git
synced 2026-11-04 10:02:15 +00:00
128 lines
3.8 KiB
Go
128 lines
3.8 KiB
Go
package session
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
)
|
|
|
|
// ScratchpadKey separates conversations, callers and configured think instances.
|
|
type ScratchpadKey struct {
|
|
SessionID string
|
|
AgentName string
|
|
ToolsetKey string
|
|
}
|
|
|
|
// ThinkScratchpadStore atomically appends a thought and returns an independent
|
|
// ordered snapshot. rootID owns cleanup, not the scratchpad's sharing identity.
|
|
type ThinkScratchpadStore interface {
|
|
AppendThought(ctx context.Context, rootID string, key ScratchpadKey, thought string) ([]string, error)
|
|
}
|
|
|
|
var ErrScratchpadOwner = errors.New("think scratchpad root missing or ownership inconsistent")
|
|
|
|
func validateScratchpadKey(rootID string, key ScratchpadKey) error {
|
|
if rootID == "" || key.SessionID == "" {
|
|
return ErrEmptyID
|
|
}
|
|
if key.AgentName == "" || key.ToolsetKey == "" {
|
|
return ErrScratchpadOwner
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *InMemorySessionStore) AppendThought(ctx context.Context, rootID string, key ScratchpadKey, thought string) ([]string, error) {
|
|
if err := validateScratchpadKey(rootID, key); err != nil {
|
|
return nil, err
|
|
}
|
|
s.scratchpadMu.Lock()
|
|
defer s.scratchpadMu.Unlock()
|
|
if err := ctx.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
root, exists := s.sessions.Load(rootID)
|
|
if !exists {
|
|
return nil, ErrScratchpadOwner
|
|
}
|
|
root.mu.RLock()
|
|
isRoot := root.ParentID == ""
|
|
root.mu.RUnlock()
|
|
if !isRoot {
|
|
return nil, ErrScratchpadOwner
|
|
}
|
|
if owner, exists := s.scratchpadOwners[key.SessionID]; exists && owner != rootID {
|
|
return nil, ErrScratchpadOwner
|
|
}
|
|
if s.scratchpadOwners == nil {
|
|
s.scratchpadOwners = make(map[string]string)
|
|
}
|
|
if s.scratchpads == nil {
|
|
s.scratchpads = make(map[string]map[ScratchpadKey][]string)
|
|
}
|
|
if s.scratchpads[rootID] == nil {
|
|
s.scratchpads[rootID] = make(map[ScratchpadKey][]string)
|
|
}
|
|
thoughts := append(s.scratchpads[rootID][key], thought)
|
|
s.scratchpads[rootID][key] = thoughts
|
|
s.scratchpadOwners[key.SessionID] = rootID
|
|
return slices.Clone(thoughts), nil
|
|
}
|
|
|
|
func (s *SQLiteSessionStore) AppendThought(ctx context.Context, rootID string, key ScratchpadKey, thought string) ([]string, error) {
|
|
if err := validateScratchpadKey(rootID, key); err != nil {
|
|
return nil, err
|
|
}
|
|
tx, err := s.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("begin think append: %w", err)
|
|
}
|
|
defer func() { _ = tx.Rollback() }()
|
|
// Acquire the write lock before reading: independently loaded sessions must
|
|
// not upgrade stale read transactions or allocate the same sequence.
|
|
result, err := tx.ExecContext(ctx, `
|
|
INSERT INTO think_scratchpad (root_id, session_id, agent_name, toolset_key, seq, thought)
|
|
SELECT ?, ?, ?, ?, COALESCE((
|
|
SELECT MAX(seq) FROM think_scratchpad WHERE session_id = ? AND agent_name = ? AND toolset_key = ?
|
|
), 0) + 1, ?
|
|
WHERE EXISTS (SELECT 1 FROM sessions WHERE id = ? AND (parent_id IS NULL OR parent_id = ''))
|
|
AND NOT EXISTS (
|
|
SELECT 1 FROM think_scratchpad WHERE session_id = ? AND root_id != ?
|
|
)`, rootID, key.SessionID, key.AgentName, key.ToolsetKey,
|
|
key.SessionID, key.AgentName, key.ToolsetKey, thought, rootID,
|
|
key.SessionID, rootID)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("append think thought: %w", err)
|
|
}
|
|
n, err := result.RowsAffected()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if n != 1 {
|
|
return nil, ErrScratchpadOwner
|
|
}
|
|
rows, err := tx.QueryContext(ctx, `SELECT thought FROM think_scratchpad
|
|
WHERE session_id = ? AND agent_name = ? AND toolset_key = ? ORDER BY seq`,
|
|
key.SessionID, key.AgentName, key.ToolsetKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var thoughts []string
|
|
for rows.Next() {
|
|
var thought string
|
|
if err := rows.Scan(&thought); err != nil {
|
|
return nil, err
|
|
}
|
|
thoughts = append(thoughts, thought)
|
|
}
|
|
err = errors.Join(rows.Err(), rows.Close())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := tx.Commit(); err != nil {
|
|
return nil, fmt.Errorf("commit think thought: %w", err)
|
|
}
|
|
return thoughts, nil
|
|
}
|