From ba740701e2a872712270de90fd668a183d040625 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sat, 8 Aug 2026 13:29:01 +0200 Subject: [PATCH] simplify prompt loading, some renames --- agent/agent.go | 26 +++++++++++++------------- agent/agent_config.go | 18 +++++++++--------- agent/commands.go | 2 +- agent/history.go | 2 -- agent/loop.go | 6 +++--- agent/new.go | 22 ++++++++++++---------- agent/prompt_resolver.go | 1 - agent/turn.go | 24 ++++++++++++------------ justfile | 2 -- prompts/tool_discovery.md | 18 ++++++++++++++++++ session/persist.go | 37 ++++++++++++++++++------------------- session/registry.go | 7 ++++--- session/session.go | 20 ++++++++++---------- session/setup.go | 5 ----- 14 files changed, 100 insertions(+), 90 deletions(-) create mode 100644 prompts/tool_discovery.md diff --git a/agent/agent.go b/agent/agent.go index ee65926..8074c92 100644 --- a/agent/agent.go +++ b/agent/agent.go @@ -399,31 +399,31 @@ func (ag *Agent) RenamePreamble(old, new string) { } // refreshToolListing replaces the "# Available Tools" section in the preamble -// with an updated listing. Called when tool_list detects directory changes. +// with an updated listing. Called when directory contents change. // Note: This only updates the tool listing, not the full tool documentation. // Tool documentation requires a full BuildPreamble call (on agent reload). func (ag *Agent) refreshToolListing(listing string) { if ag.runtime == nil { return } - + // Find the "# Available Tools" section const availableToolsMarker = "\n# Available Tools\n\n" availableToolsIdx := strings.Index(ag.runtime.Preamble, availableToolsMarker) - + if availableToolsIdx < 0 { // No tool listing exists yet if listing != "" { // Find where to insert - before "# Tool Documentation" or "## Tool discovery" const toolDocsMarker = "\n# Tool Documentation\n\n" const discoveryMarker = "\n## Tool discovery\n" - + toolDocsIdx := strings.Index(ag.runtime.Preamble, toolDocsMarker) discoveryIdx := strings.Index(ag.runtime.Preamble, discoveryMarker) - + var insertIdx int var insertBefore string - + if toolDocsIdx >= 0 { insertIdx = toolDocsIdx insertBefore = toolDocsMarker @@ -435,25 +435,25 @@ func (ag *Agent) refreshToolListing(listing string) { ag.runtime.Preamble += availableToolsMarker + listing return } - + // Insert before the marker ag.runtime.Preamble = ag.runtime.Preamble[:insertIdx] + availableToolsMarker + listing + insertBefore + ag.runtime.Preamble[insertIdx+len(insertBefore):] } return } - + // Tool listing exists - find where it ends // Look for next section marker after the listing afterMarker := ag.runtime.Preamble[availableToolsIdx+len(availableToolsMarker):] - + // Find next section marker const toolDocsMarker = "\n# Tool Documentation\n\n" const discoveryMarker = "\n## Tool discovery\n" - + var endIdx int toolDocsIdx := strings.Index(afterMarker, toolDocsMarker) discoveryIdx := strings.Index(afterMarker, discoveryMarker) - + if toolDocsIdx >= 0 { endIdx = toolDocsIdx } else if discoveryIdx >= 0 { @@ -462,11 +462,11 @@ func (ag *Agent) refreshToolListing(listing string) { // No markers found - listing goes to end endIdx = len(afterMarker) } - + // Rebuild preamble with new listing beforeListing := ag.runtime.Preamble[:availableToolsIdx+len(availableToolsMarker)] afterListing := afterMarker[endIdx:] - + if listing == "" { // Remove the entire "# Available Tools" section // Need to also remove the preceding "\n" if it exists diff --git a/agent/agent_config.go b/agent/agent_config.go index 0ff84a9..61068c4 100644 --- a/agent/agent_config.go +++ b/agent/agent_config.go @@ -41,15 +41,15 @@ func (p Prompt) MarshalJSON() ([]byte, error) { } type AgentConfig struct { - Prompt Prompt `json:"prompt,omitempty"` - Backend string `json:"backend,omitempty"` - Model string `json:"model,omitempty"` - Tools *bool `json:"tools,omitempty"` - AllowTools []string `json:"allowTools,omitempty"` - AutoLoad []string `json:"autoLoad,omitempty"` - DisallowTools []string `json:"disallowTools,omitempty"` - MaxTokens int `json:"maxTokens,omitempty"` - MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"` + Prompt Prompt `json:"prompt,omitempty"` + Backend string `json:"backend,omitempty"` + Model string `json:"model,omitempty"` + Tools *bool `json:"tools,omitempty"` + AllowTools []string `json:"allowTools,omitempty"` + AutoLoad []string `json:"autoLoad,omitempty"` + DisallowTools []string `json:"disallowTools,omitempty"` + MaxTokens int `json:"maxTokens,omitempty"` + MaxCompletionTokens int `json:"maxCompletionTokens,omitempty"` // MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited. MaxSteps int `json:"maxSteps,omitempty"` Temperature *float64 `json:"temperature,omitempty"` diff --git a/agent/commands.go b/agent/commands.go index 333acb9..83368aa 100644 --- a/agent/commands.go +++ b/agent/commands.go @@ -182,7 +182,7 @@ func (ag *Agent) cmdAgent(_ context.Context, args []string) { disp := ag.newToolServer() env := []string{"OLLIE_SESSION_ID=" + ag.sessionID, "OLLIE_UNAME=" + ag.id} env = append(env, ag.promptEnvExtra...) - rt := BuildRuntime(cfg, disp, ag.cwd, env, ag.baseLayers...) + rt := BuildRuntime(cfg, disp, ag.cwd, env, ag.systemPrompt, ag.envBlock) if rt.CfgBackend != "" { newBe, err := ag.newBackend(rt.CfgBackend) if err != nil { diff --git a/agent/history.go b/agent/history.go index 5a3a1bf..af424f3 100644 --- a/agent/history.go +++ b/agent/history.go @@ -287,8 +287,6 @@ func (s *History) addUsage(u backend.Usage, estimated bool) { } } - - func (s *History) resetTurnAccumulators() { s.turnInputTokens = 0 s.turnCachedTokens = 0 diff --git a/agent/loop.go b/agent/loop.go index 84fd6f1..df0154a 100644 --- a/agent/loop.go +++ b/agent/loop.go @@ -116,9 +116,9 @@ type agentConfig struct { Output EventHandler preamble string // compiled system+agent prompt sent as the system role GenerationParams backend.GenerationParams - PopInject func() string // returns and clears pending inject, or "" - AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place - Save func() // called after each state.update(); persists mid-turn progress + PopInject func() string // returns and clears pending inject, or "" + AutoCompact func(ctx context.Context) // called after each tool round; may compact in-place + Save func() // called after each state.update(); persists mid-turn progress // MaxSteps is the maximum number of tool-call rounds per turn. // When reached, a soft nudge is injected and the loop exits cleanly. // 0 means unlimited. diff --git a/agent/new.go b/agent/new.go index b82b8c2..34e96cd 100644 --- a/agent/new.go +++ b/agent/new.go @@ -10,20 +10,21 @@ import ( // AgentCfg is the configuration for constructing a new Agent. type AgentCfg struct { + ID string // unique agent identity (uname) + SessionID string // id of session the agent belongs to + Profile string // config profile name (e.g. "default" → agents/default.json) History *History Runtime *Runtime - Profile string // config profile name (e.g. "default" → agents/default.json) AgentsDir string - AgentID string // unique agent identity (uname) - CWD string // working directory for tool execution - BaseLayers []string + Cwd string // working directory for tool execution + SystemPrompt string + EnvBlock string PromptEnvExtra []string NewToolServer func() toolsrv.Runner NewBackend func(string) (backend.Backend, error) Output EventHandler Log *olog.Logger AuditLog *olog.Logger - SessionID string StartupMsgs []string ReadPlanStep func() string Save func() @@ -34,7 +35,7 @@ type AgentCfg struct { func NewAgent(cfg AgentCfg) *Agent { auditLog := cfg.AuditLog if auditLog == nil && cfg.Log != nil { - cfg.Log.Warn("AuditLog not provided for agent %s, deriving from Log", cfg.AgentID) + cfg.Log.Warn("AuditLog not provided for agent %s, deriving from Log", cfg.ID) auditLog = cfg.Log.Sub("audit") } @@ -42,11 +43,12 @@ func NewAgent(cfg AgentCfg) *Agent { history: cfg.History, runtime: cfg.Runtime, profile: cfg.Profile, - name: cfg.AgentID, // display name defaults to uname + name: cfg.ID, // display name defaults to uname agentsDir: cfg.AgentsDir, - id: cfg.AgentID, - cwd: cfg.CWD, - baseLayers: cfg.BaseLayers, + id: cfg.ID, + cwd: cfg.Cwd, + systemPrompt: cfg.SystemPrompt, + envBlock: cfg.EnvBlock, promptEnvExtra: cfg.PromptEnvExtra, newToolServer: cfg.NewToolServer, newBackend: cfg.NewBackend, diff --git a/agent/prompt_resolver.go b/agent/prompt_resolver.go index 9128e9d..0214a40 100644 --- a/agent/prompt_resolver.go +++ b/agent/prompt_resolver.go @@ -10,7 +10,6 @@ import ( "runtime" "strings" "time" - ) // PromptEnv returns the standard PRIME_* environment variables for prompt diff --git a/agent/turn.go b/agent/turn.go index 95b24db..952c464 100644 --- a/agent/turn.go +++ b/agent/turn.go @@ -295,7 +295,7 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string { } if errors.Is(err, context.Canceled) || errors.Is(err, ErrInterrupted) { ag.auditLog.Debug("turn: interrupted session=%s", ag.sessionID) - ag.save() + ag.save() return "" } ag.emit(Event{Role: "error", Content: err.Error()}) @@ -316,11 +316,11 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string { auditTruncate(ag.Reply()), ag.history.LastTurnCostUSD, ag.history.SessionCostUSD, ag.sessionID) ag.notifyChange() } - ag.save() + ag.save() - // Inject that was pending but never consumed (text-only response with no + // Inject that was pending but never consumed (text-only response with no // tool calls) — treat it as the next user message. - if p := ag.pendingInject.Swap(nil); p != nil { + if p := ag.pendingInject.Swap(nil); p != nil { return *p } @@ -378,17 +378,17 @@ func (ag *Agent) runCompact(ctx context.Context, trigger string) (int, error) { if err != nil { return 0, err } -if n > 0 { - ag.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, ag.sessionID) - ag.warnedContext = false - if sc := ag.spawnContext(ctx); sc != "" { - ag.history.appendUserMessage(sc) - } + if n > 0 { + ag.auditLog.Debug("compact: removed %d messages trigger=%s session=%s", n, trigger, ag.sessionID) + ag.warnedContext = false + if sc := ag.spawnContext(ctx); sc != "" { + ag.history.appendUserMessage(sc) } - return n, nil } + return n, nil +} - // ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C). +// ErrInterrupted is returned when the user cancels an agent turn (Ctrl-C). var ErrInterrupted = errors.New("interrupted") // defaultContextLength is used when the backend cannot report the model's diff --git a/justfile b/justfile index 6bece7a..7514f22 100644 --- a/justfile +++ b/justfile @@ -73,8 +73,6 @@ kde-kf5: @echo '=== Building ollie-kde (KF5) ===' just -f kde/justfile kde-kf5 -# === Install targets === - install: test install-data install-el just -f kde/justfile install-kde @echo === Install complete === diff --git a/prompts/tool_discovery.md b/prompts/tool_discovery.md new file mode 100644 index 0000000..869f72f --- /dev/null +++ b/prompts/tool_discovery.md @@ -0,0 +1,18 @@ +## Tool discovery + +Use the 9P filesystem to discover and load tools dynamically: +- **List available tools** — `ollie-9p read /tools` +- **Load a tool** — `echo "toolname" | ollie-9p write session/{sname}/agent/{aname}/tools` +- **See loaded tools** — `ollie-9p read session/{sname}/agent/{aname}/tools` + +For manual 9P operations: +```bash +# List all available tools +ollie-9p read /tools + +# Load a tool +echo "toolname" | ollie-9p write session/{sname}/agent/{aname}/tools + +# See which tools are loaded +ollie-9p read session/{sname}/agent/{aname}/tools +``` \ No newline at end of file diff --git a/session/persist.go b/session/persist.go index 43c1bcc..e5d44bb 100644 --- a/session/persist.go +++ b/session/persist.go @@ -18,24 +18,24 @@ import ( // PersistedSession is the on-disk format for a saved session with multiple agents. type PersistedSession struct { - ID string `json:"id"` - Name string `json:"name"` - CWD string `json:"cwd"` - Remote string `json:"remote,omitempty"` - Paused bool `json:"paused,omitempty"` - Agents []PersistedAgent `json:"agents"` + ID string `json:"id"` + Name string `json:"name"` + CWD string `json:"cwd"` + Remote string `json:"remote,omitempty"` + Paused bool `json:"paused,omitempty"` + Agents []PersistedAgent `json:"agents"` } // PersistedAgent is per-agent state within a session. type PersistedAgent struct { - ID string `json:"id"` // uname - Name string `json:"name"` // display name - Profile string `json:"profile"` - Backend string `json:"backend"` - Model string `json:"model"` - CWD string `json:"cwd"` - Messages []backend.Message `json:"messages"` - TaskState *agent.TaskState `json:"taskState,omitempty"` + ID string `json:"id"` // uname + Name string `json:"name"` // display name + Profile string `json:"profile"` + Backend string `json:"backend"` + Model string `json:"model"` + CWD string `json:"cwd"` + Messages []backend.Message `json:"messages"` + TaskState *agent.TaskState `json:"taskState,omitempty"` // Usage tracking TotalInputTokens int `json:"totalInputTokens,omitempty"` TotalCachedInputTokens int `json:"totalCachedInputTokens,omitempty"` @@ -295,9 +295,10 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) { Runtime: rt, Profile: pa.Profile, AgentsDir: pkgAgentsDir, - AgentID: pa.ID, - CWD: cwd, - BaseLayers: layers.BaseLayers(), + ID: pa.ID, + Cwd: cwd, + SystemPrompt: layers.SystemPrompt, + EnvBlock: layers.EnvBlock, PromptEnvExtra: promptEnv, NewToolServer: newToolServer, NewBackend: backend.NewWithName, @@ -340,5 +341,3 @@ func restoreMultiAgentSession(ps *PersistedSession) (*RestoredSession, error) { ToolCount: toolCount, }, nil } - - diff --git a/session/registry.go b/session/registry.go index 034cbdd..45be186 100644 --- a/session/registry.go +++ b/session/registry.go @@ -419,9 +419,10 @@ func CreateAgent(sessName string, args []string) (*agent.Agent, error) { Runtime: rt, Profile: profile, AgentsDir: pkgAgentsDir, - AgentID: uname, - CWD: cwd, - BaseLayers: layers.BaseLayers(), + ID: uname, + Cwd: cwd, + SystemPrompt: layers.SystemPrompt, + EnvBlock: layers.EnvBlock, PromptEnvExtra: infra.PromptEnv, NewToolServer: infra.NewToolServer, NewBackend: backend.NewWithName, diff --git a/session/session.go b/session/session.go index 9be3d47..3d76321 100644 --- a/session/session.go +++ b/session/session.go @@ -39,7 +39,8 @@ type Config struct { ListHandlers map[string]func() []string PromptEnvExtra []string Remote string - BaseLayers []string + SystemPrompt string + EnvBlock string Output agent.EventHandler } @@ -171,9 +172,10 @@ func New(cfg Config) *Session { Runtime: rt, Profile: cfg.Profile, AgentsDir: cfg.AgentsDir, - AgentID: cfg.AgentID, - CWD: paths.ExpandHome(cfg.CWD), - BaseLayers: cfg.BaseLayers, + ID: cfg.AgentID, + Cwd: paths.ExpandHome(cfg.CWD), + SystemPrompt: cfg.SystemPrompt, + EnvBlock: cfg.EnvBlock, PromptEnvExtra: cfg.PromptEnvExtra, NewToolServer: cfg.NewToolServer, NewBackend: cfg.NewBackend, @@ -310,8 +312,6 @@ func (a *Session) SetSessionID(newID string) error { return nil } - - func (a *Session) activeSessionPath(id, suffix string) string { return filepath.Join(a.sessionsDir, "active", id+suffix) } @@ -532,7 +532,7 @@ func (a *Session) Resume() error { if cwd == "" { cwd, _ = os.Getwd() } - + // First agent spawns the tool server, others reuse var reuseFrom *InfraConfig if a.proc != nil { @@ -542,7 +542,7 @@ func (a *Session) Resume() error { ToolsConn: a.toolsConn, } } - + infra, err := SetupToolServer(ToolServerConfig{ Ctx: ctx, CWD: cwd, @@ -553,14 +553,14 @@ func (a *Session) Resume() error { if err != nil { return fmt.Errorf("resume setup for agent %s failed: %w", ag.ID(), err) } - + // First agent sets session infra if i == 0 { a.proc = infra.Proc a.keeper = infra.Keeper a.toolsConn = infra.ToolsConn } - + ag.SetToolServer(infra.NewToolServer, infra.ToolsConn) } } diff --git a/session/setup.go b/session/setup.go index 9afad35..866ea42 100644 --- a/session/setup.go +++ b/session/setup.go @@ -175,11 +175,6 @@ func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname string, prompt } } -// BaseLayers returns the prompt layers as a slice for agent configuration. -func (p PromptLayers) BaseLayers() []string { - return []string{p.SystemPrompt, p.EnvBlock} -} - // LoadAutoLoadTools loads all tools from the agent config's autoLoad list. // It first sends the session ID and uname to the tool server so that // ollie-remote can attach its tool registry before processing tool_load RPCs.