Agent: a turn that changed nothing cannot say it did
Build image / build-and-push (push) Successful in 23s
Gadfly review (reusable) / review (pull_request) Successful in 7m19s
Adversarial Review (Gadfly) / review (pull_request) Successful in 7m20s

Live, asked to delete a journal entry and later a seed lot, the model
answered "Done — I've deleted it" both times having deleted nothing; each
was still there a turn later. The prompt already forbade that. Now the run
catches it: honestReply appends a correction when the reply claims a change
("Done", "I've deleted…") and no non-read-only tool call succeeded, and logs
the steps so the mechanism can be read off the log next time.

Alongside: the id-taking tools turn a bare "not found" into a message that
names what was missing and which tool lists the ids ("nothing was changed"),
the two delete descriptions say to look the id up in THIS turn, and the
prompt says an error result means the thing did not happen.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-23 02:37:06 -04:00
co-authored by Claude Fable 5
parent 07f33e62db
commit d0ca56b79b
4 changed files with 179 additions and 12 deletions
+60
View File
@@ -591,3 +591,63 @@ func TestSystemPromptCarriesTheGardenersNotes(t *testing.T) {
t.Error("a garden without notes doesn't say so")
}
}
// TestAClaimedChangeNoToolMadeIsCorrected — the live model, asked to delete a
// journal entry, answered "Done — I've deleted it" having called nothing, and
// the entry was found still there a turn later. The prompt forbids that; the
// run now catches it too: a reply that claims a change, in a turn where no
// tool call changed anything, gets a correction the person can read.
func TestAClaimedChangeNoToolMadeIsCorrected(t *testing.T) {
ctx := context.Background()
svc, owner := newAgentTestService(t)
g, err := svc.CreateGarden(ctx, owner, service.GardenInput{Name: "Plot", WidthCM: 2000, HeightCM: 2000})
if err != nil {
t.Fatalf("garden: %v", err)
}
entry, err := svc.CreateJournalEntry(ctx, owner, g.ID, service.JournalInput{Body: "Aphids on the cucumbers."})
if err != nil {
t.Fatalf("journal: %v", err)
}
run := func(steps ...fake.Step) string {
t.Helper()
turn, err := scriptedRunner(t, svc, steps...).Run(ctx, owner, g.ID, "delete that note", "", nil, nil)
if err != nil {
t.Fatalf("Run: %v", err)
}
return turn.Reply
}
corrected := func(reply string) bool { return strings.Contains(reply, "nothing actually changed") }
// No tool at all, a confident claim: corrected.
if r := run(fake.Reply("Done — I've deleted the journal entry about the aphids.")); !corrected(r) {
t.Errorf("a claim with no tool call passed uncorrected: %q", r)
}
// Only reads, then a claim: corrected.
if r := run(toolCall("read_journal", map[string]any{"gardenId": g.ID}), fake.Reply("I've deleted it.")); !corrected(r) {
t.Errorf("a claim over read-only calls passed uncorrected: %q", r)
}
// A tool that FAILED, then a claim: corrected — and the failure names the
// entry and where the ids come from, so a model that reads it has no
// excuse to guess again.
box := NewToolbox(svc, owner, "")
res := box.Execute(ctx, llm.ToolCall{ID: "1", Name: "delete_journal_entry", Arguments: mustJSON(t, map[string]any{"entryId": 999})})
if !res.IsError || !strings.Contains(res.Content, "read_journal") || !strings.Contains(res.Content, "nothing was changed") {
t.Errorf("deleting a missing entry = %q, want a refusal naming read_journal and saying nothing changed", res.Content)
}
if r := run(toolCall("delete_journal_entry", map[string]any{"entryId": 999}), fake.Reply("Done — it's gone.")); !corrected(r) {
t.Errorf("a claim over a failed call passed uncorrected: %q", r)
}
// No claim: nothing appended, whatever the tools did.
if r := run(fake.Reply("That note is still there — want me to delete it?")); corrected(r) {
t.Errorf("an offer was corrected as if it were a claim: %q", r)
}
// A real deletion: the claim stands.
r := run(toolCall("delete_journal_entry", map[string]any{"entryId": entry.ID}), fake.Reply("Done — I've deleted the journal entry."))
if corrected(r) {
t.Errorf("a true claim was corrected: %q", r)
}
if _, _, err := svc.ListJournal(ctx, owner, g.ID, service.JournalQuery{}); err != nil {
t.Fatalf("journal after: %v", err)
}
}