Address #127 review: quote the garden name, validate rectangles, require plantId
Build image / build-and-push (push) Successful in 17s

- The plan-name line of the system prompt interpolates the garden's name
  with %q like the rest of the prompt: any editor can rename a garden, and a
  name with a newline in it must not read as an instruction.
- fill_region refuses an inverted rectangle with its corners named, and a
  rectangle that misses the bed (or only touches its edge) is an error from
  the service rather than a successful fill of nothing.
- remove_plantings requires plantId; omitted it would remove plant 0 and
  report success.
- historyEntry.Undo → UndoOf (it holds the reverted change set's id).
- remove_planting's description names list_plantings as an id source.
- RemovePlanting takes the removal date itself; the dateless wrapper had no
  callers left.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
2026-08-23 00:27:48 -04:00
co-authored by Claude Fable 5
parent a1baf4b871
commit d3d7238259
6 changed files with 53 additions and 21 deletions
+13 -6
View File
@@ -72,8 +72,8 @@ func NewToolbox(svc *service.Service, actorID int64, today string) *llm.Toolbox
llm.DefineTool("remove_planting",
"Remove ONE plop from a bed, leaving the rest — the single-plant answer to clear_object's "+
"all-or-nothing. Soft-removes it (kept for planting history, undoable), like clearing a "+
"bed does. Needs the plop's id and version from describe_garden. Use for \"pull the "+
"basil out of the corner\".",
"bed does. Needs the plop's id and version (describe_garden or list_plantings). Use "+
"for \"pull the basil out of the corner\".",
a.removePlanting),
llm.DefineTool("remove_plantings",
"Remove every plop of ONE plant from an object, leaving the other plants in it — \"take the "+
@@ -261,6 +261,9 @@ func (a *adapter) fillRegion(ctx context.Context, args struct {
}
switch {
case given == 4 && strings.TrimSpace(args.Region) == "":
if !(*args.X0CM < *args.X1CM && *args.Y0CM < *args.Y1CM) {
return nil, fmt.Errorf("%w: x0Cm must be west of x1Cm and y0Cm north of y1Cm (-y is north)", domain.ErrInvalidInput)
}
spec.Region = service.Region{MinX: *args.X0CM, MinY: *args.Y0CM, MaxX: *args.X1CM, MaxY: *args.Y1CM}
case given == 0 && strings.TrimSpace(args.Region) != "":
// the named region
@@ -352,6 +355,10 @@ func (a *adapter) removePlantings(ctx context.Context, args struct {
ObjectID int64 `json:"objectId" description:"object to remove the plant from"`
PlantID int64 `json:"plantId" description:"the plant to remove every plop of (from describe_garden)"`
}) (any, error) {
if args.PlantID == 0 {
// Left out, it would "remove" plant 0 — nothing — and report success.
return nil, fmt.Errorf("%w: plantId is required — say which plant to remove, or use clear_object for all of them", domain.ErrInvalidInput)
}
n, err := a.svc.ClearPlantings(ctx, a.actor, args.ObjectID,
service.ClearOptions{PlantID: &args.PlantID, RemovedAt: a.day("")})
if err != nil {
@@ -392,8 +399,8 @@ type historyEntry struct {
Summary string `json:"summary"`
Changes string `json:"changes"`
Undone bool `json:"undone,omitempty"`
// Undo is set when this entry is itself an undo of an earlier one.
Undo *int64 `json:"undoOf,omitempty"`
// UndoOf is the earlier entry this one reverted, when it is itself an undo.
UndoOf *int64 `json:"undoOf,omitempty"`
}
func (a *adapter) readHistory(ctx context.Context, args struct {
@@ -414,7 +421,7 @@ func (a *adapter) readHistory(ctx context.Context, args struct {
entries = append(entries, historyEntry{
ID: cs.ID, When: cs.CreatedAt, Source: cs.Source, Who: cs.ActorName,
Summary: cs.Summary, Changes: describeCounts(cs.Counts),
Undone: cs.RevertedByID != nil, Undo: cs.RevertsID,
Undone: cs.RevertedByID != nil, UndoOf: cs.RevertsID,
})
}
return map[string]any{"entries": entries, "hasMore": hasMore}, nil
@@ -467,7 +474,7 @@ func (a *adapter) removePlanting(ctx context.Context, args struct {
}) (any, error) {
// Soft-remove via the service, dated the gardener's local day like every
// other tool here (the service clock's UTC day when that isn't known).
return a.svc.RemovePlantingOn(ctx, a.actor, args.PlantingID, args.Version, a.day(""))
return a.svc.RemovePlanting(ctx, a.actor, args.PlantingID, args.Version, a.day(""))
}
func (a *adapter) listSeedLots(ctx context.Context, args struct {