feat: wire persist.Store into engine, elf manager, and agent tools
This commit is contained in:
@@ -6,6 +6,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
mrand "math/rand"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,6 +14,7 @@ import (
|
|||||||
|
|
||||||
"somegit.dev/Owlibou/gnoma/internal/engine"
|
"somegit.dev/Owlibou/gnoma/internal/engine"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
||||||
gnomacfg "somegit.dev/Owlibou/gnoma/internal/config"
|
gnomacfg "somegit.dev/Owlibou/gnoma/internal/config"
|
||||||
gnomactx "somegit.dev/Owlibou/gnoma/internal/context"
|
gnomactx "somegit.dev/Owlibou/gnoma/internal/context"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/message"
|
"somegit.dev/Owlibou/gnoma/internal/message"
|
||||||
@@ -273,6 +275,14 @@ func main() {
|
|||||||
}
|
}
|
||||||
permChecker := permission.NewChecker(permission.Mode(*permMode), permRules, pipePromptFn)
|
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.
|
// Create elf manager and register agent tools.
|
||||||
// Must be created after fw and permChecker so elfs inherit security layers.
|
// Must be created after fw and permChecker so elfs inherit security layers.
|
||||||
elfMgr := elf.NewManager(elf.ManagerConfig{
|
elfMgr := elf.NewManager(elf.ManagerConfig{
|
||||||
@@ -280,13 +290,14 @@ func main() {
|
|||||||
Tools: reg,
|
Tools: reg,
|
||||||
Permissions: permChecker,
|
Permissions: permChecker,
|
||||||
Firewall: fw,
|
Firewall: fw,
|
||||||
|
Store: store,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
})
|
})
|
||||||
elfProgressCh := make(chan elf.Progress, 16)
|
elfProgressCh := make(chan elf.Progress, 16)
|
||||||
agentTool := agent.New(elfMgr)
|
agentTool := agent.New(elfMgr, store)
|
||||||
agentTool.SetProgressCh(elfProgressCh)
|
agentTool.SetProgressCh(elfProgressCh)
|
||||||
reg.Register(agentTool)
|
reg.Register(agentTool)
|
||||||
batchTool := agent.NewBatch(elfMgr)
|
batchTool := agent.NewBatch(elfMgr, store)
|
||||||
batchTool.SetProgressCh(elfProgressCh)
|
batchTool.SetProgressCh(elfProgressCh)
|
||||||
reg.Register(batchTool)
|
reg.Register(batchTool)
|
||||||
|
|
||||||
@@ -337,6 +348,7 @@ func main() {
|
|||||||
System: systemPrompt,
|
System: systemPrompt,
|
||||||
Model: *model,
|
Model: *model,
|
||||||
MaxTurns: *maxTurns,
|
MaxTurns: *maxTurns,
|
||||||
|
Store: store,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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("<persisted-output>\nFull output saved to: %s\n\nPreview (first %d chars):\n%s\n</persisted-output>",
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"somegit.dev/Owlibou/gnoma/internal/router"
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/security"
|
"somegit.dev/Owlibou/gnoma/internal/security"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
// elfMeta tracks routing metadata and pool reservations for quality feedback.
|
// elfMeta tracks routing metadata and pool reservations for quality feedback.
|
||||||
@@ -30,6 +31,7 @@ type Manager struct {
|
|||||||
tools *tool.Registry
|
tools *tool.Registry
|
||||||
permissions *permission.Checker
|
permissions *permission.Checker
|
||||||
firewall *security.Firewall
|
firewall *security.Firewall
|
||||||
|
store *persist.Store
|
||||||
logger *slog.Logger
|
logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,6 +40,7 @@ type ManagerConfig struct {
|
|||||||
Tools *tool.Registry
|
Tools *tool.Registry
|
||||||
Permissions *permission.Checker // nil = allow all (unsafe; prefer passing parent checker)
|
Permissions *permission.Checker // nil = allow all (unsafe; prefer passing parent checker)
|
||||||
Firewall *security.Firewall // nil = no scanning
|
Firewall *security.Firewall // nil = no scanning
|
||||||
|
Store *persist.Store // nil = no result persistence for elfs
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +56,7 @@ func NewManager(cfg ManagerConfig) *Manager {
|
|||||||
tools: cfg.Tools,
|
tools: cfg.Tools,
|
||||||
permissions: cfg.Permissions,
|
permissions: cfg.Permissions,
|
||||||
firewall: cfg.Firewall,
|
firewall: cfg.Firewall,
|
||||||
|
store: cfg.Store,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -96,6 +100,7 @@ func (m *Manager) Spawn(ctx context.Context, taskType router.TaskType, prompt, s
|
|||||||
System: systemPrompt,
|
System: systemPrompt,
|
||||||
Model: arm.ModelName,
|
Model: arm.ModelName,
|
||||||
MaxTurns: maxTurns,
|
MaxTurns: maxTurns,
|
||||||
|
Store: m.store,
|
||||||
Logger: m.logger,
|
Logger: m.logger,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,6 +157,7 @@ func (m *Manager) SpawnWithProvider(prov provider.Provider, model, prompt, syste
|
|||||||
System: systemPrompt,
|
System: systemPrompt,
|
||||||
Model: model,
|
Model: model,
|
||||||
MaxTurns: maxTurns,
|
MaxTurns: maxTurns,
|
||||||
|
Store: m.store,
|
||||||
Logger: m.logger,
|
Logger: m.logger,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"somegit.dev/Owlibou/gnoma/internal/router"
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/security"
|
"somegit.dev/Owlibou/gnoma/internal/security"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Config holds engine configuration.
|
// Config holds engine configuration.
|
||||||
@@ -25,6 +26,7 @@ type Config struct {
|
|||||||
System string // system prompt
|
System string // system prompt
|
||||||
Model string // override model (empty = provider default)
|
Model string // override model (empty = provider default)
|
||||||
MaxTurns int // safety limit on tool loops (0 = unlimited)
|
MaxTurns int // safety limit on tool loops (0 = unlimited)
|
||||||
|
Store *persist.Store // nil = no result persistence
|
||||||
Logger *slog.Logger
|
Logger *slog.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import (
|
|||||||
"somegit.dev/Owlibou/gnoma/internal/router"
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
"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.
|
// 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)
|
output = e.cfg.Firewall.ScanToolResult(output)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist large results to disk
|
// Persist results to /tmp for cross-tool session sharing
|
||||||
if persisted, ok := gnomactx.PersistLargeResult(output, call.ID, ".gnoma/sessions"); ok {
|
if e.cfg.Store != nil {
|
||||||
e.logger.Debug("tool result persisted to disk", "name", call.Name, "size", len(output))
|
if path, ok := e.cfg.Store.Save(call.Name, call.ID, output); ok {
|
||||||
output = persisted
|
e.logger.Debug("tool result persisted", "name", call.Name, "path", path)
|
||||||
|
output = persist.InlineReplacement(path, output)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit tool result event for the UI
|
// Emit tool result event for the UI
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"somegit.dev/Owlibou/gnoma/internal/router"
|
"somegit.dev/Owlibou/gnoma/internal/router"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
var paramSchema = json.RawMessage(`{
|
var paramSchema = json.RawMessage(`{
|
||||||
@@ -37,10 +38,11 @@ var paramSchema = json.RawMessage(`{
|
|||||||
type Tool struct {
|
type Tool struct {
|
||||||
manager *elf.Manager
|
manager *elf.Manager
|
||||||
ProgressCh chan<- elf.Progress // optional: sends structured progress to TUI
|
ProgressCh chan<- elf.Progress // optional: sends structured progress to TUI
|
||||||
|
store *persist.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(mgr *elf.Manager) *Tool {
|
func New(mgr *elf.Manager, store *persist.Store) *Tool {
|
||||||
return &Tool{manager: mgr}
|
return &Tool{manager: mgr, store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetProgressCh sets the channel for forwarding elf progress to the TUI.
|
// 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."
|
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)
|
e, err := t.manager.Spawn(ctx, taskType, a.Prompt, systemPrompt, maxTurns)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return tool.Result{Output: fmt.Sprintf("Failed to spawn elf: %v", err)}, nil
|
return tool.Result{Output: fmt.Sprintf("Failed to spawn elf: %v", err)}, nil
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"somegit.dev/Owlibou/gnoma/internal/elf"
|
"somegit.dev/Owlibou/gnoma/internal/elf"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/stream"
|
"somegit.dev/Owlibou/gnoma/internal/stream"
|
||||||
"somegit.dev/Owlibou/gnoma/internal/tool"
|
"somegit.dev/Owlibou/gnoma/internal/tool"
|
||||||
|
"somegit.dev/Owlibou/gnoma/internal/tool/persist"
|
||||||
)
|
)
|
||||||
|
|
||||||
var batchSchema = json.RawMessage(`{
|
var batchSchema = json.RawMessage(`{
|
||||||
@@ -49,10 +50,11 @@ var batchSchema = json.RawMessage(`{
|
|||||||
type BatchTool struct {
|
type BatchTool struct {
|
||||||
manager *elf.Manager
|
manager *elf.Manager
|
||||||
progressCh chan<- elf.Progress
|
progressCh chan<- elf.Progress
|
||||||
|
store *persist.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewBatch(mgr *elf.Manager) *BatchTool {
|
func NewBatch(mgr *elf.Manager, store *persist.Store) *BatchTool {
|
||||||
return &BatchTool{manager: mgr}
|
return &BatchTool{manager: mgr, store: store}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *BatchTool) SetProgressCh(ch chan<- elf.Progress) {
|
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."
|
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
|
// Spawn all elfs with slight stagger to avoid rate limit bursts
|
||||||
type elfEntry struct {
|
type elfEntry struct {
|
||||||
elf elf.Elf
|
elf elf.Elf
|
||||||
|
|||||||
Reference in New Issue
Block a user