Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 18 additions & 8 deletions agent/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,6 @@ type PromptRenderer struct {
Branches []accounting.Branch
OperatorBranchID string
Clock bookkeeping.Clock
// RecallEnabled adds the recall guidance to the prompt; set it when the
// bookkeeper carries a RecentEntries buffer and the recent_entries/get_entry tools.
RecallEnabled bool
}

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

func (r PromptRenderer) Render(input llm.ReasoningInput) ([]llm.Message, error) {
messages := []llm.Message{
{Role: llm.MessageRoleSystem, Content: r.systemPrompt()},
{Role: llm.MessageRoleSystem, Content: r.systemPrompt(hasTool(input.Tools, toolRecentEntries))},
{Role: llm.MessageRoleUser, Content: taskMessage(input)},
}

Expand Down Expand Up @@ -256,19 +253,32 @@ func (r PromptRenderer) branchesText() string {
return b.String()
}

// hasTool reports whether specs advertise a tool with the given name. The
// recall guidance is keyed off the recent_entries tool's presence so the prompt
// can never advertise a recall ability the tool set does not back.
func hasTool(specs []llm.ToolSpec, name string) bool {
for _, s := range specs {
if s.Name == name {
return true
}
}
return false
}

// systemPrompt assembles everything that is stable for this renderer's
// lifetime: agent role, intent catalog from the registry, payload format
// rules, behavior rules, and the tenant snapshot. The user message only
// carries the per-call task and any optional Instructions.
func (r PromptRenderer) systemPrompt() string {
// rules, behavior rules, and the tenant snapshot. recall adds the recall
// guidance; it is derived from the available tools, not stored. The user
// message only carries the per-call task and any optional Instructions.
func (r PromptRenderer) systemPrompt(recall bool) string {
var b strings.Builder
b.WriteString(systemPromptHeader)
b.WriteString("\n\nAvailable intents:\n")
b.WriteString(intentsText())
b.WriteString(systemPromptFormatRules)
b.WriteString(systemPromptBehaviorRules)
b.WriteString(systemPromptMultiActionRules)
if r.RecallEnabled {
if recall {
b.WriteString(systemPromptRecallRules)
}
b.WriteString("\n\n")
Expand Down
12 changes: 7 additions & 5 deletions agent/prompt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ func TestPromptRenderer_TeachesFinalAction(t *testing.T) {
}
}

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

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

renderer.RecallEnabled = true
on, _ := renderer.Render(llm.ReasoningInput{Task: "x"})
on, _ := renderer.Render(llm.ReasoningInput{
Task: "x",
Tools: []llm.ToolSpec{{Name: "recent_entries"}},
})
for _, want := range []string{"Recall rules", "recent_entries", "self-contained"} {
if !strings.Contains(on[0].Content, want) {
t.Errorf("recall-enabled prompt missing %q", want)
t.Errorf("recall prompt missing %q when recent_entries tool is present", want)
}
}
}
Expand Down
2 changes: 1 addition & 1 deletion cmd/ledger/bench.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ func benchEngineFactory() benchmark.EngineFactory {
APIKey: m.APIKey,
BaseURL: m.BaseURL,
DisableStrictSchemaWithTools: m.DisableStrictSchemaWithTools,
}, "", false)
}, "")
}
}

Expand Down
2 changes: 1 addition & 1 deletion cmd/ledger/book_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ func runBook(ctx context.Context, c *cli.Command, stdout io.Writer) error {
}
defer bus.Close()

engine, err := buildBookEngine(ctx, repo, llmCfg, "", false)
engine, err := buildBookEngine(ctx, repo, llmCfg, "")
if err != nil {
return err
}
Expand Down
9 changes: 5 additions & 4 deletions cmd/ledger/compose.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,15 +120,16 @@ func firstOpenPeriod(ctx context.Context, repo accounting.LedgerRepository) (acc
}

// buildBookEngine wires the OpenAI bookkeeper reasoning engine. operatorBranchID
// is injected into the prompt; pass "" to omit the operator-branch hint. recall
// adds the cross-turn recall guidance (pair it with a RecentEntries buffer on the agent).
func buildBookEngine(ctx context.Context, repo accounting.LedgerRepository, llmCfg config.LLM, operatorBranchID string, recall bool) (llm.ReasoningEngine[bookkeeping.Intent], error) {
// is injected into the prompt; pass "" to omit the operator-branch hint. The
// recall guidance is derived from the tool set at render time, so it needs no
// flag here -- wiring the recent_entries tool (via the agent's RecentEntries
// buffer) is what turns it on.
func buildBookEngine(ctx context.Context, repo accounting.LedgerRepository, llmCfg config.LLM, operatorBranchID string) (llm.ReasoningEngine[bookkeeping.Intent], error) {
renderer, err := agent.NewPromptRenderer(ctx, repo)
if err != nil {
return nil, fmt.Errorf("book-run: openai engine: %w", err)
}
renderer.OperatorBranchID = operatorBranchID
renderer.RecallEnabled = recall
adapter, err := openai.NewAdapter(openai.Config[bookkeeping.Intent]{
APIKey: llmCfg.APIKey,
BaseURL: llmCfg.BaseURL,
Expand Down
2 changes: 1 addition & 1 deletion cmd/ledger/tui_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ func (comp tuiComposer) bookOption(repo accounting.LedgerRepository, bus bookkee
Label: branch.Name,
Hint: branch.ID,
Start: func(ctx context.Context) (tui.Session, error) {
engine, err := buildBookEngine(ctx, repo, comp.llmCfg, branch.ID, true)
engine, err := buildBookEngine(ctx, repo, comp.llmCfg, branch.ID)
if err != nil {
return nil, err
}
Expand Down