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:
+29
-11
@@ -157,7 +157,8 @@ func newToolbox(svc *service.Service, actorID int64, today string) (*llm.Toolbox
|
||||
a.updateJournalEntry),
|
||||
llm.DefineTool("delete_journal_entry",
|
||||
"Delete a journal entry, by its id from read_journal. This is permanent — the journal is "+
|
||||
"not in the undo history — so delete only the entry the user pointed at.",
|
||||
"not in the undo history — so delete only the entry the user pointed at. Get the id "+
|
||||
"from read_journal in this turn: ids from earlier turns are not in front of you.",
|
||||
a.deleteJournalEntry),
|
||||
llm.DefineTool("read_history",
|
||||
"Read the garden's change history: every change anyone made — by hand in the editor, or "+
|
||||
@@ -227,7 +228,8 @@ func newToolbox(svc *service.Service, actorID int64, today string) (*llm.Toolbox
|
||||
llm.DefineTool("delete_seed_lot",
|
||||
"Delete a seed lot the user recorded, by its id from list_seed_lots. Plantings attributed "+
|
||||
"to it stay in the garden, just no longer linked to a purchase. Permanent — seed lots "+
|
||||
"are not in the undo history — so delete only the lot the user pointed at.",
|
||||
"are not in the undo history — so delete only the lot the user pointed at. Get the id "+
|
||||
"from list_seed_lots in this turn: ids from earlier turns are not in front of you.",
|
||||
a.deleteSeedLot),
|
||||
llm.DefineTool("delete_plant",
|
||||
"Delete a plant from the user's own catalog — a duplicate or a mistake. Refused while "+
|
||||
@@ -468,8 +470,9 @@ func (a *adapter) movePlanting(ctx context.Context, args struct {
|
||||
YCM float64 `json:"yCm" description:"new center y in the destination object's local frame (cm)"`
|
||||
ToObjectID *int64 `json:"toObjectId" description:"optional: another plantable object in the same garden to move it into; omit to move within its current object"`
|
||||
}) (any, error) {
|
||||
return a.svc.MovePlanting(ctx, a.actor, args.PlantingID,
|
||||
pl, err := a.svc.MovePlanting(ctx, a.actor, args.PlantingID,
|
||||
service.MoveInput{ToObjectID: args.ToObjectID, XCM: args.XCM, YCM: args.YCM}, args.Version)
|
||||
return pl, whenMissing(err, "planting", args.PlantingID, "list_plantings")
|
||||
}
|
||||
|
||||
func (a *adapter) findPlant(ctx context.Context, args struct {
|
||||
@@ -608,7 +611,19 @@ func (a *adapter) updatePlanting(ctx context.Context, args struct {
|
||||
case args.SeedLotID != nil:
|
||||
patch.SetSeedLotID, patch.SeedLotID = true, args.SeedLotID
|
||||
}
|
||||
return a.svc.UpdatePlanting(ctx, a.actor, args.PlantingID, patch, args.Version)
|
||||
pl, err := a.svc.UpdatePlanting(ctx, a.actor, args.PlantingID, patch, args.Version)
|
||||
return pl, whenMissing(err, "planting", args.PlantingID, "list_plantings")
|
||||
}
|
||||
|
||||
// whenMissing turns the store's bare "not found" into a message that says what
|
||||
// was not found and where the ids come from, so the model re-reads instead of
|
||||
// guessing at another id — or, as the live one did, reporting success over
|
||||
// the error.
|
||||
func whenMissing(err error, what string, id int64, from string) error {
|
||||
if errors.Is(err, domain.ErrNotFound) {
|
||||
return fmt.Errorf("%w: no %s with id %d that you can act on — %s lists the ids; nothing was changed", domain.ErrNotFound, what, id, from)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// parseDay checks a date the model typed, so a malformed one fails with a
|
||||
@@ -660,15 +675,16 @@ func (a *adapter) updateJournalEntry(ctx context.Context, args struct {
|
||||
}
|
||||
observed = &on
|
||||
}
|
||||
return a.svc.UpdateJournalEntry(ctx, a.actor, args.EntryID,
|
||||
e, err := a.svc.UpdateJournalEntry(ctx, a.actor, args.EntryID,
|
||||
service.JournalPatch{Body: args.Body, ObservedAt: observed}, args.Version)
|
||||
return e, whenMissing(err, "journal entry", args.EntryID, "read_journal")
|
||||
}
|
||||
|
||||
func (a *adapter) deleteJournalEntry(ctx context.Context, args struct {
|
||||
EntryID int64 `json:"entryId" description:"journal entry to delete (its id from read_journal)"`
|
||||
}) (any, error) {
|
||||
if err := a.svc.DeleteJournalEntry(ctx, a.actor, args.EntryID); err != nil {
|
||||
return nil, err
|
||||
return nil, whenMissing(err, "journal entry", args.EntryID, "read_journal")
|
||||
}
|
||||
return map[string]any{"deleted": args.EntryID}, nil
|
||||
}
|
||||
@@ -694,7 +710,7 @@ func (a *adapter) undoChange(ctx context.Context, args struct {
|
||||
}
|
||||
cs, conflicts, err := a.svc.RevertChangeSet(ctx, a.actor, args.ChangeSetID, domain.SourceAgent)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, whenMissing(err, "change", args.ChangeSetID, "read_history")
|
||||
}
|
||||
res := undoResult{UndoneID: args.ChangeSetID, Conflicts: conflicts}
|
||||
if cs == nil {
|
||||
@@ -806,7 +822,8 @@ func (a *adapter) removePlanting(ctx context.Context, args struct {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, on)
|
||||
pl, err := a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, on)
|
||||
return pl, whenMissing(err, "planting", args.PlantingID, "list_plantings")
|
||||
}
|
||||
|
||||
func (a *adapter) listSeedLots(ctx context.Context, args struct {
|
||||
@@ -934,14 +951,15 @@ func (a *adapter) updateSeedLot(ctx context.Context, args struct {
|
||||
SetPackedForYear: args.PackedForYear != nil, PackedForYear: args.PackedForYear,
|
||||
SetGerminationPct: args.GerminationPct != nil, GerminationPct: args.GerminationPct,
|
||||
}
|
||||
return a.svc.UpdateSeedLot(ctx, a.actor, args.LotID, patch, args.Version)
|
||||
l, err := a.svc.UpdateSeedLot(ctx, a.actor, args.LotID, patch, args.Version)
|
||||
return l, whenMissing(err, "seed lot", args.LotID, "list_seed_lots")
|
||||
}
|
||||
|
||||
func (a *adapter) deleteSeedLot(ctx context.Context, args struct {
|
||||
LotID int64 `json:"lotId" description:"seed lot to delete (its id from list_seed_lots)"`
|
||||
}) (any, error) {
|
||||
if err := a.svc.DeleteSeedLot(ctx, a.actor, args.LotID); err != nil {
|
||||
return nil, err
|
||||
return nil, whenMissing(err, "seed lot", args.LotID, "list_seed_lots")
|
||||
}
|
||||
return map[string]any{"deleted": args.LotID}, nil
|
||||
}
|
||||
@@ -965,7 +983,7 @@ func (a *adapter) deletePlanting(ctx context.Context, args struct {
|
||||
PlantingID int64 `json:"plantingId" description:"plop to delete outright (its id from describe_garden or list_plantings)"`
|
||||
}) (any, error) {
|
||||
if err := a.svc.DeletePlanting(ctx, a.actor, args.PlantingID); err != nil {
|
||||
return nil, err
|
||||
return nil, whenMissing(err, "planting", args.PlantingID, "list_plantings")
|
||||
}
|
||||
return map[string]any{"deleted": args.PlantingID}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user