From 0f835a0802493220a6091c1b772a7ce1aced3521 Mon Sep 17 00:00:00 2001 From: Victor Vargas Date: Wed, 8 Jul 2026 23:27:17 -0700 Subject: [PATCH] feat(agent): wire AGENTS.md discovery into the agent loop's system prompt Export persona.DiscoverAgentsMD and add agent.Config.AgentsMD so project and global AGENTS.md rules actually reach the model. Previously buildInitialMessages always called AssembleSystemPrompt with an empty string, so no AGENTS.md content was ever injected despite the discovery logic already existing. Co-Authored-By: Claude Sonnet 5 --- pkg/agent/loop.go | 3 ++- pkg/agent/loop_test.go | 25 +++++++++++++++++++++++++ pkg/persona/persona.go | 4 ++-- pkg/persona/persona_test.go | 4 ++-- 4 files changed, 31 insertions(+), 5 deletions(-) diff --git a/pkg/agent/loop.go b/pkg/agent/loop.go index 0d98e22..1c0916a 100644 --- a/pkg/agent/loop.go +++ b/pkg/agent/loop.go @@ -42,6 +42,7 @@ type Config struct { OnIteration OnIterationHook ToolTimeout time.Duration ChatTemplateKwargs map[string]any // passed to the LLM provider (e.g. Qwen enable_thinking) + AgentsMD string // discovered AGENTS.md content, folded into the system prompt } // Iteration represents a single cycle of the agent loop. @@ -230,7 +231,7 @@ func (l *Loop) RunStream(ctx context.Context, input string, history ...llm.Messa } func (l *Loop) buildInitialMessages(input string, history []llm.Message) []llm.Message { - systemPrompt := persona.AssembleSystemPrompt(l.cfg.Persona, "") + systemPrompt := persona.AssembleSystemPrompt(l.cfg.Persona, l.cfg.AgentsMD) messages := make([]llm.Message, 0, len(history)+2) messages = append(messages, llm.Message{Role: llm.RoleSystem, Content: systemPrompt}) messages = append(messages, history...) diff --git a/pkg/agent/loop_test.go b/pkg/agent/loop_test.go index e81f128..4c59bcf 100644 --- a/pkg/agent/loop_test.go +++ b/pkg/agent/loop_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "iter" + "strings" "testing" "time" @@ -116,6 +117,30 @@ func TestRun_NoToolCalls(t *testing.T) { } } +func TestRun_IncludesAgentsMD(t *testing.T) { + var capturedSystemPrompt string + mockClient := &mockLLM{ + generateFunc: func(ctx context.Context, req llm.CompletionRequest) (llm.CompletionResponse, error) { + capturedSystemPrompt = req.Messages[0].Content + return llm.CompletionResponse{Content: "done"}, nil + }, + } + + loop := New(Config{ + LLM: mockClient, + Persona: persona.DefaultPersona(), + Tools: tools.NewRegistry(), + AgentsMD: "Never edit go.mod directly.", + }) + + if _, err := loop.Run(context.Background(), "Hello"); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(capturedSystemPrompt, "Never edit go.mod directly.") { + t.Errorf("expected system prompt to include AGENTS.md content, got %q", capturedSystemPrompt) + } +} + func TestRun_ToolCalls(t *testing.T) { registry := tools.NewRegistry() registry.Register(tools.Tool{ diff --git a/pkg/persona/persona.go b/pkg/persona/persona.go index 0d83982..c092b23 100644 --- a/pkg/persona/persona.go +++ b/pkg/persona/persona.go @@ -79,8 +79,8 @@ func AssembleSystemPrompt(p Persona, agentsMD string) string { return strings.Join(parts, "\n\n") } -// discoverAgentsMD walks up the directory tree looking for AGENTS.md files. -func discoverAgentsMD(root string) string { +// DiscoverAgentsMD walks up the directory tree looking for AGENTS.md files. +func DiscoverAgentsMD(root string) string { var parts []string current := root diff --git a/pkg/persona/persona_test.go b/pkg/persona/persona_test.go index 86a3ffc..ed60fea 100644 --- a/pkg/persona/persona_test.go +++ b/pkg/persona/persona_test.go @@ -45,9 +45,9 @@ func TestDiscoverAgentsMD(t *testing.T) { agentsPath := filepath.Join(tmpDir, "AGENTS.md") os.WriteFile(agentsPath, []byte("test instructions"), 0644) - result := discoverAgentsMD(tmpDir) + result := DiscoverAgentsMD(tmpDir) if !contains(result, "test instructions") { - t.Error("expected discoverAgentsMD to find AGENTS.md") + t.Error("expected DiscoverAgentsMD to find AGENTS.md") } }