From d251dd7507eb50d7d0b700245875b085b8c0c691 Mon Sep 17 00:00:00 2001 From: vikingowl Date: Sun, 5 Apr 2026 21:59:55 +0200 Subject: [PATCH] feat: wire persist.Store into engine, elf manager, and agent tools --- cmd/gnoma/main.go | 18 ++++++++++-- internal/context/persist.go | 53 ------------------------------------ internal/elf/manager.go | 6 ++++ internal/engine/engine.go | 2 ++ internal/engine/loop.go | 11 +++++--- internal/tool/agent/agent.go | 12 ++++++-- internal/tool/agent/batch.go | 12 ++++++-- 7 files changed, 50 insertions(+), 64 deletions(-) delete mode 100644 internal/context/persist.go diff --git a/cmd/gnoma/main.go b/cmd/gnoma/main.go index 2fa2442..f2ff2c4 100644 --- a/cmd/gnoma/main.go +++ b/cmd/gnoma/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log/slog" + mrand "math/rand" "os" "os/signal" "strings" @@ -13,6 +14,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/engine" "encoding/json" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" gnomacfg "somegit.dev/Owlibou/gnoma/internal/config" gnomactx "somegit.dev/Owlibou/gnoma/internal/context" "somegit.dev/Owlibou/gnoma/internal/message" @@ -273,6 +275,14 @@ func main() { } permChecker := permission.NewChecker(permission.Mode(*permMode), permRules, pipePromptFn) + // Generate session-scoped ID for /tmp artifact directory + sessionID := fmt.Sprintf("%s-%06x", + time.Now().Format("20060102-150405"), + mrand.Int63()&0xffffff, + ) + store := persist.New(sessionID) + logger.Debug("session store initialized", "dir", store.Dir()) + // Create elf manager and register agent tools. // Must be created after fw and permChecker so elfs inherit security layers. elfMgr := elf.NewManager(elf.ManagerConfig{ @@ -280,13 +290,14 @@ func main() { Tools: reg, Permissions: permChecker, Firewall: fw, + Store: store, Logger: logger, }) elfProgressCh := make(chan elf.Progress, 16) - agentTool := agent.New(elfMgr) + agentTool := agent.New(elfMgr, store) agentTool.SetProgressCh(elfProgressCh) reg.Register(agentTool) - batchTool := agent.NewBatch(elfMgr) + batchTool := agent.NewBatch(elfMgr, store) batchTool.SetProgressCh(elfProgressCh) reg.Register(batchTool) @@ -337,7 +348,8 @@ func main() { System: systemPrompt, Model: *model, MaxTurns: *maxTurns, - Logger: logger, + Store: store, + Logger: logger, }) if err != nil { fmt.Fprintf(os.Stderr, "error: %v\n", err) diff --git a/internal/context/persist.go b/internal/context/persist.go deleted file mode 100644 index 772c7b6..0000000 --- a/internal/context/persist.go +++ /dev/null @@ -1,53 +0,0 @@ -package context - -import ( - "fmt" - "os" - "path/filepath" - "strings" -) - -const ( - // DefaultMaxResultSize is the threshold for persisting tool results. - DefaultMaxResultSize = 50_000 // chars - // PreviewSize is the number of chars to show inline. - PreviewSize = 2000 - // ToolResultsDir is the subdirectory for persisted results. - ToolResultsDir = "tool-results" -) - -// PersistLargeResult checks if a tool result exceeds the size limit. -// If so, writes it to disk and returns a preview + file path. -// Otherwise returns the original content unchanged. -func PersistLargeResult(content, toolUseID, sessionDir string) (string, bool) { - if len(content) <= DefaultMaxResultSize { - return content, false - } - - // Create directory - dir := filepath.Join(sessionDir, ToolResultsDir) - os.MkdirAll(dir, 0o755) - - // Write full result to disk - filename := toolUseID + ".txt" - path := filepath.Join(dir, filename) - os.WriteFile(path, []byte(content), 0o644) - - // Build preview - preview := content - if len(preview) > PreviewSize { - preview = preview[:PreviewSize] - } - - return fmt.Sprintf("\nFull output saved to: %s\n\nPreview (first %d chars):\n%s\n", - path, PreviewSize, preview), true -} - -// TruncateToolResult truncates a tool result to a maximum size with an indicator. -func TruncateToolResult(content string, maxSize int) string { - if len(content) <= maxSize { - return content - } - lines := strings.Split(content[:maxSize], "\n") - return strings.Join(lines, "\n") + fmt.Sprintf("\n\n... (truncated, %d total chars)", len(content)) -} diff --git a/internal/elf/manager.go b/internal/elf/manager.go index d8160e9..e5a47ce 100644 --- a/internal/elf/manager.go +++ b/internal/elf/manager.go @@ -12,6 +12,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/router" "somegit.dev/Owlibou/gnoma/internal/security" "somegit.dev/Owlibou/gnoma/internal/tool" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" ) // elfMeta tracks routing metadata and pool reservations for quality feedback. @@ -30,6 +31,7 @@ type Manager struct { tools *tool.Registry permissions *permission.Checker firewall *security.Firewall + store *persist.Store logger *slog.Logger } @@ -38,6 +40,7 @@ type ManagerConfig struct { Tools *tool.Registry Permissions *permission.Checker // nil = allow all (unsafe; prefer passing parent checker) Firewall *security.Firewall // nil = no scanning + Store *persist.Store // nil = no result persistence for elfs Logger *slog.Logger } @@ -53,6 +56,7 @@ func NewManager(cfg ManagerConfig) *Manager { tools: cfg.Tools, permissions: cfg.Permissions, firewall: cfg.Firewall, + store: cfg.Store, logger: logger, } } @@ -96,6 +100,7 @@ func (m *Manager) Spawn(ctx context.Context, taskType router.TaskType, prompt, s System: systemPrompt, Model: arm.ModelName, MaxTurns: maxTurns, + Store: m.store, Logger: m.logger, }) if err != nil { @@ -152,6 +157,7 @@ func (m *Manager) SpawnWithProvider(prov provider.Provider, model, prompt, syste System: systemPrompt, Model: model, MaxTurns: maxTurns, + Store: m.store, Logger: m.logger, }) if err != nil { diff --git a/internal/engine/engine.go b/internal/engine/engine.go index d4d9b0f..18cb80b 100644 --- a/internal/engine/engine.go +++ b/internal/engine/engine.go @@ -12,6 +12,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/router" "somegit.dev/Owlibou/gnoma/internal/security" "somegit.dev/Owlibou/gnoma/internal/tool" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" ) // Config holds engine configuration. @@ -25,6 +26,7 @@ type Config struct { System string // system prompt Model string // override model (empty = provider default) MaxTurns int // safety limit on tool loops (0 = unlimited) + Store *persist.Store // nil = no result persistence Logger *slog.Logger } diff --git a/internal/engine/loop.go b/internal/engine/loop.go index 7e7bd3a..cb89e76 100644 --- a/internal/engine/loop.go +++ b/internal/engine/loop.go @@ -14,6 +14,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/router" "somegit.dev/Owlibou/gnoma/internal/stream" "somegit.dev/Owlibou/gnoma/internal/tool" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" ) // Submit sends a user message and runs the agentic loop to completion. @@ -394,10 +395,12 @@ func (e *Engine) executeSingleTool(ctx context.Context, call message.ToolCall, t output = e.cfg.Firewall.ScanToolResult(output) } - // Persist large results to disk - if persisted, ok := gnomactx.PersistLargeResult(output, call.ID, ".gnoma/sessions"); ok { - e.logger.Debug("tool result persisted to disk", "name", call.Name, "size", len(output)) - output = persisted + // Persist results to /tmp for cross-tool session sharing + if e.cfg.Store != nil { + if path, ok := e.cfg.Store.Save(call.Name, call.ID, output); ok { + e.logger.Debug("tool result persisted", "name", call.Name, "path", path) + output = persist.InlineReplacement(path, output) + } } // Emit tool result event for the UI diff --git a/internal/tool/agent/agent.go b/internal/tool/agent/agent.go index 0f4d33a..cc60f5b 100644 --- a/internal/tool/agent/agent.go +++ b/internal/tool/agent/agent.go @@ -11,6 +11,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/router" "somegit.dev/Owlibou/gnoma/internal/stream" "somegit.dev/Owlibou/gnoma/internal/tool" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" ) var paramSchema = json.RawMessage(`{ @@ -37,10 +38,11 @@ var paramSchema = json.RawMessage(`{ type Tool struct { manager *elf.Manager ProgressCh chan<- elf.Progress // optional: sends structured progress to TUI + store *persist.Store } -func New(mgr *elf.Manager) *Tool { - return &Tool{manager: mgr} +func New(mgr *elf.Manager, store *persist.Store) *Tool { + return &Tool{manager: mgr, store: store} } // SetProgressCh sets the channel for forwarding elf progress to the TUI. @@ -80,6 +82,12 @@ func (t *Tool) Execute(ctx context.Context, args json.RawMessage) (tool.Result, systemPrompt := "You are an elf — a focused sub-agent of gnoma. Complete the given task thoroughly and concisely. Use tools as needed." + var preSave []persist.ResultFile + if t.store != nil { + preSave, _ = t.store.List("") + } + _ = preSave // used in Task 4 for ResultFilePaths diff + e, err := t.manager.Spawn(ctx, taskType, a.Prompt, systemPrompt, maxTurns) if err != nil { return tool.Result{Output: fmt.Sprintf("Failed to spawn elf: %v", err)}, nil diff --git a/internal/tool/agent/batch.go b/internal/tool/agent/batch.go index 83954d0..3917441 100644 --- a/internal/tool/agent/batch.go +++ b/internal/tool/agent/batch.go @@ -11,6 +11,7 @@ import ( "somegit.dev/Owlibou/gnoma/internal/elf" "somegit.dev/Owlibou/gnoma/internal/stream" "somegit.dev/Owlibou/gnoma/internal/tool" + "somegit.dev/Owlibou/gnoma/internal/tool/persist" ) var batchSchema = json.RawMessage(`{ @@ -49,10 +50,11 @@ var batchSchema = json.RawMessage(`{ type BatchTool struct { manager *elf.Manager progressCh chan<- elf.Progress + store *persist.Store } -func NewBatch(mgr *elf.Manager) *BatchTool { - return &BatchTool{manager: mgr} +func NewBatch(mgr *elf.Manager, store *persist.Store) *BatchTool { + return &BatchTool{manager: mgr, store: store} } func (t *BatchTool) SetProgressCh(ch chan<- elf.Progress) { @@ -91,6 +93,12 @@ func (t *BatchTool) Execute(ctx context.Context, args json.RawMessage) (tool.Res systemPrompt := "You are an elf — a focused sub-agent of gnoma. Complete the given task thoroughly and concisely. Use tools as needed." + var preSave []persist.ResultFile + if t.store != nil { + preSave, _ = t.store.List("") + } + _ = preSave // used in Task 4 + // Spawn all elfs with slight stagger to avoid rate limit bursts type elfEntry struct { elf elf.Elf