stabilize provider prompt prefix
This commit is contained in:
parent
5dbfa3060f
commit
64b76d9bf0
|
|
@ -414,6 +414,9 @@ func (s *History) injectPreamble(preamble string) {
|
|||
}
|
||||
msg := backend.Message{Role: "system", Content: preamble}
|
||||
if len(s.messages) > 0 && s.messages[0].Role == "system" {
|
||||
if s.messages[0].Content == preamble {
|
||||
return
|
||||
}
|
||||
s.messages[0] = msg
|
||||
} else {
|
||||
s.messages = append([]backend.Message{msg}, s.messages...)
|
||||
|
|
|
|||
|
|
@ -5,8 +5,8 @@ import (
|
|||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"ollie/cmd/olliesrv/internal/backend"
|
||||
"ollie/cmd/olliesrv/internal/prompts"
|
||||
|
|
@ -75,15 +75,16 @@ const (
|
|||
|
||||
// --- Environment block ---
|
||||
|
||||
// EnvironmentBlock renders the environment context section for the preamble.
|
||||
// EnvironmentBlock renders stable environment context for the preamble.
|
||||
// Volatile values such as the current date do not belong in the provider-cached
|
||||
// prefix and are supplied separately when needed.
|
||||
func EnvironmentBlock(cwd, platform string, isGitRepo bool, sandbox string) string {
|
||||
date := time.Now().Format("2006-01-02")
|
||||
git := "false"
|
||||
if isGitRepo {
|
||||
git = "true"
|
||||
}
|
||||
return fmt.Sprintf("# Environment\n\n- Working directory: %s\n- Platform: %s\n- Current date: %s\n- Is git repo: %s\n- Sandbox: %s",
|
||||
cwd, platform, date, git, sandbox)
|
||||
return fmt.Sprintf("# Environment\n\n- Working directory: %s\n- Platform: %s\n- Is git repo: %s\n- Sandbox: %s",
|
||||
cwd, platform, git, sandbox)
|
||||
}
|
||||
|
||||
// --- Tool rendering ---
|
||||
|
|
@ -92,6 +93,13 @@ func EnvironmentBlock(cwd, platform string, isGitRepo bool, sandbox string) stri
|
|||
// 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 {
|
||||
infos = append([]toolsrv.ToolInfo(nil), infos...)
|
||||
sort.SliceStable(infos, func(i, j int) bool {
|
||||
if infos[i].Name == infos[j].Name {
|
||||
return infos[i].Server < infos[j].Server
|
||||
}
|
||||
return infos[i].Name < infos[j].Name
|
||||
})
|
||||
var sb strings.Builder
|
||||
for _, ti := range infos {
|
||||
if ti.Server != "" {
|
||||
|
|
@ -129,17 +137,17 @@ func RenderTools(infos []toolsrv.ToolInfo) string {
|
|||
// agent. The agent struct stores a pointer to the active Runtime; switching
|
||||
// agents replaces it atomically.
|
||||
type Runtime struct {
|
||||
Backend backend.Backend
|
||||
ToolServer *toolsrv.Conn
|
||||
Preamble *Preamble
|
||||
Tools []backend.Tool
|
||||
ToolMeta map[string]toolsrv.ToolInfo
|
||||
Exec toolExecutor
|
||||
GenParams backend.GenerationParams
|
||||
MaxSteps int
|
||||
Backend backend.Backend
|
||||
ToolServer *toolsrv.Conn
|
||||
Preamble *Preamble
|
||||
Tools []backend.Tool
|
||||
ToolMeta map[string]toolsrv.ToolInfo
|
||||
Exec toolExecutor
|
||||
GenParams backend.GenerationParams
|
||||
MaxSteps int
|
||||
CompactionModel string
|
||||
UserPrompt string // resolved user prompts, prepended to each turn's input
|
||||
Messages []string
|
||||
UserPrompt string // resolved user prompts, prepended to each turn's input
|
||||
Messages []string
|
||||
}
|
||||
|
||||
// PreambleString returns the fully rendered system prompt.
|
||||
|
|
|
|||
|
|
@ -7,75 +7,57 @@ import (
|
|||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
func TestEnvironmentBlockStable(t *testing.T) {
|
||||
got := EnvironmentBlock("/work", "linux", true, "default")
|
||||
want := "# Environment\n\n- Working directory: /work\n- Platform: linux\n- Is git repo: true\n- Sandbox: default"
|
||||
if got != want {
|
||||
t.Fatalf("EnvironmentBlock() = %q, want %q", got, want)
|
||||
}
|
||||
if strings.Contains(got, "Current date") {
|
||||
t.Fatal("environment block contains volatile current date")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreambleSections(t *testing.T) {
|
||||
toolInfos := []toolsrv.ToolInfo{
|
||||
{
|
||||
Name: "file_read",
|
||||
Description: "Read a file with line numbers",
|
||||
Prompt: "**file_read**\n\nRead a file with line numbers...",
|
||||
InputSchema: []byte(`{"type":"object"}`),
|
||||
Server: "",
|
||||
},
|
||||
{
|
||||
Name: "file_write",
|
||||
Description: "Create or overwrite a file",
|
||||
Prompt: "**file_write**\n\nCreate or overwrite a file...",
|
||||
InputSchema: []byte(`{"type":"object"}`),
|
||||
Server: "remote", // should be excluded
|
||||
},
|
||||
{
|
||||
Name: "shell",
|
||||
Description: "Execute shell command",
|
||||
Prompt: "",
|
||||
InputSchema: []byte(`{}`),
|
||||
Server: "",
|
||||
},
|
||||
{Name: "file_read", Description: "Read a file with line numbers", Prompt: "**file_read**\n\nRead a file with line numbers...", InputSchema: []byte(`{"type":"object"}`)},
|
||||
{Name: "file_write", Description: "Create or overwrite a file", Prompt: "**file_write**\n\nCreate or overwrite a file...", InputSchema: []byte(`{"type":"object"}`), Server: "remote"},
|
||||
{Name: "shell", Description: "Execute shell command", InputSchema: []byte(`{}`)},
|
||||
}
|
||||
|
||||
p := &Preamble{}
|
||||
p.Set(SectionSystem, "# Core System Prompt\nTest")
|
||||
p.Set(SectionEnv, "# Environment\n- Test env")
|
||||
p.Set(SectionAgent, "# Agent Prompt\nTest instructions")
|
||||
p.Set(SectionTools, RenderTools(toolInfos))
|
||||
|
||||
preamble := p.String()
|
||||
|
||||
// Must contain core sections
|
||||
for _, want := range []string{
|
||||
"# Core System Prompt\nTest",
|
||||
"# Environment\n- Test env",
|
||||
"# Agent Prompt\nTest instructions",
|
||||
"# Tools",
|
||||
"## file_read",
|
||||
"**file_read**\n\nRead a file with line numbers...",
|
||||
"## shell",
|
||||
"Execute shell command",
|
||||
} {
|
||||
for _, want := range []string{"# Core System Prompt\nTest", "# Environment\n- Test env", "# Agent Prompt\nTest instructions", "# Tools", "## file_read", "**file_read**\n\nRead a file with line numbers...", "## shell", "Execute shell command"} {
|
||||
if !strings.Contains(preamble, want) {
|
||||
t.Errorf("preamble missing: %q", want)
|
||||
}
|
||||
}
|
||||
|
||||
// Remote server tools excluded
|
||||
if strings.Contains(preamble, "file_write") {
|
||||
t.Error("preamble should exclude tools with Server != \"\"")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderToolsStableOrder(t *testing.T) {
|
||||
infos := []toolsrv.ToolInfo{{Name: "z_tool", Description: "z"}, {Name: "a_tool", Description: "a"}}
|
||||
first := RenderTools(infos)
|
||||
second := RenderTools([]toolsrv.ToolInfo{infos[1], infos[0]})
|
||||
if first != second || strings.Index(first, "a_tool") > strings.Index(first, "z_tool") {
|
||||
t.Fatalf("tool rendering is not deterministic: %q", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreambleNoTools(t *testing.T) {
|
||||
p := &Preamble{}
|
||||
p.Set(SectionSystem, "# Core System Prompt\nTest")
|
||||
p.Set(SectionEnv, "# Environment\n- Test env")
|
||||
p.Set(SectionAgent, "# Agent Prompt\nTest instructions")
|
||||
p.Set(SectionTools, RenderTools(nil))
|
||||
|
||||
preamble := p.String()
|
||||
|
||||
if strings.Contains(preamble, "# Tools") {
|
||||
t.Error("no tools → no Tools section")
|
||||
}
|
||||
if !strings.Contains(preamble, "# Core System Prompt") {
|
||||
t.Error("core section missing")
|
||||
if strings.Contains(preamble, "# Tools") || !strings.Contains(preamble, "# Core System Prompt") {
|
||||
t.Fatal("unexpected no-tools preamble")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -84,13 +66,9 @@ func TestPreambleSetReplaces(t *testing.T) {
|
|||
p.Set(SectionSystem, "first")
|
||||
p.Set(SectionEnv, "env")
|
||||
p.Set(SectionSystem, "second")
|
||||
|
||||
got := p.String()
|
||||
if strings.Contains(got, "first") {
|
||||
t.Error("Set should replace, not append")
|
||||
}
|
||||
if !strings.Contains(got, "second") {
|
||||
t.Error("Set should store new value")
|
||||
if strings.Contains(got, "first") || !strings.Contains(got, "second") {
|
||||
t.Fatalf("preamble replacement failed: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -99,10 +77,7 @@ func TestPreambleEmptySectionsOmitted(t *testing.T) {
|
|||
p.Set(SectionSystem, "system")
|
||||
p.Set(SectionEnv, "")
|
||||
p.Set(SectionAgent, "agent")
|
||||
|
||||
got := p.String()
|
||||
// Should be "system\nagent" with no extra newlines from empty env
|
||||
if strings.Contains(got, "\n\n") {
|
||||
if got := p.String(); strings.Contains(got, "\n\n") {
|
||||
t.Errorf("empty sections should be omitted, got: %q", got)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -135,7 +135,10 @@ func (ag *Agent) executeTurn(ctx context.Context, input string) string {
|
|||
meta[info.Name] = info
|
||||
}
|
||||
ag.runtime.ToolMeta = meta
|
||||
ag.runtime.Preamble.Set(SectionTools, RenderTools(ti))
|
||||
toolPreamble := RenderTools(ti)
|
||||
if ag.runtime.Preamble.Get(SectionTools) != toolPreamble {
|
||||
ag.runtime.Preamble.Set(SectionTools, toolPreamble)
|
||||
}
|
||||
}
|
||||
|
||||
// Install turn-scoped output handler to intercept events for reply
|
||||
|
|
|
|||
Loading…
Reference in New Issue