Agent: a turn that changed nothing cannot say it did
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user