Skip to content

Commit c80ee32

Browse files
committed
refactor(agent): derive recall guidance from the tool set, drop the flag
The prompt's "Recall rules" block and the recall tools (recent_entries/ get_entry) were two switches that had to be flipped together: the renderer carried RecallEnabled while the tools were wired off the agent's RecentEntries buffer. They could drift — a prompt advertising a recall ability the tool set doesn't back. Make the tool set the single source of truth: PromptRenderer.Render now derives the recall guidance from whether input.Tools advertises recent_entries (which the harness already populates each turn from the runner's tool registry). The RecallEnabled field and buildBookEngine's recall parameter are gone; wiring the recall tools is what turns the guidance on. No behavior change: TUI wires the recall tools (guidance on), book-run/bench don't (off).
1 parent e716af1 commit c80ee32

6 files changed

Lines changed: 33 additions & 20 deletions

File tree

agent/prompt.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,6 @@ type PromptRenderer struct {
2525
Branches []accounting.Branch
2626
OperatorBranchID string
2727
Clock bookkeeping.Clock
28-
// RecallEnabled adds the recall guidance to the prompt; set it when the
29-
// bookkeeper carries a RecentEntries buffer and the recent_entries/get_entry tools.
30-
RecallEnabled bool
3128
}
3229

3330
// NewPromptRenderer snapshots the company, chart, periods, and branches from repo.
@@ -64,7 +61,7 @@ func NewPromptRenderer(ctx context.Context, repo accounting.LedgerRepository) (P
6461

6562
func (r PromptRenderer) Render(input llm.ReasoningInput) ([]llm.Message, error) {
6663
messages := []llm.Message{
67-
{Role: llm.MessageRoleSystem, Content: r.systemPrompt()},
64+
{Role: llm.MessageRoleSystem, Content: r.systemPrompt(hasTool(input.Tools, toolRecentEntries))},
6865
{Role: llm.MessageRoleUser, Content: taskMessage(input)},
6966
}
7067

@@ -256,19 +253,32 @@ func (r PromptRenderer) branchesText() string {
256253
return b.String()
257254
}
258255

256+
// hasTool reports whether specs advertise a tool with the given name. The
257+
// recall guidance is keyed off the recent_entries tool's presence so the prompt
258+
// can never advertise a recall ability the tool set does not back.
259+
func hasTool(specs []llm.ToolSpec, name string) bool {
260+
for _, s := range specs {
261+
if s.Name == name {
262+
return true
263+
}
264+
}
265+
return false
266+
}
267+
259268
// systemPrompt assembles everything that is stable for this renderer's
260269
// lifetime: agent role, intent catalog from the registry, payload format
261-
// rules, behavior rules, and the tenant snapshot. The user message only
262-
// carries the per-call task and any optional Instructions.
263-
func (r PromptRenderer) systemPrompt() string {
270+
// rules, behavior rules, and the tenant snapshot. recall adds the recall
271+
// guidance; it is derived from the available tools, not stored. The user
272+
// message only carries the per-call task and any optional Instructions.
273+
func (r PromptRenderer) systemPrompt(recall bool) string {
264274
var b strings.Builder
265275
b.WriteString(systemPromptHeader)
266276
b.WriteString("\n\nAvailable intents:\n")
267277
b.WriteString(intentsText())
268278
b.WriteString(systemPromptFormatRules)
269279
b.WriteString(systemPromptBehaviorRules)
270280
b.WriteString(systemPromptMultiActionRules)
271-
if r.RecallEnabled {
281+
if recall {
272282
b.WriteString(systemPromptRecallRules)
273283
}
274284
b.WriteString("\n\n")

agent/prompt_test.go

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ func TestPromptRenderer_TeachesFinalAction(t *testing.T) {
118118
}
119119
}
120120

121-
func TestPromptRenderer_RecallRulesGatedByFlag(t *testing.T) {
121+
func TestPromptRenderer_RecallRulesDerivedFromTools(t *testing.T) {
122122
_, repo := awsBillScenario(t)
123123
renderer, err := agent.NewPromptRenderer(context.Background(), repo)
124124
if err != nil {
@@ -127,14 +127,16 @@ func TestPromptRenderer_RecallRulesGatedByFlag(t *testing.T) {
127127

128128
off, _ := renderer.Render(llm.ReasoningInput{Task: "x"})
129129
if strings.Contains(off[0].Content, "Recall rules") {
130-
t.Errorf("recall rules should be absent when RecallEnabled is false")
130+
t.Errorf("recall rules should be absent when the recent_entries tool is not advertised")
131131
}
132132

133-
renderer.RecallEnabled = true
134-
on, _ := renderer.Render(llm.ReasoningInput{Task: "x"})
133+
on, _ := renderer.Render(llm.ReasoningInput{
134+
Task: "x",
135+
Tools: []llm.ToolSpec{{Name: "recent_entries"}},
136+
})
135137
for _, want := range []string{"Recall rules", "recent_entries", "self-contained"} {
136138
if !strings.Contains(on[0].Content, want) {
137-
t.Errorf("recall-enabled prompt missing %q", want)
139+
t.Errorf("recall prompt missing %q when recent_entries tool is present", want)
138140
}
139141
}
140142
}

cmd/ledger/bench.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,7 @@ func benchEngineFactory() benchmark.EngineFactory {
148148
APIKey: m.APIKey,
149149
BaseURL: m.BaseURL,
150150
DisableStrictSchemaWithTools: m.DisableStrictSchemaWithTools,
151-
}, "", false)
151+
}, "")
152152
}
153153
}
154154

cmd/ledger/book_run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,7 @@ func runBook(ctx context.Context, c *cli.Command, stdout io.Writer) error {
103103
}
104104
defer bus.Close()
105105

106-
engine, err := buildBookEngine(ctx, repo, llmCfg, "", false)
106+
engine, err := buildBookEngine(ctx, repo, llmCfg, "")
107107
if err != nil {
108108
return err
109109
}

cmd/ledger/compose.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -119,15 +119,16 @@ func firstOpenPeriod(ctx context.Context, repo accounting.LedgerRepository) (acc
119119
}
120120

121121
// buildBookEngine wires the OpenAI bookkeeper reasoning engine. operatorBranchID
122-
// is injected into the prompt; pass "" to omit the operator-branch hint. recall
123-
// adds the cross-turn recall guidance (pair it with a RecentEntries buffer on the agent).
124-
func buildBookEngine(ctx context.Context, repo accounting.LedgerRepository, llmCfg config.LLM, operatorBranchID string, recall bool) (llm.ReasoningEngine[bookkeeping.Intent], error) {
122+
// is injected into the prompt; pass "" to omit the operator-branch hint. The
123+
// recall guidance is derived from the tool set at render time, so it needs no
124+
// flag here -- wiring the recent_entries tool (via the agent's RecentEntries
125+
// buffer) is what turns it on.
126+
func buildBookEngine(ctx context.Context, repo accounting.LedgerRepository, llmCfg config.LLM, operatorBranchID string) (llm.ReasoningEngine[bookkeeping.Intent], error) {
125127
renderer, err := agent.NewPromptRenderer(ctx, repo)
126128
if err != nil {
127129
return nil, fmt.Errorf("book-run: openai engine: %w", err)
128130
}
129131
renderer.OperatorBranchID = operatorBranchID
130-
renderer.RecallEnabled = recall
131132
adapter, err := openai.NewAdapter(openai.Config[bookkeeping.Intent]{
132133
APIKey: llmCfg.APIKey,
133134
BaseURL: llmCfg.BaseURL,

cmd/ledger/tui_run.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ func (comp tuiComposer) bookOption(repo accounting.LedgerRepository, bus bookkee
111111
Label: branch.Name,
112112
Hint: branch.ID,
113113
Start: func(ctx context.Context) (tui.Session, error) {
114-
engine, err := buildBookEngine(ctx, repo, comp.llmCfg, branch.ID, true)
114+
engine, err := buildBookEngine(ctx, repo, comp.llmCfg, branch.ID)
115115
if err != nil {
116116
return nil, err
117117
}

0 commit comments

Comments
 (0)