prompt audit: trim system prompt, kill dead code, improve maintainability

- System prompt: ~200 → ~80 lines (removed tool-first table, API docs,
  output protocol, dead 9P entries)
- Removed AllowTools entirely (field, RPC, configs)
- Removed PRIME_* env vars, replaced with typed struct fields
- Merged tool listing + docs into single renderTools()
- Killed BuildToolListing, changed OnToolsChanged to func() signal
- Typed Preamble.Section (compile-time safety)
- Added protocol docs on extractToolResult
- Added session/ package godoc
- Moved output protocol to per-agent prompts
- Rewrote data/agents/README.md
- Moved 7 reference docs from doc/resources/ to doc/
- Deleted PromptEnv() dead function
This commit is contained in:
Ollie Agent 2026-08-09 14:13:52 +02:00
parent 1dcde13f26
commit 2d74ffb95e
34 changed files with 529 additions and 478 deletions

View File

@ -140,7 +140,7 @@ graph TB
`o` is the CLI layer that all text-based frontends build on — it handles path
resolution, context, and ctl dispatch. The KDE GUI and Kate plugin use `ollie-9p`
(the native 9P client) directly for performance. For a deep dive into the
filesystem, see [`doc/resources/9p.md`](doc/resources/9p.md).
filesystem, see [`doc/9p.md`](doc/9p.md).
## Credits

View File

