Agent: undo for real, past seasons, and tools that correct the record
Six tools the live assistant kept needing and a prompt that knows about them: - undo_change wraps RevertChangeSet(source=agent). A revert is its own change set, so Run reports the last one as the turn's handle when the turn changed nothing else — an undo-only reply keeps its "Undo this", which is now a redo. - describe_garden takes a year: the season view (GardenFull(year)), pulled plops included, with removed/removedAt per group and per plop; list_years says which years have records. Rotation questions finally have data. - update_planting corrects a plop's date, count, label, radius or seed lot in place; remove_planting, remove_plantings and clear_object take a removedAt so a harvest can be backdated. - update_journal_entry / delete_journal_entry correct a note instead of stacking a contradicting one. - update_garden renames/resizes/re-units a garden and rewrites its notes — and the notes now go into the system prompt as the gardener's standing facts, so "remember we're in zone 6a" persists across conversations. describe_garden also reports the garden's notes, version and grid, which the new tools need. Prompt, CLAUDE.md and DESIGN.md updated to match; UI step labels for the new tools. Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
+60
-24
@@ -550,13 +550,22 @@ func (s *Service) ClearPlantings(ctx context.Context, actorID, objectID int64, o
|
||||
}
|
||||
|
||||
// DescribeResult is a structured summary of a garden for prompting an agent.
|
||||
// Version and Notes are here for update_garden: the version is its guard, and
|
||||
// the notes are the whole text a new note has to be merged into.
|
||||
type DescribeResult struct {
|
||||
GardenID int64 `json:"gardenId"`
|
||||
Name string `json:"name"`
|
||||
WidthCM float64 `json:"widthCm"`
|
||||
HeightCM float64 `json:"heightCm"`
|
||||
UnitPref string `json:"unitPref"`
|
||||
Objects []DescribeObject `json:"objects"`
|
||||
GardenID int64 `json:"gardenId"`
|
||||
Name string `json:"name"`
|
||||
WidthCM float64 `json:"widthCm"`
|
||||
HeightCM float64 `json:"heightCm"`
|
||||
UnitPref string `json:"unitPref"`
|
||||
GridSizeCM float64 `json:"gridSizeCm"`
|
||||
Notes string `json:"notes,omitempty"`
|
||||
Version int64 `json:"version"`
|
||||
// Year is set on a season view: the plantings are then every plop whose time
|
||||
// in the ground overlapped that year, pulled ones included, rather than what
|
||||
// is growing now.
|
||||
Year *int `json:"year,omitempty"`
|
||||
Objects []DescribeObject `json:"objects"`
|
||||
}
|
||||
|
||||
// DescribeObject is one object plus its active plantings grouped by plant, for
|
||||
@@ -605,6 +614,11 @@ type DescribeGroup struct {
|
||||
// DaysToMaturity is the plant's, when the catalog knows it — with PlantedAt,
|
||||
// enough to say when the harvest is due.
|
||||
DaysToMaturity *int `json:"daysToMaturity,omitempty"`
|
||||
// Removed counts the plops in the group that have been pulled, and RemovedAt
|
||||
// is when ("first…last" when they differ). Only a season view lists pulled
|
||||
// plops, so both are absent from a describe of what is growing now.
|
||||
Removed int `json:"removed,omitempty"`
|
||||
RemovedAt string `json:"removedAt,omitempty"`
|
||||
// Each lists the plops individually (id, version, position, location) only
|
||||
// when the group has at most maxListedPlops of them.
|
||||
Each []DescribePlanting `json:"each,omitempty"`
|
||||
@@ -626,14 +640,18 @@ type DescribePlanting struct {
|
||||
Location string `json:"location"`
|
||||
RadiusCM float64 `json:"radiusCm"`
|
||||
PlantedAt string `json:"plantedAt,omitempty"`
|
||||
// RemovedAt is set on a pulled plop, which only a season view lists.
|
||||
RemovedAt string `json:"removedAt,omitempty"`
|
||||
}
|
||||
|
||||
// DescribeGarden returns a structured summary — dimensions, objects, and each
|
||||
// object's active plantings grouped by plant (count, rough location, planting
|
||||
// date) — for a garden the actor can view. Built on GardenFull so it inherits
|
||||
// the ACL check.
|
||||
func (s *Service) DescribeGarden(ctx context.Context, actorID, gardenID int64) (*DescribeResult, error) {
|
||||
full, err := s.GardenFull(ctx, actorID, gardenID, nil)
|
||||
// object's plantings grouped by plant (count, rough location, planting date) —
|
||||
// for a garden the actor can view. year nil describes what is growing now; a
|
||||
// year is the season view, every plop whose time in the ground overlapped it,
|
||||
// pulled ones included — what "what was in this bed last year?" needs. Built
|
||||
// on GardenFull so it inherits the ACL check and the year's bounds.
|
||||
func (s *Service) DescribeGarden(ctx context.Context, actorID, gardenID int64, year *int) (*DescribeResult, error) {
|
||||
full, err := s.GardenFull(ctx, actorID, gardenID, year)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -648,12 +666,16 @@ func (s *Service) DescribeGarden(ctx context.Context, actorID, gardenID int64) (
|
||||
}
|
||||
|
||||
res := &DescribeResult{
|
||||
GardenID: full.Garden.ID,
|
||||
Name: full.Garden.Name,
|
||||
WidthCM: full.Garden.WidthCM,
|
||||
HeightCM: full.Garden.HeightCM,
|
||||
UnitPref: full.Garden.UnitPref,
|
||||
Objects: make([]DescribeObject, 0, len(full.Objects)),
|
||||
GardenID: full.Garden.ID,
|
||||
Name: full.Garden.Name,
|
||||
WidthCM: full.Garden.WidthCM,
|
||||
HeightCM: full.Garden.HeightCM,
|
||||
UnitPref: full.Garden.UnitPref,
|
||||
GridSizeCM: full.Garden.GridSizeCM,
|
||||
Notes: full.Garden.Notes,
|
||||
Version: full.Garden.Version,
|
||||
Year: year,
|
||||
Objects: make([]DescribeObject, 0, len(full.Objects)),
|
||||
}
|
||||
for i := range full.Objects {
|
||||
o := &full.Objects[i]
|
||||
@@ -722,9 +744,13 @@ func describeGroups(o *domain.GardenObject, plops []domain.Planting, plantByID m
|
||||
PlantID: pid, Plant: plant.Name, Plops: len(members),
|
||||
Where: summarizeWhere(o, members), PlantedAt: dateRange(members),
|
||||
DaysToMaturity: plant.DaysToMaturity,
|
||||
RemovedAt: dateRangeOf(members, func(pl domain.Planting) *string { return pl.RemovedAt }),
|
||||
}
|
||||
for _, pl := range members {
|
||||
g.Plants += effectiveCount(pl)
|
||||
if pl.RemovedAt != nil {
|
||||
g.Removed++
|
||||
}
|
||||
}
|
||||
if len(members) <= maxListedPlops {
|
||||
g.Each = make([]DescribePlanting, 0, len(members))
|
||||
@@ -746,6 +772,9 @@ func describePlanting(pl domain.Planting, plantName string) DescribePlanting {
|
||||
if pl.PlantedAt != nil {
|
||||
d.PlantedAt = *pl.PlantedAt
|
||||
}
|
||||
if pl.RemovedAt != nil {
|
||||
d.RemovedAt = *pl.RemovedAt
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
@@ -759,19 +788,26 @@ func effectiveCount(pl domain.Planting) int {
|
||||
}
|
||||
|
||||
// dateRange is the planting date shared by a group's plops, "first…last" when
|
||||
// they were planted on different days, or "" when none is dated. ISO dates
|
||||
// order as strings, so min/max need no parsing.
|
||||
// they were planted on different days, or "" when none is dated.
|
||||
func dateRange(plops []domain.Planting) string {
|
||||
return dateRangeOf(plops, func(pl domain.Planting) *string { return pl.PlantedAt })
|
||||
}
|
||||
|
||||
// dateRangeOf summarizes one date field across a group's plops: the one date
|
||||
// they share, "first…last" when they differ, or "" when none is set. ISO dates
|
||||
// order as strings, so min/max need no parsing.
|
||||
func dateRangeOf(plops []domain.Planting, pick func(domain.Planting) *string) string {
|
||||
first, last := "", ""
|
||||
for _, pl := range plops {
|
||||
if pl.PlantedAt == nil || *pl.PlantedAt == "" {
|
||||
d := pick(pl)
|
||||
if d == nil || *d == "" {
|
||||
continue
|
||||
}
|
||||
if first == "" || *pl.PlantedAt < first {
|
||||
first = *pl.PlantedAt
|
||||
if first == "" || *d < first {
|
||||
first = *d
|
||||
}
|
||||
if *pl.PlantedAt > last {
|
||||
last = *pl.PlantedAt
|
||||
if *d > last {
|
||||
last = *d
|
||||
}
|
||||
}
|
||||
if first == last {
|
||||
|
||||
Reference in New Issue
Block a user