simplify prompt loading, some renames

This commit is contained in:
Ollie Agent 2026-08-08 13:29:01 +02:00
parent 605212e1fd
commit ba740701e2
14 changed files with 100 additions and 90 deletions

View File

@ -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

View File

@ -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"`

View File

@ -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 {

View File

@ -287,8 +287,6 @@ func (s *History) addUsage(u backend.Usage, estimated bool) {
}
}
func (s *History) resetTurnAccumulators() {
s.turnInputTokens = 0
s.turnCachedTokens = 0

View File

@ -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.

View File

@ -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,

View File

@ -10,7 +10,6 @@ import (
"runtime"
"strings"
"time"
)
// PromptEnv returns the standard PRIME_* environment variables for prompt

View File

@ -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

View File

@ -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 ===

18
prompts/tool_discovery.md Normal file
View File

@ -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
```

View File

@ -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
}

View File

@ -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,

View File

@ -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)
}
}

View File

@ -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.