Simplify preamble construction system
- Extract hardcoded tool sections into prompts.ToolSections() function - Replace template system with simple Go code for better maintainability - Fix refreshToolListing to preserve tool documentation and discovery sections - Add comprehensive unit tests for prompts and agent packages - Remove unnecessary tool_sections.md template file The refactoring simplifies the preamble construction while maintaining all functionality. Tool listing updates dynamically via refreshToolListing, while tool documentation requires agent reload (full BuildPreamble call).
This commit is contained in:
parent
57052180d3
commit
605212e1fd
|
|
@ -26,7 +26,8 @@ type Agent struct {
|
|||
name string // display name (defaults to id)
|
||||
profile string // config profile name (e.g. "default")
|
||||
agentsDir string
|
||||
baseLayers []string // system prompt layers for /agent reloads
|
||||
systemPrompt string // system prompt for /agent reloads
|
||||
envBlock string // environment block for /agent reloads
|
||||
promptEnvExtra []string // PRIME_* vars for prompt resolution
|
||||
newToolServer func() toolsrv.Runner
|
||||
newBackend func(string) (backend.Backend, error)
|
||||
|
|
@ -54,8 +55,8 @@ type Agent struct {
|
|||
sessionID string // the owning session's ID
|
||||
startupMessages []string
|
||||
readPlanStep func() string
|
||||
save func() // trigger debounced persistence
|
||||
flush func() // immediately flush persistence
|
||||
save func() // trigger debounced persistence
|
||||
flush func() // immediately flush persistence
|
||||
onStateChange func(agentID, state string) // optional callback for state changes
|
||||
}
|
||||
|
||||
|
|
@ -399,16 +400,82 @@ 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.
|
||||
// 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
|
||||
}
|
||||
const marker = "\n# Available Tools\n\n"
|
||||
if idx := strings.Index(ag.runtime.Preamble, marker); idx >= 0 {
|
||||
ag.runtime.Preamble = ag.runtime.Preamble[:idx]
|
||||
|
||||
// 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
|
||||
} else if discoveryIdx >= 0 {
|
||||
insertIdx = discoveryIdx
|
||||
insertBefore = discoveryMarker
|
||||
} else {
|
||||
// Just append
|
||||
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
|
||||
}
|
||||
if listing != "" {
|
||||
ag.runtime.Preamble += marker + listing
|
||||
|
||||
// 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 {
|
||||
endIdx = discoveryIdx
|
||||
} else {
|
||||
// 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
|
||||
if availableToolsIdx > 0 && ag.runtime.Preamble[availableToolsIdx-1] == '\n' {
|
||||
beforeListing = ag.runtime.Preamble[:availableToolsIdx]
|
||||
}
|
||||
ag.runtime.Preamble = beforeListing + afterListing
|
||||
} else {
|
||||
ag.runtime.Preamble = beforeListing + listing + afterListing
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRefreshToolListing(t *testing.T) {
|
||||
// Create a test agent with minimal setup
|
||||
ag := &Agent{
|
||||
runtime: &Runtime{
|
||||
Preamble: "# Core System Prompt\nTest\n# Environment\n- Test env\n# Agent Prompt\nTest instructions\n\n# Tool Sections\n\n# Available Tools\n\n- **old_tool** — Old description\n\n# Tool Documentation\n\n**old_tool**\n\nOld docs...\n\n**Schema**:\n```json\n{}\n```\n\n## Tool discovery\nInstructions...",
|
||||
},
|
||||
}
|
||||
|
||||
// Test 1: refreshToolListing with new listing
|
||||
newListing := "- **new_tool** — New description\n"
|
||||
ag.refreshToolListing(newListing)
|
||||
|
||||
// Should contain new listing
|
||||
if !strings.Contains(ag.runtime.Preamble, "**new_tool** — New description") {
|
||||
t.Error("refreshToolListing should include new tool in listing")
|
||||
}
|
||||
|
||||
// Should NOT contain old listing
|
||||
if strings.Contains(ag.runtime.Preamble, "**old_tool** — Old description") {
|
||||
t.Error("refreshToolListing should remove old tool from listing")
|
||||
}
|
||||
|
||||
// PROBLEM: Should contain tool documentation and discovery sections
|
||||
// but current implementation removes them!
|
||||
if !strings.Contains(ag.runtime.Preamble, "# Tool Documentation") {
|
||||
t.Error("refreshToolListing currently removes # Tool Documentation section (BUG)")
|
||||
}
|
||||
if !strings.Contains(ag.runtime.Preamble, "## Tool discovery") {
|
||||
t.Error("refreshToolListing currently removes Tool discovery section (BUG)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToolListingEmpty(t *testing.T) {
|
||||
ag := &Agent{
|
||||
runtime: &Runtime{
|
||||
Preamble: "# Core System Prompt\nTest\n# Environment\n- Test env\n# Agent Prompt\nTest instructions\n\n# Tool Sections\n\n# Available Tools\n\n- **tool** — Description\n\n# Tool Documentation\n\n**tool**\n\nDocs...\n\n## Tool discovery\nInstructions...",
|
||||
},
|
||||
}
|
||||
|
||||
// Test with empty listing (all tools removed)
|
||||
ag.refreshToolListing("")
|
||||
|
||||
// Should not contain tool sections marker
|
||||
if strings.Contains(ag.runtime.Preamble, "# Available Tools") {
|
||||
t.Error("refreshToolListing with empty listing should remove # Available Tools")
|
||||
}
|
||||
|
||||
// Should still contain other sections
|
||||
if !strings.Contains(ag.runtime.Preamble, "# Core System Prompt") {
|
||||
t.Error("refreshToolListing should preserve core system prompt")
|
||||
}
|
||||
if !strings.Contains(ag.runtime.Preamble, "# Environment") {
|
||||
t.Error("refreshToolListing should preserve environment section")
|
||||
}
|
||||
if !strings.Contains(ag.runtime.Preamble, "# Agent Prompt") {
|
||||
t.Error("refreshToolListing should preserve agent prompt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRefreshToolListingNoToolsInitially(t *testing.T) {
|
||||
ag := &Agent{
|
||||
runtime: &Runtime{
|
||||
Preamble: "# Core System Prompt\nTest\n# Environment\n- Test env\n# Agent Prompt\nTest instructions\n\n## Tool discovery\nInstructions...",
|
||||
},
|
||||
}
|
||||
|
||||
// Test adding tools when none existed before
|
||||
newListing := "- **new_tool** — New description\n"
|
||||
ag.refreshToolListing(newListing)
|
||||
|
||||
// Should now contain tool sections
|
||||
if !strings.Contains(ag.runtime.Preamble, "# Available Tools") {
|
||||
t.Error("refreshToolListing should add # Available Tools when none existed")
|
||||
}
|
||||
if !strings.Contains(ag.runtime.Preamble, "**new_tool** — New description") {
|
||||
t.Error("refreshToolListing should include new tool")
|
||||
}
|
||||
}
|
||||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"ollie/backend"
|
||||
"ollie/prompts"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
|
|
@ -17,7 +18,7 @@ import (
|
|||
// env provides additional environment variables injected into prompt resolution
|
||||
// subprocesses (e.g. OLLIE_SESSION_ID=xxx).
|
||||
// The caller is responsible for registering all servers on d before calling this.
|
||||
func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string, baseLayers ...string) *Runtime {
|
||||
func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string, systemPrompt, envBlock string) *Runtime {
|
||||
var messages []string
|
||||
|
||||
var allToolInfos []toolsrv.ToolInfo
|
||||
|
|
@ -33,14 +34,14 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
|||
allTools = toolInfosToBackend(allToolInfos)
|
||||
}
|
||||
|
||||
var preamble string
|
||||
var agentPrompt string
|
||||
var genParams backend.GenerationParams
|
||||
var maxSteps int
|
||||
if cfg != nil {
|
||||
if resolved, err := resolvePrompt(cfg.Prompt, cwd, env); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "resolve prompt: %v\n", err)
|
||||
} else {
|
||||
preamble = resolved
|
||||
agentPrompt = resolved
|
||||
}
|
||||
genParams = backend.GenerationParams{
|
||||
MaxTokens: cfg.MaxTokens,
|
||||
|
|
@ -137,50 +138,7 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
|||
compactionModel = cfg.CompactionModel
|
||||
}
|
||||
|
||||
// Prepend base layers (system prompt, operational model, environment)
|
||||
// before the agent-specific preamble.
|
||||
if len(baseLayers) > 0 {
|
||||
var prefix strings.Builder
|
||||
for _, layer := range baseLayers {
|
||||
if layer != "" {
|
||||
prefix.WriteString(layer)
|
||||
prefix.WriteByte('\n')
|
||||
}
|
||||
}
|
||||
if prefix.Len() > 0 {
|
||||
preamble = prefix.String() + preamble
|
||||
}
|
||||
}
|
||||
|
||||
// Append compact tool surface listing (name + description) plus
|
||||
// full documentation for loaded tools (schema, args, examples).
|
||||
var toolListing strings.Builder
|
||||
for _, ti := range allToolInfos {
|
||||
if ti.Description != "" && ti.Server == "" {
|
||||
fmt.Fprintf(&toolListing, "- **%s** — %s\n", ti.Name, ti.Description)
|
||||
}
|
||||
}
|
||||
if toolListing.Len() > 0 {
|
||||
preamble += "\n# Available Tools\n\n" + toolListing.String()
|
||||
}
|
||||
// Inject full tool prompts so models that ignore native tool schemas
|
||||
// still see usage docs, arg definitions, and examples in-context.
|
||||
var toolDocs strings.Builder
|
||||
for _, ti := range allToolInfos {
|
||||
if ti.Prompt != "" && ti.Server == "" {
|
||||
toolDocs.WriteString(ti.Prompt)
|
||||
toolDocs.WriteString("\n\n")
|
||||
if len(ti.InputSchema) > 0 && string(ti.InputSchema) != "{}" {
|
||||
fmt.Fprintf(&toolDocs, "**Schema**:\n```json\n%s\n```\n\n", ti.InputSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
if toolDocs.Len() > 0 {
|
||||
preamble += "\n# Tool Documentation\n\n" + toolDocs.String()
|
||||
}
|
||||
|
||||
// Append 9P discovery instructions for loading additional tools.
|
||||
preamble += "\n## Tool discovery\n\nUse the 9P filesystem to discover and load tools dynamically:\n- **List available tools** — `ollie-9p read session/{id}/agent/{aid}/tools`\n- **Load a tool** — `echo \"toolname\" | ollie-9p write session/{id}/agent/{aid}/tools`\n- **See loaded tools** — `ollie-9p read session/{id}/agent/{aid}/tools.loaded`\n"
|
||||
preamble := BuildPreamble(systemPrompt, envBlock, agentPrompt, allToolInfos)
|
||||
|
||||
return &Runtime{
|
||||
ToolServer: srv,
|
||||
|
|
@ -200,6 +158,63 @@ func BuildRuntime(cfg *AgentConfig, srv toolsrv.Runner, cwd string, env []string
|
|||
}
|
||||
}
|
||||
|
||||
// BuildPreamble constructs the complete system prompt from its components.
|
||||
// The preamble evolves when:
|
||||
// - Working directory changes (envBlock updates)
|
||||
// - Tools are loaded/unloaded (allToolInfos changes)
|
||||
// - Agent profile switches (agentPrompt changes)
|
||||
// This function should be called whenever any component changes.
|
||||
func BuildPreamble(systemPrompt, envBlock, agentPrompt string, allToolInfos []toolsrv.ToolInfo) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 1. Core system prompt (identity, rules, operating principles)
|
||||
sb.WriteString(systemPrompt)
|
||||
sb.WriteByte('\n')
|
||||
|
||||
// 2. Environment context (working directory, platform, date, etc.)
|
||||
sb.WriteString(envBlock)
|
||||
sb.WriteByte('\n')
|
||||
|
||||
// 3. Agent-specific prompt (role, task instructions from config)
|
||||
sb.WriteString(agentPrompt)
|
||||
sb.WriteByte('\n')
|
||||
|
||||
// 4. Compact tool surface listing (name + description)
|
||||
var toolListing strings.Builder
|
||||
for _, ti := range allToolInfos {
|
||||
if ti.Description != "" && ti.Server == "" {
|
||||
fmt.Fprintf(&toolListing, "- **%s** — %s\n", ti.Name, ti.Description)
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Full tool documentation (prompts, schemas, examples)
|
||||
var toolDocs strings.Builder
|
||||
for _, ti := range allToolInfos {
|
||||
if ti.Prompt != "" && ti.Server == "" {
|
||||
toolDocs.WriteString(ti.Prompt)
|
||||
toolDocs.WriteString("\n\n")
|
||||
if len(ti.InputSchema) > 0 && string(ti.InputSchema) != "{}" {
|
||||
fmt.Fprintf(&toolDocs, "**Schema**:\n```json\n%s\n```\n\n", ti.InputSchema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Render tool sections
|
||||
if toolListing.Len() > 0 || toolDocs.Len() > 0 {
|
||||
toolSections := prompts.ToolSections(toolListing.String(), toolDocs.String())
|
||||
if toolSections != "" {
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(toolSections)
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Tool discovery instructions (9P filesystem)
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString(prompts.ToolDiscoveryPrompt)
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func toolInfosToBackend(infos []toolsrv.ToolInfo) []backend.Tool {
|
||||
out := make([]backend.Tool, len(infos))
|
||||
for i, t := range infos {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,183 @@
|
|||
package agent
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"ollie/toolsrv"
|
||||
)
|
||||
|
||||
func TestBuildPreamble(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",
|
||||
},
|
||||
{
|
||||
Name: "shell",
|
||||
Description: "Execute shell command",
|
||||
Prompt: "",
|
||||
InputSchema: []byte(`{}`),
|
||||
Server: "",
|
||||
},
|
||||
}
|
||||
|
||||
systemPrompt := "# Core System Prompt\nTest"
|
||||
envBlock := "# Environment\n- Test env"
|
||||
agentPrompt := "# Agent Prompt\nTest instructions"
|
||||
|
||||
preamble := BuildPreamble(systemPrompt, envBlock, agentPrompt, toolInfos)
|
||||
|
||||
// Must contain all sections in order
|
||||
expectedSections := []string{
|
||||
"# Core System Prompt\nTest\n",
|
||||
"# Environment\n- Test env\n",
|
||||
"# Agent Prompt\nTest instructions\n",
|
||||
"# Tool Sections\n",
|
||||
"# Available Tools\n",
|
||||
"- **file_read** — Read a file with line numbers\n",
|
||||
"# Tool Documentation\n",
|
||||
"**file_read**\n\nRead a file with line numbers...\n\n**Schema**:\n```json\n{\"type\":\"object\"}\n```\n\n",
|
||||
"## Tool discovery\n",
|
||||
}
|
||||
|
||||
for _, section := range expectedSections {
|
||||
if !strings.Contains(preamble, section) {
|
||||
t.Errorf("BuildPreamble missing section: %q", section)
|
||||
}
|
||||
}
|
||||
|
||||
// Should NOT contain remote server tools
|
||||
if strings.Contains(preamble, "file_write") {
|
||||
t.Error("BuildPreamble should exclude tools with Server != \"\"")
|
||||
}
|
||||
|
||||
// Should NOT contain tools without prompts in docs section
|
||||
if strings.Contains(preamble, "**shell**\n\n") {
|
||||
t.Error("BuildPreamble should exclude tools without prompts from docs section")
|
||||
}
|
||||
|
||||
// Verify single instances of key headers
|
||||
if strings.Count(preamble, "# Available Tools") != 1 {
|
||||
t.Errorf("BuildPreamble should have exactly one '# Available Tools' section, got %d",
|
||||
strings.Count(preamble, "# Available Tools"))
|
||||
}
|
||||
if strings.Count(preamble, "# Tool Documentation") != 1 {
|
||||
t.Errorf("BuildPreamble should have exactly one '# Tool Documentation' section, got %d",
|
||||
strings.Count(preamble, "# Tool Documentation"))
|
||||
}
|
||||
|
||||
// Verify tool listing includes all non-server tools with descriptions
|
||||
if !strings.Contains(preamble, "**file_read** — Read a file with line numbers") {
|
||||
t.Error("BuildPreamble should include file_read in tool listing")
|
||||
}
|
||||
if !strings.Contains(preamble, "**shell** — Execute shell command") {
|
||||
t.Error("BuildPreamble should include shell in tool listing (has description)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPreambleEmptyTools(t *testing.T) {
|
||||
systemPrompt := "# Core System Prompt\nTest"
|
||||
envBlock := "# Environment\n- Test env"
|
||||
agentPrompt := "# Agent Prompt\nTest instructions"
|
||||
|
||||
preamble := BuildPreamble(systemPrompt, envBlock, agentPrompt, nil)
|
||||
|
||||
// Should not contain tool sections when no tools
|
||||
if strings.Contains(preamble, "# Tool Sections") {
|
||||
t.Error("BuildPreamble should not include tool sections when no tools")
|
||||
}
|
||||
if strings.Contains(preamble, "# Available Tools") {
|
||||
t.Error("BuildPreamble should not include Available Tools when no tools")
|
||||
}
|
||||
if strings.Contains(preamble, "# Tool Documentation") {
|
||||
t.Error("BuildPreamble should not include Tool Documentation when no tools")
|
||||
}
|
||||
|
||||
// Should still contain core sections
|
||||
expectedSections := []string{
|
||||
"# Core System Prompt\nTest\n",
|
||||
"# Environment\n- Test env\n",
|
||||
"# Agent Prompt\nTest instructions\n",
|
||||
"## Tool discovery\n",
|
||||
}
|
||||
|
||||
for _, section := range expectedSections {
|
||||
if !strings.Contains(preamble, section) {
|
||||
t.Errorf("BuildPreamble missing core section: %q", section)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPreambleOnlyListingNoDocs(t *testing.T) {
|
||||
toolInfos := []toolsrv.ToolInfo{
|
||||
{
|
||||
Name: "test_tool",
|
||||
Description: "Test description",
|
||||
Prompt: "", // No prompt means no docs
|
||||
InputSchema: []byte(`{}`),
|
||||
Server: "",
|
||||
},
|
||||
}
|
||||
|
||||
systemPrompt := "# Core System Prompt\nTest"
|
||||
envBlock := "# Environment\n- Test env"
|
||||
agentPrompt := "# Agent Prompt\nTest instructions"
|
||||
|
||||
preamble := BuildPreamble(systemPrompt, envBlock, agentPrompt, toolInfos)
|
||||
|
||||
// Should have tool sections with listing but no docs
|
||||
if !strings.Contains(preamble, "# Tool Sections") {
|
||||
t.Error("BuildPreamble should include # Tool Sections")
|
||||
}
|
||||
if !strings.Contains(preamble, "# Available Tools") {
|
||||
t.Error("BuildPreamble should include # Available Tools")
|
||||
}
|
||||
if !strings.Contains(preamble, "**test_tool** — Test description") {
|
||||
t.Error("BuildPreamble should include tool in listing")
|
||||
}
|
||||
if strings.Contains(preamble, "# Tool Documentation") {
|
||||
t.Error("BuildPreamble should NOT include # Tool Documentation when no tool has prompt")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPreambleOnlyDocsNoListing(t *testing.T) {
|
||||
toolInfos := []toolsrv.ToolInfo{
|
||||
{
|
||||
Name: "test_tool",
|
||||
Description: "", // No description means not in listing
|
||||
Prompt: "**test_tool**\n\nTest docs...",
|
||||
InputSchema: []byte(`{}`),
|
||||
Server: "",
|
||||
},
|
||||
}
|
||||
|
||||
systemPrompt := "# Core System Prompt\nTest"
|
||||
envBlock := "# Environment\n- Test env"
|
||||
agentPrompt := "# Agent Prompt\nTest instructions"
|
||||
|
||||
preamble := BuildPreamble(systemPrompt, envBlock, agentPrompt, toolInfos)
|
||||
|
||||
// Should have tool sections with docs but no listing
|
||||
if !strings.Contains(preamble, "# Tool Sections") {
|
||||
t.Error("BuildPreamble should include # Tool Sections")
|
||||
}
|
||||
if strings.Contains(preamble, "# Available Tools") {
|
||||
t.Error("BuildPreamble should NOT include # Available Tools when no tool has description")
|
||||
}
|
||||
if !strings.Contains(preamble, "# Tool Documentation") {
|
||||
t.Error("BuildPreamble should include # Tool Documentation")
|
||||
}
|
||||
if !strings.Contains(preamble, "**test_tool**") {
|
||||
t.Error("BuildPreamble should include tool in docs")
|
||||
}
|
||||
}
|
||||
|
|
@ -12,7 +12,8 @@ import (
|
|||
//go:embed system_prompt.md
|
||||
var DefaultSystemPrompt string
|
||||
|
||||
|
||||
//go:embed tool_discovery.md
|
||||
var ToolDiscoveryPrompt string
|
||||
|
||||
// Environment renders the environment context block with live values.
|
||||
func Environment(cwd, platform string, isGitRepo bool, sandbox string) string {
|
||||
|
|
@ -49,3 +50,28 @@ func ResolveSystemPrompt(overridePath string) string {
|
|||
fmt.Fprintf(os.Stderr, "system_prompt %q: %v (using default)\n", expanded, err)
|
||||
return DefaultSystemPrompt
|
||||
}
|
||||
|
||||
// ToolSections renders the tool listing and documentation sections with dynamic content.
|
||||
func ToolSections(toolListing, toolDocs string) string {
|
||||
if toolListing == "" && toolDocs == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("# Tool Sections\n\n")
|
||||
|
||||
if toolListing != "" {
|
||||
sb.WriteString("# Available Tools\n\n")
|
||||
sb.WriteString(toolListing)
|
||||
}
|
||||
|
||||
if toolDocs != "" {
|
||||
if toolListing != "" {
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("# Tool Documentation\n\n")
|
||||
sb.WriteString(toolDocs)
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,104 @@
|
|||
package prompts
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestToolSections(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
listing string
|
||||
docs string
|
||||
wantPrefix string
|
||||
wantContains []string
|
||||
wantNotContains []string
|
||||
}{
|
||||
{
|
||||
name: "both empty",
|
||||
listing: "",
|
||||
docs: "",
|
||||
wantPrefix: "",
|
||||
},
|
||||
{
|
||||
name: "only listing",
|
||||
listing: "- **file_read** — Read a file\n",
|
||||
docs: "",
|
||||
wantPrefix: "# Tool Sections\n\n# Available Tools\n\n",
|
||||
wantContains: []string{"# Available Tools", "file_read"},
|
||||
wantNotContains: []string{"# Tool Documentation"},
|
||||
},
|
||||
{
|
||||
name: "only docs",
|
||||
listing: "",
|
||||
docs: "**file_read**\n\nRead a file...\n",
|
||||
wantPrefix: "# Tool Sections\n\n# Tool Documentation\n\n",
|
||||
wantContains: []string{"# Tool Documentation", "file_read"},
|
||||
wantNotContains: []string{"# Available Tools"},
|
||||
},
|
||||
{
|
||||
name: "both listing and docs",
|
||||
listing: "- **file_read** — Read a file\n",
|
||||
docs: "**file_read**\n\nRead a file...\n",
|
||||
wantPrefix: "# Tool Sections\n\n# Available Tools\n\n",
|
||||
wantContains: []string{"# Available Tools", "# Tool Documentation", "file_read"},
|
||||
},
|
||||
{
|
||||
name: "listing with newline before docs",
|
||||
listing: "- **file_read** — Read a file\n",
|
||||
docs: "**file_read**\n\nRead a file...\n",
|
||||
wantContains: []string{"# Available Tools\n\n- **file_read** — Read a file\n\n# Tool Documentation"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ToolSections(tt.listing, tt.docs)
|
||||
|
||||
if tt.wantPrefix != "" && !strings.HasPrefix(got, tt.wantPrefix) {
|
||||
t.Errorf("ToolSections() = %q, want prefix %q", got, tt.wantPrefix)
|
||||
}
|
||||
|
||||
for _, want := range tt.wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("ToolSections() missing %q in output: %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
for _, notWant := range tt.wantNotContains {
|
||||
if strings.Contains(got, notWant) {
|
||||
t.Errorf("ToolSections() unexpectedly contains %q in output: %q", notWant, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Verify no double newlines between sections
|
||||
if strings.Contains(got, "\n\n\n# Tool Documentation") {
|
||||
t.Errorf("ToolSections() has triple newline before Tool Documentation: %q", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnvironment(t *testing.T) {
|
||||
got := Environment("/home/test", "linux", true, "default")
|
||||
|
||||
wantContains := []string{
|
||||
"Working directory: /home/test",
|
||||
"Platform: linux",
|
||||
"Is git repo: true",
|
||||
"Sandbox: default",
|
||||
"elevated: true",
|
||||
}
|
||||
|
||||
for _, want := range wantContains {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Errorf("Environment() missing %q in output: %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
// Test with git false
|
||||
got2 := Environment("/home/test", "linux", false, "")
|
||||
if !strings.Contains(got2, "Is git repo: false") {
|
||||
t.Errorf("Environment() should show git repo false")
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue