remove built-in SYSTEM_PROMPT.md loading

System prompt is now assembled via x/prime in the agent prompt array,
same as all other prompt fragments. Remove expandSystemPrompt and
the agentPrompt field — resolved prompt goes directly into preamble.
This commit is contained in:
Levi Neely 2026-04-26 15:10:05 +02:00
parent d0e1b6f6ef
commit cafeff4a1e
3 changed files with 23 additions and 114 deletions

View File

@ -186,7 +186,6 @@ func (s *agent) handleCommand(ctx context.Context, input string, handler EventHa
s.cfg.Tools = env.tools
s.cfg.Exec = env.exec
s.cfg.GenerationParams = env.genParams
s.agentPrompt = env.agentPrompt
s.agentName = name
s.session = nil
s.sessionID = NewSessionID()

View File

@ -9,7 +9,6 @@ import (
"io"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"slices"
"strconv"
@ -32,7 +31,6 @@ type AgentEnv struct {
exec toolExecutor
Hooks Hooks
preamble string
agentPrompt string // agent-specific suffix appended after the base system prompt
genParams backend.GenerationParams
Messages []string
}
@ -57,14 +55,14 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv
allTools := toolInfosToBackend(allToolInfos)
hooks := Hooks{}
agentPrompt := ""
var preamble string
var genParams backend.GenerationParams
if cfg != nil {
for k, v := range cfg.Hooks {
hooks[k] = []string(v)
}
if resolved, err := resolvePrompt(cfg.Prompt, cwd); err == nil {
agentPrompt = resolved
preamble = resolved
}
genParams = backend.GenerationParams{
MaxTokens: cfg.MaxTokens,
@ -81,13 +79,6 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv
}
}
path := DefaultPromptsDir() + "/SYSTEM_PROMPT.md"
data, err := os.ReadFile(path)
if err != nil {
panic(fmt.Sprintf("loadSystemPrompt: %s: %v", path, err))
}
preamble := expandSystemPrompt(string(data), cwd)
exec := func(ctx context.Context, name string, args json.RawMessage) (string, error) {
server, ok := serverOf[name]
if !ok {
@ -110,7 +101,6 @@ func BuildAgentEnv(cfg *config.Config, d tools.Dispatcher, cwd string) AgentEnv
exec: exec,
Hooks: hooks,
preamble: preamble,
agentPrompt: agentPrompt,
genParams: genParams,
Messages: messages,
}
@ -122,31 +112,6 @@ func DefaultPromptsDir() string {
return paths.CfgDir() + "/prompts"
}
func expandSystemPrompt(content, cwd string) string {
if cwd == "" {
cwd, _ = os.Getwd()
}
isGit := "false"
if _, err := os.Stat(filepath.Join(cwd, ".git")); err == nil {
isGit = "true"
}
mapper := func(key string) string {
switch key {
case "PWD":
return cwd
case "PRIME_DATE":
return time.Now().Format("2006-01-02")
case "PRIME_PLATFORM":
return strings.ToLower(runtime.GOOS)
case "PRIME_IS_GIT_REPO":
return isGit
default:
return os.Getenv(key)
}
}
return os.Expand(content, mapper)
}
// AgentConfigPath resolves the config file path for a named agent.
func AgentConfigPath(agentsDir, name string) string {
@ -204,7 +169,6 @@ type agent struct {
newDispatcher func() tools.Dispatcher
newBackend func(string) (backend.Backend, error)
cwd string
agentPrompt string // agent-specific prompt suffix; kept for system prompt rebuilds
startupMessages []string
currentAction atomic.Pointer[actionHandle]
fifo PromptFIFO
@ -318,7 +282,6 @@ func NewAgentCore(cfg AgentCoreConfig) Core {
sessionsDir: cfg.SessionsDir,
sessionID: cfg.SessionID,
cwd: paths.ExpandHome(cfg.CWD),
agentPrompt: cfg.Env.agentPrompt,
startupMessages: cfg.Env.Messages,
dispatcher: cfg.Env.dispatcher,
newDispatcher: cfg.NewDispatcher,
@ -512,9 +475,6 @@ func (s *agent) spawnContext(ctx context.Context, handler EventHandler) string {
handler(infoEvent(result.Warning))
}
var parts []string
if s.agentPrompt != "" {
parts = append(parts, s.agentPrompt)
}
if result.Context != "" {
parts = append(parts, result.Context)
}

View File

@ -526,45 +526,6 @@ func TestAgentSpawnFiresOnce(t *testing.T) {
}
}
// --- expandSystemPrompt ---
func TestExpandSystemPrompt_IsGitTrue(t *testing.T) {
dir := t.TempDir()
os.Mkdir(filepath.Join(dir, ".git"), 0700)
got := expandSystemPrompt("${PRIME_IS_GIT_REPO}", dir)
if got != "true" {
t.Errorf("got %q; want true", got)
}
}
func TestExpandSystemPrompt_IsGitFalse(t *testing.T) {
got := expandSystemPrompt("${PRIME_IS_GIT_REPO}", t.TempDir())
if got != "false" {
t.Errorf("got %q; want false", got)
}
}
func TestExpandSystemPrompt_PWDUsesCWD(t *testing.T) {
dir := t.TempDir()
got := expandSystemPrompt("${PWD}", dir)
if got != dir {
t.Errorf("got %q; want %q", got, dir)
}
}
// --- BuildAgentEnv: missing system prompt ---
func TestBuildAgentEnv_MissingSystemPrompt(t *testing.T) {
t.Setenv("OLLIE_CFG_PATH", t.TempDir()) // no SYSTEM_PROMPT.md written
defer func() {
if recover() == nil {
t.Error("expected panic from BuildAgentEnv; got none")
}
}()
BuildAgentEnv(nil, tools.NewDispatcher(), "")
}
// --- panic recovery ---
func TestSubmit_PanicRecovery(t *testing.T) {
@ -2237,26 +2198,20 @@ func TestFirstSentence_ShortNoSentenceEnd(t *testing.T) {
// --- BuildAgentEnv (nil config path) ---
func setupCfgDir(t *testing.T, preamble string) string {
func setupCfgDir(t *testing.T) string {
t.Helper()
dir := t.TempDir()
if err := os.MkdirAll(dir+"/prompts", 0700); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(dir+"/prompts/SYSTEM_PROMPT.md", []byte(preamble), 0600); err != nil {
t.Fatal(err)
}
t.Setenv("OLLIE_CFG_PATH", dir)
return dir
}
func TestBuildAgentEnv_NilConfig(t *testing.T) {
setupCfgDir(t, "base prompt")
setupCfgDir(t)
d := tools.NewDispatcher()
env := BuildAgentEnv(nil, d, t.TempDir())
if env.preamble != "base prompt" {
t.Errorf("preamble = %q; want %q", env.preamble, "base prompt")
if env.preamble != "" {
t.Errorf("preamble = %q; want empty", env.preamble)
}
if len(env.Hooks) != 0 {
t.Errorf("expected no hooks; got %v", env.Hooks)
@ -2269,23 +2224,19 @@ func TestBuildAgentEnv_NilConfig(t *testing.T) {
}
}
func TestBuildAgentEnv_AgentPromptAppended(t *testing.T) {
setupCfgDir(t, "base prompt")
func TestBuildAgentEnv_PromptBecomesPreamble(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"agent suffix"}}}
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"the prompt"}}}
env := BuildAgentEnv(cfg, d, t.TempDir())
// preamble contains only the system prompt; agent prompt is kept separate
if !strings.Contains(env.preamble, "base prompt") {
t.Errorf("preamble missing base: %q", env.preamble)
}
if env.agentPrompt != "agent suffix" {
t.Errorf("agentPrompt = %q; want %q", env.agentPrompt, "agent suffix")
if env.preamble != "the prompt" {
t.Errorf("preamble = %q; want %q", env.preamble, "the prompt")
}
}
func TestBuildAgentEnv_HooksAndParams(t *testing.T) {
setupCfgDir(t, "base")
setupCfgDir(t)
d := tools.NewDispatcher()
temp := 0.7
cfg := &config.Config{
@ -2307,7 +2258,7 @@ func TestBuildAgentEnv_HooksAndParams(t *testing.T) {
}
func TestBuildAgentEnv_ExecUnknownTool(t *testing.T) {
setupCfgDir(t, "base")
setupCfgDir(t)
d := tools.NewDispatcher()
env := BuildAgentEnv(nil, d, t.TempDir())
@ -2360,7 +2311,7 @@ func (m *mockTrustedSetter) CallTool(ctx context.Context, tool string, args json
}
func TestBuildAgentEnv_ListToolsError(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
d := &mockDispatcher{listErr: fmt.Errorf("boom")}
env := BuildAgentEnv(nil, d, t.TempDir())
if len(env.Messages) != 1 || !strings.Contains(env.Messages[0], "boom") {
@ -2369,7 +2320,7 @@ func TestBuildAgentEnv_ListToolsError(t *testing.T) {
}
func TestBuildAgentEnv_ToolsPopulated(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool", Description: "desc", InputSchema: json.RawMessage(`{}`)}},
}
@ -2380,7 +2331,7 @@ func TestBuildAgentEnv_ToolsPopulated(t *testing.T) {
}
func TestBuildAgentEnv_TrustedTools(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
setter := &mockTrustedSetter{}
d := &mockDispatcher{servers: map[string]tools.Server{"execute": setter}}
cfg := &config.Config{TrustedTools: []string{"bash"}}
@ -2390,19 +2341,18 @@ func TestBuildAgentEnv_TrustedTools(t *testing.T) {
}
}
func TestBuildAgentEnv_AgentPromptNoSystemPrompt(t *testing.T) {
// Empty system prompt file — agentPrompt is kept separate from preamble.
setupCfgDir(t, "")
func TestBuildAgentEnv_PromptOnly(t *testing.T) {
setupCfgDir(t)
d := tools.NewDispatcher()
cfg := &config.Config{Prompt: config.Prompt{Value: []string{"only agent"}}}
env := BuildAgentEnv(cfg, d, t.TempDir())
if env.agentPrompt != "only agent" {
t.Errorf("agentPrompt = %q; want %q", env.agentPrompt, "only agent")
if env.preamble != "only agent" {
t.Errorf("preamble = %q; want %q", env.preamble, "only agent")
}
}
func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
@ -2420,7 +2370,7 @@ func TestBuildAgentEnv_ExecDispatchSuccess(t *testing.T) {
}
func TestBuildAgentEnv_ExecDispatchError(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {
@ -2435,7 +2385,7 @@ func TestBuildAgentEnv_ExecDispatchError(t *testing.T) {
}
func TestBuildAgentEnv_ExecToolResultIsError(t *testing.T) {
setupCfgDir(t, "")
setupCfgDir(t)
d := &mockDispatcher{
tools: []tools.ToolInfo{{Server: "s1", Name: "mytool"}},
dispatch: func(ctx context.Context, server, tool string, args json.RawMessage) (json.RawMessage, error) {