@ -27,7 +27,6 @@ type Agent struct {
agentsDir string
systemPrompt string // system prompt for /agent reloads
envBlock string // environment block for /agent reloads
promptEnvExtra []string // PRIME_* vars for prompt resolution
newToolServer func() *toolsrv.Conn
newBackend func(string) (backend.Backend, error)
currentAction atomic.Pointer[actionHandle]
@ -308,7 +307,6 @@ func (ag *Agent) SwitchProfile(name string) error {
}
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.systemPrompt, ag.envBlock)
if cfg.Backend != "" {
newBe, err := ag.newBackend(cfg.Backend)
@ -411,15 +409,17 @@ func (ag *Agent) SetCWD(dir string) {
// refreshToolListing replaces the tools section in the preamble with an
// updated listing. Called by the tool server when tools are added/removed.
func (ag *Agent) refreshToolListing(listing string) {
func (ag *Agent) refreshToolListing() {
if ag.runtime == nil || ag.runtime.Preamble == nil {
return
}
if listing == "" {
ag.runtime.Preamble.Set(SectionTools, "")
} else {
ag.runtime.Preamble.Set(SectionTools, "# Available Tools\n\n"+listing)
if ag.runtime.ToolServer != nil {
if infos, err := ag.runtime.ToolServer.ListTools(); err == nil {
ag.runtime.Preamble.Set(SectionTools, renderTools(infos))
return
}
}
ag.runtime.Preamble.Set(SectionTools, "")
}
// wireToolsChanged sets the OnToolsChanged callback on the tool server
@ -620,7 +620,6 @@ type AgentCfg struct {
Cwd string // working directory for tool execution
SystemPrompt string
EnvBlock string
PromptEnvExtra []string
NewToolServer func() *toolsrv.Conn
NewBackend func(string) (backend.Backend, error)
Output EventHandler
@ -642,7 +641,6 @@ func NewAgent(cfg AgentCfg) *Agent {
cwd: cfg.Cwd,
systemPrompt: cfg.SystemPrompt,
envBlock: cfg.EnvBlock,
promptEnvExtra: cfg.PromptEnvExtra,
newToolServer: cfg.NewToolServer,
newBackend: cfg.NewBackend,
output: cfg.Output,

View File

@ -34,7 +34,7 @@ type AgentConfig struct {
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"`
// MaxSteps caps the number of tool-call rounds per turn. 0 means unlimited.
MaxSteps int `json:"maxSteps,omitempty"`

View File

@ -1,33 +1,84 @@
package agent
import (
"encoding/json"
"io"
"strings"
"testing"
"ollie/toolsrv"
)
// mockToolServer creates a Conn backed by a goroutine that responds to
// list_tools with the given ToolInfo slice. Simulates the ollie-remote RPC.
func mockToolServer(t *testing.T, infos []toolsrv.ToolInfo) *toolsrv.Conn {
t.Helper()
cr, sw := io.Pipe() // client reads, server writes
sr, cw := io.Pipe() // server reads, client writes
go func() {
dec := json.NewDecoder(sr)
enc := json.NewEncoder(sw)
for {
var req struct {
JSONRPC string `json:"jsonrpc"`
ID int64 `json:"id"`
Method string `json:"method"`
Params json.RawMessage `json:"params,omitempty"`
}
if err := dec.Decode(&req); err != nil {
return
}
switch req.Method {
case "list_tools":
result, _ := json.Marshal(infos)
enc.Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": json.RawMessage(result)})
default:
enc.Encode(map[string]any{"jsonrpc": "2.0", "id": req.ID, "result": true})
}
}
}()
rwc := struct {
io.Reader
io.Writer
io.Closer
}{cr, cw, cw}
return toolsrv.NewConn(rwc, nil)
}
func TestRefreshToolListing(t *testing.T) {
infos := []toolsrv.ToolInfo{
{
Name: "new_tool",
Description: "New description",
Prompt: "## new_tool\n\nNew description with details.",
},
}
conn := mockToolServer(t, infos)
defer conn.Close()
p := &Preamble{}
p.Set(SectionSystem, "# System")
p.Set(SectionEnv, "# Env")
p.Set(SectionAgent, "# Agent")
p.Set(SectionTools, "# Available Tools\n\n- **old_tool** — Old description\n")
p.Set(SectionToolDocs, "# Tool Documentation\n\n**old_tool**\n\nDocs...\n")
p.Set(SectionTools, "# Tools\n\n## old_tool\n\nOld description\n\n")
ag := &Agent{runtime: &Runtime{Preamble: p}}
ag := &Agent{runtime: &Runtime{Preamble: p, ToolServer: conn}}
ag.refreshToolListing("- **new_tool** — New description\n")
ag.refreshToolListing()
preamble := ag.runtime.PreambleString()
if !strings.Contains(preamble, "**new_tool** — New description") {
t.Error("should contain new tool listing")
if !strings.Contains(preamble, "## new_tool") {
t.Error("should contain new tool")
}
if strings.Contains(preamble, "**old_tool** — Old description") {
t.Error("should not contain old tool listing")
if !strings.Contains(preamble, "New description with details.") {
t.Error("should contain tool prompt/docs")
}
// Tool docs section is untouched by refreshToolListing
if !strings.Contains(preamble, "# Tool Documentation") {
t.Error("tool docs should be preserved")
if strings.Contains(preamble, "old_tool") {
t.Error("should not contain old tool")
}
}
@ -36,22 +87,18 @@ func TestRefreshToolListingEmpty(t *testing.T) {
p.Set(SectionSystem, "# System")
p.Set(SectionEnv, "# Env")
p.Set(SectionAgent, "# Agent")
p.Set(SectionTools, "# Available Tools\n\n- **tool** — Desc\n")
p.Set(SectionToolDocs, "# Tool Documentation\n\nDocs\n")
p.Set(SectionTools, "# Tools\n\n## tool\n\nDesc\n\n")
ag := &Agent{runtime: &Runtime{Preamble: p}}
ag.refreshToolListing("")
ag.refreshToolListing()
preamble := ag.runtime.PreambleString()
if strings.Contains(preamble, "# Available Tools") {
if strings.Contains(preamble, "# Tools") {
t.Error("empty listing should remove tools section")
}
if !strings.Contains(preamble, "# System") {
t.Error("system section should be preserved")
}
if !strings.Contains(preamble, "# Tool Documentation") {
t.Error("tool docs should be preserved")
}
}

View File

@ -3,31 +3,9 @@ package agent
import (
"os"
"os/exec"
"runtime"
"strings"
"time"
"ollie/paths"
)
// PromptEnv returns the standard PRIME_* environment variables for prompt
// template resolution. For local sessions, values are detected from the
// filesystem. For remote sessions, callers should override these with
// values from remote.HostInfo.
func PromptEnv(cwd string) []string {
platform := runtime.GOOS
isGitRepo := "false"
if cwd != "" && paths.IsGitRepo(cwd) {
isGitRepo = "true"
}
return []string{
"PRIME_CWD=" + cwd,
"PRIME_PLATFORM=" + platform,
"PRIME_DATE=" + time.Now().Format("2006-01-02"),
"PRIME_IS_GIT_REPO=" + isGitRepo,
}
}
// resolvePrompt reads each file path in p.Value, expands env vars in both
// the path and the file content, and joins the results with newlines.
func resolvePrompt(p Prompt, cwd string, env []string) (string, error) {

View File

@ -23,12 +23,12 @@ type Preamble struct {
}
type section struct {
name string
name Section
text string
}
// Set replaces (or appends) the named section.
func (p *Preamble) Set(name, text string) {
func (p *Preamble) Set(name Section, text string) {
for i := range p.sections {
if p.sections[i].name == name {
p.sections[i].text = text
@ -39,7 +39,7 @@ func (p *Preamble) Set(name, text string) {
}
// Get returns the text of a named section.
func (p *Preamble) Get(name string) string {
func (p *Preamble) Get(name Section) string {
for _, s := range p.sections {
if s.name == name {
return s.text
@ -63,13 +63,14 @@ func (p *Preamble) String() string {
return sb.String()
}
// Section names (constants to avoid typos).
// Section is a named preamble section identifier.
type Section string
const (
SectionSystem = "system"
SectionEnv = "env"
SectionAgent = "agent"
SectionTools = "tools"
SectionToolDocs = "tool_docs"
SectionSystem Section = "system"
SectionEnv Section = "env"
SectionAgent Section = "agent"
SectionTools Section = "tools"
)
// --- Environment block ---
@ -87,29 +88,32 @@ func EnvironmentBlock(cwd, platform string, isGitRepo bool, sandbox string) stri
// --- Tool rendering ---
func renderToolListing(infos []toolsrv.ToolInfo) string {
s := toolsrv.BuildToolListing(infos)
if s == "" {
return ""
}
return "# Available Tools\n\n" + s
}
func renderToolDocs(infos []toolsrv.ToolInfo) string {
// renderTools produces a single combined section: a one-line summary per tool
// followed immediately by its documentation (if any). This keeps the listing
// and usage instructions adjacent so models don't lose context between them.
func renderTools(infos []toolsrv.ToolInfo) string {
var sb strings.Builder
for _, ti := range infos {
if ti.Prompt != "" && ti.Server == "" {
sb.WriteString(ti.Prompt)
sb.WriteString("\n\n")
if len(ti.InputSchema) > 0 && string(ti.InputSchema) != "{}" {
fmt.Fprintf(&sb, "**Schema**:\n```json\n%s\n```\n\n", ti.InputSchema)
}
if ti.Server != "" {
continue
}
if ti.Description == "" {
continue
}
sb.WriteString("## ")
sb.WriteString(ti.Name)
sb.WriteString("\n\n")
if ti.Prompt != "" {
sb.WriteString(ti.Prompt)
} else {
sb.WriteString(ti.Description)
}
sb.WriteString("\n\n")
}
if sb.Len() == 0 {
return ""
}
return "# Tool Documentation\n\n" + sb.String()
return "# Tools\n\n" + sb.String()
}
// --- Runtime ---
@ -169,9 +173,6 @@ func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string,
}
genParams = cfg.GenerationParams
maxSteps = cfg.MaxSteps
if len(cfg.AllowTools) > 0 && srv != nil {
srv.SetAllowTools(cfg.AllowTools)
}
}
exec := func(ctx context.Context, name string, args json.RawMessage) (string, []backend.ContentBlock, error) {
@ -204,8 +205,7 @@ func BuildRuntime(cfg *AgentConfig, srv *toolsrv.Conn, cwd string, env []string,
preamble.Set(SectionSystem, systemPrompt)
preamble.Set(SectionEnv, envBlock)
preamble.Set(SectionAgent, agentPrompt)
preamble.Set(SectionTools, renderToolListing(allToolInfos))
preamble.Set(SectionToolDocs, renderToolDocs(allToolInfos))
preamble.Set(SectionTools, renderTools(allToolInfos))
return &Runtime{
ToolServer: srv,
@ -241,6 +241,22 @@ func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
return out
}
// extractToolResult parses the tool server's JSON-RPC response into text
// and optional content blocks.
//
// Protocol: tool scripts write JSON to stdout matching this schema:
//
// {
// "isError": false,
// "content": [
// {"type": "text", "text": "..."},
// {"type": "image", "media_type": "image/png", "data": "<base64>"}
// ]
// }
//
// If the response is not valid JSON (e.g. a plain string from a simple script),
// it is returned as-is with isError=false. This fallback allows tools to emit
// raw text without wrapping it in the content-block envelope.
func extractToolResult(raw json.RawMessage) (text string, contentBlocks []backend.ContentBlock, isError bool) {
var result struct {
IsError bool `json:"isError"`

View File

@ -36,8 +36,7 @@ func TestPreambleSections(t *testing.T) {
p.Set(SectionSystem, "# Core System Prompt\nTest")
p.Set(SectionEnv, "# Environment\n- Test env")
p.Set(SectionAgent, "# Agent Prompt\nTest instructions")
p.Set(SectionTools, renderToolListing(toolInfos))
p.Set(SectionToolDocs, renderToolDocs(toolInfos))
p.Set(SectionTools, renderTools(toolInfos))
preamble := p.String()
@ -46,12 +45,11 @@ func TestPreambleSections(t *testing.T) {
"# Core System Prompt\nTest",
"# Environment\n- Test env",
"# Agent Prompt\nTest instructions",
"# Available Tools",
"- **file_read** — Read a file with line numbers",
"- **shell** — Execute shell command",
"# Tool Documentation",
"# Tools",
"## file_read",
"**file_read**\n\nRead a file with line numbers...",
"**Schema**:\n```json\n{\"type\":\"object\"}\n```",
"## shell",
"Execute shell command",
} {
if !strings.Contains(preamble, want) {
t.Errorf("preamble missing: %q", want)
@ -69,16 +67,12 @@ func TestPreambleNoTools(t *testing.T) {
p.Set(SectionSystem, "# Core System Prompt\nTest")
p.Set(SectionEnv, "# Environment\n- Test env")
p.Set(SectionAgent, "# Agent Prompt\nTest instructions")
p.Set(SectionTools, renderToolListing(nil))
p.Set(SectionToolDocs, renderToolDocs(nil))
p.Set(SectionTools, renderTools(nil))
preamble := p.String()
if strings.Contains(preamble, "# Available Tools") {
t.Error("no tools → no Available Tools section")
}
if strings.Contains(preamble, "# Tool Documentation") {
t.Error("no tools → no Tool Documentation section")
if strings.Contains(preamble, "# Tools") {
t.Error("no tools → no Tools section")
}
if !strings.Contains(preamble, "# Core System Prompt") {
t.Error("core section missing")

View File

@ -260,17 +260,6 @@ func handleRPC(ctx context.Context, srv *toolsrv.Server, req toolsrv.RPCRequest,
srv.SetCWD(params.Dir)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
case "set_allow_tools":
var params struct {
Names []string `json:"names"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &toolsrv.RPCError{Code: -32602, Message: err.Error()}})
return
}
srv.SetAllowTools(params.Names)
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: true})
case "detach":
ok := srv.Detach()
enc.Encode(rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: ok})

View File

@ -2,54 +2,62 @@
Each `.json` file defines an agent configuration. Agent configs are installed to `~/.config/ollie/agents/` and selected at runtime via `/agent <name>`.
## default
Base agent with no specializations. Provides the standard ollie toolset and prompt assembly. Also serves as the base for JIT-generated subagents — `subagent_generate` copies `default.json` and replaces the prompt with SYSTEM_PROMPT + the identity text.
## coding
Coding-focused agent. Extends default with agent instructions covering code modification discipline, security practices, and exploratory reading habits. Also enables the `execute_bypass_bash` trusted tool for operations requiring bypass privileges, and configures an MCP server for bypass operations.
## System prompt
The system prompt is assembled from the `prompt` array in the agent config. Each entry is a shell command whose stdout is appended to the prompt. Commands run in the session's working directory with access to `$PWD`.
The base system prompt is embedded in the binary and automatically prepended.
The fully assembled system prompt is visible at `session/{sname}/agent/{aname}/systemprompt`.
## Hooks
Hooks are lifecycle callbacks executed at specific points:
| Hook | When | Use case |
|---|---|---|
| `agentSpawn` | Session creation | Start bypass adapter, initialize state |
| `postTurn` | After each turn | Task list display, logging |
| `preCompact` | Before context compaction | Save state |
| `postCompact` | After context compaction | Notify user |
| `turnError` | On turn error (rate limit, transient failure) | Fallback handling, model switching |
Hook commands run with `OLLIE_SESSION_ID` set to the current session ID.
## MCP Servers
Agent configs can specify MCP (Model Context Protocol) servers for external tool providers:
## Config format
```json
{
"mcpServers": {
"server-name": {
"command": "server-binary",
"args": [],
"env": {"KEY": "value"}
}
}
"prompt": ["$XDG_CONFIG_HOME/ollie/prompts/agent-coding.md"],
"autoLoad": ["shell", "file_read", "file_edit", "file_glob", "file_grep"],
"maxSteps": 25,
"temperature": 0.5,
"maxTokens": 16384,
"backend": "openrouter",
"model": "deepseek/deepseek-v4-flash",
"systemPrompt": "/path/to/override.md",
"compactionModel": "cheap-model-name"
}
```
Tools from MCP servers are registered alongside built-in tools and can be invoked by the agent.
| Field | Purpose |
|-------|---------|
| `prompt` | File paths to concatenate as the agent prompt. Env vars expanded. |
| `autoLoad` | Tools loaded at session start. |
| `maxSteps` | Cap on tool-call rounds per turn. 0 = unlimited. |
| `temperature` | Sampling temperature. |
| `maxTokens` | Max output tokens per response. |
| `backend` | Override backend for this agent. |
| `model` | Override model for this agent. |
| `systemPrompt` | Override the embedded system prompt with a file path. |
| `compactionModel` | Use a cheaper model for context compaction. |
## Agents
| Name | Role |
|------|------|
| `default` | Base coding agent. Also the base for JIT subagents. |
| `driver` | Full-featured agent with all tools, LSP, GUI, memory, subagents. |
| `copilot` | Read-only code copilot. Suggests changes as code fences. |
| `navigator` | Pair programming partner. Reads and advises, doesn't edit. |
| `explorer` | Code exploration and explanation. Read-only. |
| `librarian` | Document search and knowledge curation. Read-only. |
| `taskmanager` | TODO list management and planning. |
| `theo` | Security auditor (Theo de Raadt voice). Read-only. |
## System prompt assembly
The final system prompt sent to the model is assembled in sections:
1. **system** — embedded base prompt (`prompts/system_prompt.md`)
2. **env** — runtime environment (cwd, platform, date, git status)
3. **agent** — concatenated files from the `prompt` array
4. **tools** — auto-generated from loaded tools (name + `.meta` prompt)
The fully assembled prompt is readable at `session/{sname}/agent/{aname}/systemprompt`.
## Adding a new agent
Copy `default.json`, rename it, and adjust the `prompt` array to include the prompts you want. Add hooks, trusted tools, MCP servers, and generation parameters as needed.
1. Copy `default.json`, rename it.
2. Adjust the `prompt` array to reference your prompt files in `data/prompts/`.
3. Set `autoLoad` to the tools this agent needs.
4. Set generation parameters as needed.
5. Run `just install-data` to install.

View File

@ -9,10 +9,5 @@
"file_read",
"file_glob",
"file_grep"
],
"allowTools": [
"file_read",
"file_glob",
"file_grep"
]
}

View File

@ -4,6 +4,8 @@
"$XDG_CONFIG_HOME/ollie/prompts/agent-coding.md"
],
"autoLoad": [
"shell",
"reasoning_think",
"file_read",
"file_edit",
"file_glob",

View File

@ -32,8 +32,7 @@
"process_dismiss",
"logseq",
"system_logs",
"browser_screencap",
"route"
"browser_screencap"
],
"backend": "openrouter",
"model": "deepseek/deepseek-v4-flash",

View File

@ -12,13 +12,6 @@
"memory_recall",
"memory_remember"
],
"allowTools": [
"file_read",
"file_glob",
"file_grep",
"memory_recall",
"memory_remember"
],
"maxSteps": 20,
"temperature": 0.3,
"maxTokens": 16384

View File

@ -16,17 +16,6 @@
"memory_recall",
"memory_remember"
],
"allowTools": [
"file_read",
"file_glob",
"file_grep",
"file_write",
"file_edit",
"subagent_generate",
"subagent_spawn",
"memory_recall",
"memory_remember"
],
"maxSteps": 20,
"temperature": 0.3,
"maxTokens": 8192

View File

@ -1,5 +1,10 @@
You are a coding agent.
# Output
- Format output as markdown.
- Short declarative sentences. No qualifiers. No weasel words. No filler. Say what needs to be said and stop.
# Planning
- Before non-trivial tasks: inspect, plan, act, verify, update plan.
@ -46,3 +51,36 @@ You are a coding agent.
- Do not introduce security vulnerabilities: command injection, XSS, SQL injection, path traversal, etc.
- If you notice insecure code you wrote, fix it immediately.
# API Documentation
**When working with any API, framework, or library: if you are not highly confident in your knowledge of the specific methods, properties, or behaviors involved, you MUST look up the official documentation before writing code.**
Do not guess at API behavior. Do not rely on pattern matching from similar-looking APIs. Do not assume method signatures or property semantics.
**Procedure:**
1. Identify the specific API/framework/library in use (Qt, React, Go stdlib, etc.)
2. If uncertain about any method, property, or behavior, fetch the official docs via `curl` or `shell`
3. Read the relevant sections before implementing
4. Cite what you learned when explaining your implementation
**Examples of when to look up docs:**
- Using a method you haven't used recently
- Uncertain whether a property is read-only
- Unsure what signals/events are emitted and when
- Don't know the exact return type or error conditions
- Working with positioning, layout, or scrolling APIs (these are notoriously tricky)
**How to fetch docs:**
```bash
# Qt documentation
curl -s "https://doc.qt.io/qt-6/qml-qtquick-listview.html" | grep -A10 "methodName"
# Go stdlib
go doc package.Function
# MDN for web APIs
curl -s "https://developer.mozilla.org/en-US/docs/Web/API/..."
```
This is not optional. Guessing at APIs wastes time, breaks code, and frustrates users.

View File

@ -2,6 +2,11 @@ You are a code exploration and explanation agent. You answer questions about cod
You do NOT edit files. You read, trace, and explain.
# Output
- Format output as markdown.
- Short declarative sentences. No qualifiers. No weasel words. No filler. Say what needs to be said and stop.
# How you work
- Use LSP aggressively: go-to-definition, find references, hover for types. Don't guess what something is when you can look it up.

View File

@ -1,5 +1,10 @@
You are a knowledge curator agent. You scan, search, and read documents to answer questions, produce summaries, and synthesize information.
# Output
- Format output as markdown.
- Short declarative sentences. No qualifiers. No weasel words. No filler. Say what needs to be said and stop.
# Role
- Search and read documents in `$HOME/doc` to answer user queries.

View File

@ -2,6 +2,11 @@ You are a Navigator — a pair programming partner operating at strategic altitu
You do NOT edit files. You use your tools to read, search, and understand — never to modify.
# Output
- Format output as markdown.
- Short declarative sentences. No qualifiers. No weasel words. No filler. Say what needs to be said and stop.
# Thinking Modes
**Strategic awareness**

View File

@ -1,5 +1,10 @@
You are a task management agent. You maintain the user's TODO list — adding, completing, refiling, and organizing tasks.
# Output
- Format output as markdown.
- Short declarative sentences. No qualifiers. No weasel words. No filler. Say what needs to be said and stop.
# Role
- Sync external sources (tickets, notes, project docs) into the TODO list when asked.

150
doc/prompt-audit.md Normal file
View File

@ -0,0 +1,150 @@
# Prompt Construction Audit
Date: 2026-08-09
## Architecture
```
┌──────────────────────────────────────────────────────────┐
│ Final system message (one blob) │
├────────────┬──────────┬────────┬─────────┬───────────────┤
│ system │ env │ agent │ tools │ tool_docs │
│ (embedded) │(runtime) │(.json) │(listing)│(.meta prompts)│
└────────────┴──────────┴────────┴─────────┴───────────────┘
```
- **system**: `prompts/system_prompt.md` — embedded in binary, always prepended
- **env**: `EnvironmentBlock()` — cwd, platform, date, git status, sandbox
- **agent**: resolved from `prompt` array in agent JSON config (file paths → concatenated)
- **tools**: `"- **name** — description"` per loaded tool
- **tool_docs**: full `.meta` prompt text + JSON schema per tool (where `Server == ""`)
All sections join with single `\n`. Sent as a single system message before conversation history on every turn.
---
## Findings
### 1. System prompt is agent-unaware
The embedded `system_prompt.md` is identical for every agent profile. It contains:
- ~100 lines of **9P namespace reference** (session management, ctl commands, examples)
- Large **tool-first table** mapping tasks→tools (lsp_*, gui_*, memory_*)
- **Sandbox & Bypass** section
- **API documentation fetching** section (curl examples)
Agents that can't run `ollie-9p` (copilot, librarian, explorer) pay the token cost for all of it. The model sees instructions about tools it doesn't have.
**Affected agents**: all (but most wasteful for restricted agents)
---
### 2. Tool visibility vs. executability mismatch
`ListTools()` returns all loaded tools regardless of `allowTools`. The restriction only fires at execution time.
**Example**: `copilot.json` autoLoads `shell` + `reasoning_think`, then sets `allowTools: [file_read, file_glob, file_grep]`. The model sees shell in the preamble, the tool listing, tool_docs, AND the function-calling schema — tries to call it — gets rejected at execution.
**Code path**: `BuildRuntime()` calls `srv.ListTools()` first, then `srv.SetAllowTools()` after. The listing is never filtered.
**Affected agents**: copilot, librarian, taskmanager (any with `allowTools`)
---
### 3. Prompt references tools that don't exist or aren't loaded
| Reference | Where | Problem |
|-----------|-------|---------|
| `route` | driver.json autoLoad | No tool or .meta exists — it's a 9P file |
| `reasoning_think` | agent-coding.md line 35 | default.json doesn't load it |
| `lsp_*` | system_prompt.md table | Only loaded in driver.json |
| `gui_*` | system_prompt.md table | Only in navigator/explorer/driver |
| `memory_*` | system_prompt.md table | Only in librarian/taskmanager/driver |
The model either ignores phantom tool references (best case) or tries to use them (worst case, wastes a step).
---
### 4. Redundant/conflicting content between layers
- System prompt: "Format output as markdown" — copilot prompt has strict code-fence format
- System prompt: "Never agree with something incorrect" — user-preferences.md says the same differently
- System prompt: tool-first principle — coding prompt says "run the build, run tests" but default.json has no shell
- user-preferences.md included in every agent — but system prompt already covers accuracy/honesty
Repeated instructions dilute signal. Conflicting format guidance forces reconciliation.
---
### 5. 9P reference is documentation, not behavioral instruction
The 9P section reads like API docs for a frontend developer. Lists every file, mode, operation. The agent actually needs:
1. How to read/write plan: `ollie-9p write session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/plan`
2. How to load a tool: `tool_load` tool handles this now
Everything else (session/new, session/idx, generate, complete, route, etc.) is irrelevant to agent behavior.
~100 lines of prompt budget on reference material the model rarely uses.
---
### 6. Tool docs section is bloated with schemas
`renderToolDocs()` includes full `.meta` prompt AND JSON schema for every loaded tool with `Server == ""` and a non-empty prompt.
The function-calling API already provides the schema to the model natively. The tool_docs section is a second copy.
For a fully-loaded driver agent (30+ tools): ~500+ lines of preamble on duplicated schema information.
---
### 7. `PRIME_*` env vars — legacy naming
`PromptEnv()` produces `PRIME_CWD`, `PRIME_PLATFORM`, `PRIME_DATE`, `PRIME_IS_GIT_REPO`. Inherited from upstream fork. Inconsistent with `OLLIE_SESSION_ID`, `OLLIE_UNAME`.
Minor impact but confusing for anyone reading configs.
---
### 8. `agents/README.md` is stale
References things that don't exist:
- A `coding` agent JSON (no file — the prompt is used by default.json and driver.json)
- `execute_bypass_bash` trusted tool
- Hooks and MCP servers (no current config uses them)
- "Shell command prompt entries" (prompts are file paths now, not commands)
---
## Priority ranking
| # | Finding | Impact | Effort | Status |
|---|---------|--------|--------|--------|
| 1 | System prompt agent-unaware | High | Medium | RESOLVED — trimmed content, not architecture |
| 2 | Tool visibility/executability mismatch | High | Low | RESOLVED — removed AllowTools entirely |
| 3 | Phantom tool references | Medium | Low | RESOLVED — removed route, added reasoning_think |
| 4 | Redundant/conflicting content | Medium | Medium | RESOLVED — moved output protocol to agents, added shell to default |
| 5 | 9P reference bloat | High | Medium | RESOLVED — trimmed to essentials |
| 6 | Tool docs schema duplication | High | Medium | RESOLVED — merged listing+docs, removed schema dump |
| 7 | PRIME_* legacy naming | Low | Low | RESOLVED — killed entirely, direct struct fields |
| 8 | Stale README | Low | Low | RESOLVED — rewritten |
## Decisions made
### Finding 1: System prompt is agent-unaware
**Decision**: Not an architecture problem. The system prompt IS generic and should remain so — all agents live in 9P, all agents can use ollie-9p, all agents have sandbox. The fix is content quality, not composability.
Changes applied to `prompts/system_prompt.md`:
- Trimmed Core Identity (removed filler paragraph)
- Removed shell heredoc reference from Output protocol, removed size restriction
- Moved API Documentation section to `data/prompts/agent-coding.md` (coding-specific)
- Rewrote Tool & Skill Registry: clear tool/skill distinction, explicit `ollie-9p read tools`, no vague "write to ctl"
- Removed entire Tool-First Principle section (16-row table, hard rule, rationale) — dynamic registry and tool_docs handle this now; will re-add if needed
- Trimmed 9P section: removed dead files (complete, route), removed help/aliases, merged tables, replaced abstract {sname}/{aname} with $OLLIE_SESSION_ID/$OLLIE_UNAME for self-ops, cut examples from 15 to 6
Result: ~200 lines → ~90 lines. Higher signal density.
### Finding 5: 9P reference bloat
**Decision**: Resolved as part of finding 1. Kept all namespace tables (agents need to know the API) but cut dead entries and reduced examples to what agents actually do.

View File

@ -1023,16 +1023,65 @@ The TUI streaming regression (line-by-line instead of char-by-char since Aug 4)
### SLOC (Aug 9)
| Component | Lines (excl. tests, backends, generated) |
| Component | Lines (excl. tests, backends, fsedsl, generated) |
|-----------|--------------------:|
| agent | 3,178 |
| toolsrv | 2,497 |
| fs | 1,737 |
| session | 1,402 |
| cmd/olliesrv | 1,165 |
| agent | 3,167 |
| toolsrv | 2,521 |
| fs | 1,774 |
| session | 1,386 |
| cmd/olliesrv | 1,161 |
| bypass | 733 |
| lib9p + client | 690 |
| lib9p | 690 |
| cmd/ollie-9p | 356 |
| cmd/ollie-remote | 369 |
| sandbox | 293 |
| log + env + paths + format | 363 |
| **Total (core)** | **12,414** |
| log + env + paths + format + prompts | 428 |
| **Total (core)** | **12,878** |
---
## Aug 9, 2026 (cont.) — Prompt Audit & Maintainability Pass
Systematic audit of the prompt construction system and codebase maintainability.
### Prompt System
- **System prompt**: ~200 → ~80 lines. Removed tool-first table, API docs section (moved to agent prompt), dead 9P entries, output protocol (moved per-agent).
- **AllowTools**: removed entirely (field, RPC, config). Tools are loaded dynamically; static allowlists served no purpose.
- **Phantom refs fixed**: `route` removed from driver autoLoad, `reasoning_think` + `shell` added to default autoLoad.
- **Output protocol**: moved from system prompt to per-agent prompts. Theo's brevity rule applied to all agents except copilot.
- **Tool docs**: merged listing + documentation into single `renderTools()`. Removed JSON schema dump (function-calling API provides it natively). Removed `BuildToolListing` dead code.
- **PRIME_* env vars**: removed entirely. Replaced `SessionInfra.PromptEnv []string` with typed `Platform string` + `IsGitRepo bool` fields.
### Code Quality
- **`OnToolsChanged`**: changed from `func(string)` to `func()` signal. Agent re-fetches tool list on signal instead of receiving a pre-formatted string it ignores.
- **`Preamble` sections**: type-safe `Section` type replaces raw strings. Compile-time typo detection.
- **`extractToolResult`**: documented the tool output protocol (JSON content-block envelope with raw-text fallback).
- **Package godoc**: `session/` package comment documenting Init→Create→Register→Kill lifecycle.
- **`PromptEnv()`**: deleted (was a no-op returning nil after PRIME_* removal).
### Documentation
- Moved 7 reference docs from `doc/resources/` to `doc/` (architecture, 9p, writing-tools, tool-registry, core, remote-execution, edsl). Left whitepaper material (evolution, multi-agent, misc, no-mcp, ideas) in `resources/`.
- Rewrote `data/agents/README.md` to match current config schema and agent roster.
- Fixed stale cross-references in README.md and doc/tool-registry.md.
### SLOC (Aug 9, post-audit)
| Component | Lines (excl. tests, generated) |
|-----------|--------------------:|
| agent | 3,159 |
| toolsrv | 2,464 |
| session | 1,377 |
| fs | 1,774 |
| cmd/olliesrv | 1,161 |
| bypass | 733 |
| lib9p | 690 |
| cmd/ollie-9p | 356 |
| cmd/ollie-remote | 358 |
| sandbox | 293 |
| log + env + paths + format + prompts | 428 |
| **Total (core)** | **12,793** |
Excludes: backends (4,229), fsedsl (1,030), tests, KDE, tools, generated code.

View File

@ -74,7 +74,7 @@ sudo — the privilege wrapping is entirely in the dispatch layer.
```
Credentials are prompted via `kdialog`/`zenity` on desktop, or forwarded over
SSH for remote execution. See [`doc/resources/writing-tools.md`](doc/resources/writing-tools.md) for
SSH for remote execution. See [`doc/writing-tools.md`](doc/writing-tools.md) for
details.
### Host-conditional variants
@ -105,5 +105,5 @@ the registry — it doesn't exist on this host.
On a systemd host the model sees unit filtering and time ranges. On Alpine it
sees tail + grep. On a host with neither, the tool doesn't appear. One `.meta`
deploys everywhere. See [`doc/resources/writing-tools.md`](doc/resources/writing-tools.md) for the
deploys everywhere. See [`doc/writing-tools.md`](doc/writing-tools.md) for the
full variant specification.

View File

@ -1,14 +1,11 @@
# Core Identity
You are Ollie, a highly capable, general-purpose AI agent designed to assist across many domains while adapting your behavior, reasoning style, vocabulary, and output format to the user's goals. You are a single continuous agent with stable operating principles, not a collection of disconnected personas. When adopting a domain-specific role, you are changing mode, not identity.
You are Ollie, a general-purpose AI agent. You adapt your behavior, reasoning style, and output format to the user's goals. You are a single continuous agent with stable operating principles — when adopting a domain-specific role, you are changing mode, not identity.
Your core function is to be useful, accurate, adaptive, and context-aware. You should behave like a flexible expert generalist: able to provide broad help by default, and able to assume specialized domain roles when the task requires it.
# Output protocol
# Output
- Format output as markdown.
- Never embed large documents (>500 characters) directly in tool arguments.
- For large content, use `shell` with heredoc or `file_write`.
- Be direct, brief, and to the point. Do NOT produce verbose explanations, narration, or filler. Say what needs to be said and stop.
# Accuracy and honesty
@ -18,97 +15,24 @@ Your core function is to be useful, accurate, adaptive, and context-aware. You s
- Never present speculation as fact. If you don't know, say you don't know.
- When a request seems ambiguous, investigate the cwd and project structure before asking for clarification. The answer is usually one command away.
# API Documentation
**When working with any API, framework, or library: if you are not highly confident in your knowledge of the specific methods, properties, or behaviors involved, you MUST look up the official documentation before writing code.**
Do not guess at API behavior. Do not rely on pattern matching from similar-looking APIs. Do not assume method signatures or property semantics.
**Procedure:**
1. Identify the specific API/framework/library in use (Qt, React, Go stdlib, etc.)
2. If uncertain about any method, property, or behavior, fetch the official docs via `curl` or `shell`
3. Read the relevant sections before implementing
4. Cite what you learned when explaining your implementation
**Examples of when to look up docs:**
- Using a method you haven't used recently
- Uncertain whether a property is read-only
- Unsure what signals/events are emitted and when
- Don't know the exact return type or error conditions
- Working with positioning, layout, or scrolling APIs (these are notoriously tricky)
**How to fetch docs:**
```bash
# Qt documentation
curl -s "https://doc.qt.io/qt-6/qml-qtquick-listview.html" | grep -A10 "methodName"
# Go stdlib
go doc package.Function
# MDN for web APIs
curl -s "https://developer.mozilla.org/en-US/docs/Web/API/..."
```
This is not optional. Guessing at APIs wastes time, breaks code, and frustrates users.
# Tool & Skill Registry
You have autonomous access to tools and skills. Proactively load what you need — don't wait to be told.
You have autonomous access to tools and skills. These are two different things:
- **Tools** are executable functions. Call them directly by name with JSON arguments.
- **Skills** are markdown knowledge modules. They inject reference material into your context. You interact with skills through the `skill_list` and `skill_load` tools.
Proactively load what you need — don't wait to be told.
## Tools
Tools are loaded into your session at startup (configured in the agent's `autoLoad` list). Additional tools can be loaded dynamically via the agent's ctl file:
Tools are loaded at startup. Additional tools can be loaded at runtime via the `tool_load` tool. Once loaded, a tool is a first-class function — call it directly by name. Do not invoke tools through `shell`.
- **List loaded tools** — write `tools` to `ctl`
- **Load a tool** — write `tool_load <name>` to `ctl`
Once loaded, a tool becomes a first-class function. Call it directly by name with JSON arguments matching its schema.
To see all available tools (loaded and unloaded): `ollie-9p read tools`
## Skills
Skills are markdown modules that provide specialized domain knowledge, conventions, and commands. Loading a skill injects its content directly into your context.
- **`skill_list`** — discover available skill modules.
- **`skill_load`** — load a skill into context: `{"name": "skillname"}`.
**Autonomous behavior**: When you encounter a task that maps to an available skill (e.g., web dev → `web-dev-browser-screencapture`, git work → `github-cli`, knowledge queries → `agent-kb`), load the relevant skill immediately. Do not ask for permission.
## Tool-First Principle
**Always prefer dedicated tools over `shell`.** The `shell` tool is a last-resort fallback, not a default. Dedicated tools are purpose-built, produce structured output, and avoid the class of errors that come from constructing shell commands (quoting, escaping, parsing text output, brittle pipelines).
> **⛔ HARD RULE: NEVER use `shell` to edit or write files.**
>
> Do not use `sed`, `awk`, `perl -pi`, `echo >`, `tee`, `cat <<EOF >`, heredocs, or any other shell construct to modify file contents. **Always** use `file_edit` for modifications and `file_write` for creation. No exceptions. No "just this once." If you catch yourself constructing a shell command that writes to a file, stop and use the dedicated tool instead.
**Procedure** — before reaching for `shell`, follow this sequence:
1. **Is a loaded tool already fit for purpose?** If so, use it directly.
2. **Search available tools** — check `session/{sname}/agent/{aname}/tools` (read) to see what's available. Load any missing tool by writing its name there.
3. **Only if no tool exists** for the operation, fall back to `shell`.
**Common mappings** (not exhaustive):
| Task | Use this | Not this |
|---|---|---|
| Read a file | `file_read` | `cat`, `head`, `tail` |
| Write/create a file | `file_write` | `echo >`, `tee`, `cat <<EOF >` |
| Edit a file | `file_edit` | `sed`, `awk`, `perl -pi` |
| Search file contents | `file_grep` | `grep`, `rg`, `ag` |
| Find files by pattern | `file_glob` | `find`, `ls`, `fd` |
| Go to definition | `lsp_definition` | `grep` for function name |
| Find references | `lsp_references` | `grep` for symbol |
| Rename a symbol | `lsp_rename` | find-and-replace across files |
| Get type info | `lsp_hover` | reading source manually |
| Check for errors | `lsp_diagnostics` | running compiler and parsing output |
| Take a screenshot | `gui_screenshot` | `scrot`, `import` |
| Manage windows | `gui_windows` | `wmctrl`, `xdotool` |
| Clipboard access | `gui_clipboard` | `xclip`, `xsel`, `wl-copy` |
| Desktop notifications | `gui_notify` | `notify-send` |
| Search memories | `memory_recall` | `grep` over memory files |
| Store a memory | `memory_remember` | `echo >` to memory path |
**Why this matters**: Shell commands produce unstructured text that requires parsing, are sensitive to locale and environment, and compound errors silently. Dedicated tools have typed inputs/outputs, built-in error handling, and consistent behavior. Using them leads to fewer mistakes and more efficient execution.
Use `skill_list` to discover available skills. Use `skill_load` to inject one into context. When you encounter a task that maps to an available skill, load it immediately without asking.
# Sandbox & Bypass
@ -138,89 +62,55 @@ Your world model is a 9P filesystem. Your session ID is `${OLLIE_SESSION_ID}`. U
| `agents` | read | Available agent names, one per line |
| `backends` | read | Available backend names, one per line |
| `models` | read | Available models (backend\tmodel per line) |
| `help` | read | Help text |
| `ctl` | write | Server control commands (e.g. `invalidate` to refresh model cache) |
| `session/` | dir | Sessions (see below) |
| `complete` | r/w | Write: JSON `{"file","prefix","suffix"}`. Read: code completion result. |
| `generate` | r/w | Write: JSON `{"prompt"}` or plain text. Read: one-shot LLM generation result. |
| `route` | r/w | Write: task description. Read: `backend=X model=Y` recommendation. |
| `generate` | r/w | Write prompt text or JSON `{"prompt":"..."}`. Read: one-shot LLM generation result. |
| `session/` | dir | Sessions |
`complete`, `generate`, and `route` are request-response files:
```bash
echo 'implement login page' | ollie-9p rdwr route
echo '{"prompt":"summarize X"}' | ollie-9p rdwr generate
echo '{"file":"main.go","prefix":"func "}' | ollie-9p rdwr complete
```
## Session Management (`session/`)
## Session (`session/{sname}/`)
| File | Mode | Purpose |
|---|---|---|
| `session/new` | r/w | Write key=value to create a new session |
| `session/idx` | read | Session index (name\tstate\tcwd\tbackend\tmodel\tagentName\tid) |
| `aliases` | read | Table of alias\tpath for all walkable-but-unlisted names |
| `session/new` | r/w | Write key=value lines to create a new session |
| `session/idx` | read | Session index (name\tstate\tcwd\tbackend\tmodel\tagentName\tid) |
| `plan` | r/w | Markdown checklist; survives compaction |
| `env` | read | Session environment variables |
| `agent/` | dir | Agents within this session |
## Session Directory (`session/{sname}/`)
## Agent (`session/{sname}/agent/{aname}/`)
| File | Mode | Purpose |
|---|---|---|
| `plan` | r/w | Agent-scoped markdown checklist; survives compaction |
| `env` | read | Session environment variables |
| `agent/` | dir | Agents within this session |
## Agent Directory (`session/{sname}/agent/{aname}/`)
Each agent has its own directory. The `{aname}` is the agent's numeric ID (stable, used for permissions).
| File | Mode | Purpose |
|---|---|---|
| `prompt` | write | Submit a prompt to this agent |
| `fifo` | r/w | Prompt queue. Write: enqueue. Read: dequeue. |
| `chat` | read | Conversation (filtered text, streamable) |
| `chat.raw` | read | Conversation (full markup with block markers) |
| `statewait` | read | Blocks until state changes; returns new value |
| `cfg` | r/w | Agent config: backend, model, cwd, params (key=value) |
| `ctl` | rdwr | Control: stop, compact, clear, inject, agent, model, models, tools, tool_load, cwd, name, systemprompt |
| `stats` | read | Agent statistics (usage=, cost=, ctxsz=) |
| `tools` | r/w | Write tool name to load. Read: list loaded tools |
| `proc/` | dir | Detached background processes |
| `prompt` | write | Submit a prompt to this agent |
| `fifo` | r/w | Prompt queue. Write: enqueue. Read: dequeue. |
| `chat` | read | Conversation (filtered text, streamable) |
| `chat.raw` | read | Full conversation with block markers |
| `statewait` | read | Blocks until state changes; returns new value |
| `cfg` | r/w | Agent config (key=value: backend, model, cwd, temperature, etc.) |
| `ctl` | rdwr | Control: stop, compact, clear, inject, agent, model, tools, tool_load, cwd, name |
| `stats` | read | Usage, cost, context size |
| `tools` | r/w | Write: load tool by name. Read: list loaded tools. |
| `proc/` | dir | Detached background processes |
## Operations
```bash
# Spawn a session
echo "cwd=$PWD" | ollie-9p write session/new
printf 'name=reviewer\ncwd=%s\n' "$PWD" | ollie-9p write session/new
# Kill a session
ollie-9p rm session/{sname}
# Rename a session
ollie-9p mv session/{sname} session/{newname}
# Send a prompt
echo "fix the bug" | ollie-9p write session/{sname}/agent/{aname}/prompt
# Read state
ollie-9p read session/{sname}/agent/{aname}/state
# Read chat
ollie-9p read session/{sname}/agent/{aname}/chat
# Control commands
echo "stop" | ollie-9p write session/{sname}/agent/{aname}/ctl
echo "model gpt-4o" | ollie-9p write session/{sname}/agent/{aname}/ctl
echo "compact" | ollie-9p write session/{sname}/agent/{aname}/ctl
# Read/write config
ollie-9p read session/{sname}/agent/{aname}/cfg
echo "temperature=0.7" | ollie-9p write session/{sname}/agent/{aname}/cfg
# Read latest response
offset=$(ollie-9p read session/{sname}/agent/{aname}/offset)
ollie-9p read session/{sname}/agent/{aname}/chat | tail -c +$((offset + 1))
# Read/write your plan
ollie-9p read session/$OLLIE_SESSION_ID/plan
echo "## Plan\n- [ ] step one" | ollie-9p write session/$OLLIE_SESSION_ID/plan
# Load a tool
echo "file_read" | ollie-9p write session/{sname}/agent/{aname}/tools
echo "file_read" | ollie-9p write session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/tools
# Control commands
echo "compact" | ollie-9p write session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl
echo "model gpt-4o" | ollie-9p write session/$OLLIE_SESSION_ID/agent/$OLLIE_UNAME/ctl
# Spawn a peer session
printf 'name=worker\ncwd=%s\n' "$PWD" | ollie-9p write session/new
# Send a prompt to another agent
echo "fix the bug" | ollie-9p write session/{sname}/agent/{aname}/prompt
# One-shot generation (no session)
echo "summarize this" | ollie-9p rdwr generate
```

View File

@ -1,3 +1,10 @@
// Package session manages the lifecycle of agent sessions.
//
// A session owns one or more agents, a tool server connection, and persistence
// state. The package-level registry (initialized via Init) tracks all active
// sessions and provides lookup, creation, and teardown.
//
// Typical flow: Init → Create → Register → (use) → Kill or Shutdown.
package session
import (
@ -384,7 +391,7 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
}
// Build prompt layers
layers := BuildPromptLayers(cfg, p.CWD, sessID, p.ID, infra.PromptEnv, "")
layers := BuildPromptLayers(cfg, p.CWD, sessID, p.ID, infra.Platform, infra.IsGitRepo, "")
// Load tools
LoadAutoLoadTools(cfg, infra.ToolsConn, sessID, p.ID, func(f string, a ...any) {
@ -393,7 +400,6 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
// Build runtime
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + p.ID}
env = append(env, infra.PromptEnv...)
rt := agent.BuildRuntime(cfg, infra.ToolsConn, p.CWD, env, layers.SystemPrompt, layers.EnvBlock)
// Create backend
@ -422,7 +428,6 @@ func buildAgent(sess *Session, p AgentParams) (*agent.Agent, error) {
Cwd: p.CWD,
SystemPrompt: layers.SystemPrompt,
EnvBlock: layers.EnvBlock,
PromptEnvExtra: infra.PromptEnv,
NewToolServer: infra.NewToolServer,
NewBackend: backend.NewWithName,
Log: pkgSink.NewLogger("agent"),

View File

@ -3,9 +3,9 @@ package session
import (
"context"
"fmt"
"strings"
"ollie/agent"
"ollie/paths"
"ollie/prompts"
"ollie/toolsrv"
)
@ -17,7 +17,8 @@ type SessionInfra struct {
Keeper *toolsrv.ProcessKeeper
ToolsConn *toolsrv.Conn
NewToolServer func() *toolsrv.Conn
PromptEnv []string
Platform string
IsGitRepo bool
}
// ToolServerConfig configures tool server spawning.
@ -70,14 +71,16 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
Keeper: r.Keeper,
ToolsConn: newToolServer(),
NewToolServer: newToolServer,
PromptEnv: agent.PromptEnv(cfg.CWD),
Platform: "linux",
IsGitRepo: paths.IsGitRepo(cfg.CWD),
}, nil
}
var proc *toolsrv.Process
var keeper *toolsrv.ProcessKeeper
var newToolServer func() *toolsrv.Conn
var promptEnv []string
var err error
var platform string
var isGitRepo bool
if cfg.RemoteTarget != "" {
proc, err = toolsrv.SpawnRemote(cfg.Ctx, toolsrv.RemoteConfig{
@ -100,11 +103,8 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
}
return conn
}
promptEnv = []string{
"PRIME_CWD=" + cfg.CWD,
"PRIME_PLATFORM=" + proc.Info.Platform,
"PRIME_IS_GIT_REPO=" + fmt.Sprintf("%v", proc.Info.IsGitRepo),
}
platform = proc.Info.Platform
isGitRepo = proc.Info.IsGitRepo
} else {
var dialOpts []toolsrv.Option
if cfg.Yolo {
@ -124,7 +124,8 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
}
return conn
}
promptEnv = agent.PromptEnv(cfg.CWD)
platform = "linux"
isGitRepo = paths.IsGitRepo(cfg.CWD)
}
toolsConn := newToolServer()
@ -134,7 +135,8 @@ func SetupToolServer(cfg ToolServerConfig) (*SessionInfra, error) {
Keeper: keeper,
ToolsConn: toolsConn,
NewToolServer: newToolServer,
PromptEnv: promptEnv,
Platform: platform,
IsGitRepo: isGitRepo,
}, nil
}
@ -145,28 +147,12 @@ type PromptLayers struct {
}
// BuildPromptLayers resolves system prompt, operational model, and environment block.
func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname string, promptEnv []string, systemPromptOverride string) PromptLayers {
env := []string{"OLLIE_SESSION_ID=" + sessID, "OLLIE_UNAME=" + uname}
env = append(env, promptEnv...)
func BuildPromptLayers(cfg *agent.AgentConfig, cwd, sessID, uname, platform string, isGitRepo bool, systemPromptOverride string) PromptLayers {
spOverride := systemPromptOverride
if spOverride == "" && cfg != nil {
spOverride = cfg.SystemPrompt
}
sysPrompt := prompts.ResolveSystemPrompt(spOverride)
platform := "linux"
isGitRepo := false
for _, e := range promptEnv {
if k, v, ok := strings.Cut(e, "="); ok {
switch k {
case "PRIME_PLATFORM":
platform = v
case "PRIME_IS_GIT_REPO":
isGitRepo = v == "true"
}
}
}
envBlock := agent.EnvironmentBlock(cwd, platform, isGitRepo, "")
return PromptLayers{

View File

@ -32,7 +32,7 @@ type Conn struct {
cleanup func()
// onToolsChanged is a callback for tool directory change notifications.
onToolsChanged func(string)
onToolsChanged func()
}
// NewConn wraps an io.ReadWriteCloser as a JSON-RPC connection.
@ -92,12 +92,7 @@ func (c *Conn) SetCWD(dir string) {
c.call("set_cwd", params)
}
func (c *Conn) SetAllowTools(names []string) {
params, _ := json.Marshal(map[string]any{"names": names})
c.call("set_allow_tools", params)
}
func (c *Conn) SetOnToolsChanged(fn func(string)) {
func (c *Conn) SetOnToolsChanged(fn func()) {
c.pendingMu.Lock()
c.onToolsChanged = fn
c.pendingMu.Unlock()

View File

@ -120,16 +120,6 @@ func (ts *testServer) handleRPC(ctx context.Context, req rpcRequest) rpcResponse
ts.srv.SetCWD(params.Dir)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)}
case "set_allow_tools":
var params struct {
Names []string `json:"names"`
}
if err := json.Unmarshal(req.Params, &params); err != nil {
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Error: &rpcError{Code: -32602, Message: err.Error()}}
}
ts.srv.SetAllowTools(params.Names)
return rpcResponse{JSONRPC: "2.0", ID: req.ID, Result: json.RawMessage(`true`)}
case "detach":
ok := ts.srv.Detach()
result, _ := json.Marshal(ok)
@ -387,34 +377,6 @@ func TestRPC_SetCWD(t *testing.T) {
}
}
func TestRPC_SetAllowTools(t *testing.T) {
ts := newTestServer(t)
defer ts.srv.Close()
ctx := context.Background()
params, _ := json.Marshal(map[string]any{"names": []string{"tool1", "tool2"}})
req := rpcRequest{JSONRPC: "2.0", ID: 1, Method: "set_allow_tools", Params: params}
resp := ts.handleRPC(ctx, req)
if resp.Error != nil {
t.Fatalf("unexpected error: %v", resp.Error.Message)
}
var result bool
if err := json.Unmarshal(resp.Result, &result); err != nil {
t.Fatalf("unmarshal result: %v", err)
}
if !result {
t.Error("expected result=true")
}
// Verify allowed tools are set
allowed := ts.srv.AllowTools()
if len(allowed) != 2 {
t.Errorf("expected 2 allowed tools, got %d", len(allowed))
}
}
func TestRPC_Detach_NoRunningProcess(t *testing.T) {
ts := newTestServer(t)
defer ts.srv.Close()

View File

@ -48,10 +48,6 @@ type Server struct {
// Yolo skips the landrun sandbox for all execution.
Yolo bool
// allowTools restricts which named tool scripts can be invoked via call_tool/pipe.
// Empty means all are allowed.
allowTools map[string]bool
toolRegistry *Registry
sessionID string
@ -59,9 +55,9 @@ type Server struct {
// should be injected into the agent's context.
OnInjection func(content string)
// OnToolsChanged is called when tool_list detects the available tools
// have changed, providing the updated preamble-format listing.
OnToolsChanged func(listing string)
// OnToolsChanged is called when the set of loaded tools changes.
// The receiver should re-fetch the tool list to update its state.
OnToolsChanged func()
// rate limiting state (per-Server)
rateLimitMu sync.Mutex
@ -85,30 +81,6 @@ func WithYolo() Option { return func(s *Server) { s.Yolo = true } }
// WithAllowTools restricts which tool scripts can be invoked.
func WithAllowTools(names []string) Option {
return func(s *Server) {
if len(names) > 0 {
s.allowTools = make(map[string]bool, len(names))
for _, n := range names {
s.allowTools[n] = true
}
}
}
}
// AllowTools returns the set of allowed tool names, or nil if unrestricted.
func (s *Server) AllowTools() []string {
if len(s.allowTools) == 0 {
return nil
}
out := make([]string, 0, len(s.allowTools))
for k := range s.allowTools {
out = append(out, k)
}
return out
}
// WithToolRegistry attaches a tool registry and session ID to the Server.
func WithToolRegistry(r *Registry, sessionID string) Option {
return func(s *Server) {
@ -119,8 +91,8 @@ func WithToolRegistry(r *Registry, sessionID string) Option {
// LoadTool loads a tool by name into this server's tool registry.
// Returns an error if no registry is configured or the tool cannot be found.
// If OnToolsChanged is set, it is called with the updated tool listing so the
// system prompt reflects the newly available tool immediately.
// If OnToolsChanged is set, it is called to signal that the available tools
// have changed. The caller is responsible for re-fetching the current list.
func (s *Server) LoadTool(name string) error {
if s.toolRegistry == nil {
return fmt.Errorf("no tool registry configured")
@ -132,8 +104,7 @@ func (s *Server) LoadTool(name string) error {
return err
}
if s.OnToolsChanged != nil {
listing := BuildToolListing(s.toolRegistry.Loaded(s.sessionID))
s.OnToolsChanged(listing)
s.OnToolsChanged()
}
return nil
}
@ -173,18 +144,6 @@ func (s *Server) SetCWD(dir string) {
s.wdMu.Unlock()
}
// SetAllowTools restricts which tool scripts can be invoked.
func (s *Server) SetAllowTools(names []string) {
if len(names) > 0 {
s.allowTools = make(map[string]bool, len(names))
for _, n := range names {
s.allowTools[n] = true
}
} else {
s.allowTools = nil
}
}
// SetEnv adds a session-scoped environment variable injected into all
// subsequent subprocess invocations for this session.
func (s *Server) SetEnv(key, value string) {
@ -394,7 +353,7 @@ func (s *Server) cleanupDetached() {
}
// SetOnToolsChanged sets the callback for tool directory changes.
func (s *Server) SetOnToolsChanged(fn func(string)) {
func (s *Server) SetOnToolsChanged(fn func()) {
s.OnToolsChanged = fn
}
@ -407,15 +366,4 @@ func (s *Server) ToolRegistryRevision() uint64 {
return s.toolRegistry.Revision(s.sessionID)
}
// BuildToolListing formats a slice of ToolInfo into the preamble listing
// format: "- **name** — description\n" per entry.
// Only local tools (Server == "") with a non-empty description are included.
func BuildToolListing(infos []ToolInfo) string {
var sb strings.Builder
for _, ti := range infos {
if ti.Description != "" && ti.Server == "" {
fmt.Fprintf(&sb, "- **%s** — %s\n", ti.Name, ti.Description)
}
}
return sb.String()
}