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:
@@ -120,13 +120,15 @@ func (r *Runner) Run(ctx context.Context, actorID, gardenID int64, message, toda
|
||||
result *agent.Result
|
||||
runErr error
|
||||
truncErr bool
|
||||
tools *adapter
|
||||
)
|
||||
changeSet, err := r.svc.WithChangeSet(ctx, actorID, gardenID, service.ChangeSetOptions{
|
||||
Source: domain.SourceAgent,
|
||||
Summary: turnSummary(message),
|
||||
AgentRunID: &runID,
|
||||
}, func(ctx context.Context) error {
|
||||
box := NewToolbox(r.svc, actorID, today)
|
||||
var box *llm.Toolbox
|
||||
box, tools = newToolbox(r.svc, actorID, today)
|
||||
a := agent.New(r.model, systemPrompt(garden, today),
|
||||
agent.WithMaxSteps(maxSteps),
|
||||
agent.WithToolErrorLimits(maxConsecutiveToolErrors, maxSameCallRepeats),
|
||||
@@ -156,6 +158,12 @@ func (r *Runner) Run(ctx context.Context, actorID, gardenID int64, message, toda
|
||||
turn := &Turn{Truncated: truncErr}
|
||||
if changeSet != nil {
|
||||
turn.ChangeSetID = &changeSet.ID
|
||||
} else if tools != nil {
|
||||
// An undo is its own change set, outside the turn's scope (it has to
|
||||
// point back at what it reverted). A turn that did nothing BUT undo
|
||||
// would otherwise come back with no handle, and the reply would lose
|
||||
// the "Undo this" that every other change gets — here it is a redo.
|
||||
turn.ChangeSetID = tools.lastRevert()
|
||||
}
|
||||
if result != nil {
|
||||
turn.Reply = result.Output
|
||||
@@ -216,7 +224,8 @@ func turnSummary(message string) string {
|
||||
}
|
||||
|
||||
// systemPrompt gives the model the conventions it cannot infer, the day it is,
|
||||
// and the rules of conduct the live instance showed it needs.
|
||||
// the gardener's standing notes, and the rules of conduct the live instance
|
||||
// showed it needs.
|
||||
//
|
||||
// The compass convention in particular is not guessable: -y is north because
|
||||
// screen y grows downward, and a model that assumes otherwise plants the south
|
||||
@@ -226,6 +235,11 @@ func turnSummary(message string) string {
|
||||
// testing: reported a change it never made, narrated every planting into the
|
||||
// journal, swapped four beds on an ambiguous sentence, and answered an imperial
|
||||
// gardener in centimeters.
|
||||
//
|
||||
// The garden's notes are the assistant's memory. They are the owner's own text
|
||||
// (only the owner can edit them), so they are given as background the gardener
|
||||
// wrote — zone, frost dates, soil, how they like things done — and update_garden
|
||||
// is how the assistant adds to them when told something worth keeping.
|
||||
func systemPrompt(g *domain.Garden, today string) string {
|
||||
units := "The gardener works in meters and centimeters; answer in those."
|
||||
size := fmt.Sprintf("%.0f x %.0f cm", g.WidthCM, g.HeightCM)
|
||||
@@ -234,10 +248,19 @@ func systemPrompt(g *domain.Garden, today string) string {
|
||||
"(1 ft = 30.48 cm, 1 in = 2.54 cm) and answer in feet and inches, never in centimeters."
|
||||
size = fmt.Sprintf("%.1f x %.1f ft (%.0f x %.0f cm)", g.WidthCM/30.48, g.HeightCM/30.48, g.WidthCM, g.HeightCM)
|
||||
}
|
||||
notes := "The gardener has written no notes about this garden yet."
|
||||
if n := strings.TrimSpace(g.Notes); n != "" {
|
||||
// %q: the notes are the gardener's own words, but they are data, not
|
||||
// prompt — quoting keeps a line in them from reading as an instruction
|
||||
// to someone the garden is shared with.
|
||||
notes = "The gardener's notes about this garden — their standing facts about the place, to use as " +
|
||||
"context (zone, frost dates, soil, sun, how they like things done): " + fmt.Sprintf("%q", n)
|
||||
}
|
||||
return fmt.Sprintf(`You are pansy's garden assistant. You help plan and edit a real garden by calling tools.
|
||||
|
||||
The garden you are working on is %q (id %d), %s. Today is %s — the gardener's local date.
|
||||
%s
|
||||
%s
|
||||
|
||||
Conventions you cannot guess and must not assume:
|
||||
- Every measurement a tool takes or returns is in CENTIMETERS.
|
||||
@@ -263,15 +286,29 @@ How to work:
|
||||
middle third, a strip along one edge) give fill_region a rectangle instead of placing plops by hand.
|
||||
- A garden named %s is this garden's plan for that year; copy_garden with that name
|
||||
makes one. Never use a different real garden as a scratch space.
|
||||
- Past seasons: describe_garden with a year shows what was in each bed that year, pulled plants
|
||||
included; list_years says which years have records. Check it before advising on rotation or
|
||||
answering "what was here last year?" — do not guess from what is growing now.
|
||||
- To undo something — yours or anyone's — find the change in read_history and call undo_change
|
||||
with its id. It reverts as a new change that can itself be undone. "Undo the beets" means the
|
||||
change that planted the beets, not pulling them out today; do not re-create what you can
|
||||
revert. A change already marked undone stays undone.
|
||||
- To correct a record rather than change the garden — a planting date, a plant count, a journal
|
||||
entry's text or date, the garden's notes — use update_planting, update_journal_entry and
|
||||
update_garden instead of removing and re-adding.
|
||||
- When the gardener tells you something worth keeping about the place — their zone, usual
|
||||
frost dates, soil, a standing preference — add it to the garden's notes with update_garden
|
||||
(keeping what is already there), and say you did. You will see those notes in every later
|
||||
conversation.
|
||||
- When a tool refuses (for example, the user only has view access to this garden), explain what
|
||||
happened in plain words. Do not retry it.
|
||||
|
||||
How to behave:
|
||||
- Only claim what a tool actually did. If a tool failed, or there is no tool for what was asked,
|
||||
say so plainly — never describe a change you did not make.
|
||||
- You cannot undo. Every reply of yours that changed the garden has an "Undo this" button under
|
||||
it, and the History panel can revert any change; point the gardener there, or offer to reverse
|
||||
the change by hand with tools.
|
||||
say so plainly — never describe a change you did not make, and never say something is undone
|
||||
unless undo_change did it.
|
||||
- Every reply of yours that changed the garden has an "Undo this" button under it, and the
|
||||
History panel can revert any change; mention that when it helps.
|
||||
- When a request could mean materially different things — "swap the cucumbers and the melons"
|
||||
with two beds of each — say what you would do and ask, rather than clearing beds on a guess.
|
||||
When it is clear, just do it.
|
||||
@@ -283,5 +320,5 @@ How to behave:
|
||||
// %q throughout for the garden's name: any editor can rename a garden, and
|
||||
// a name is data, not prompt — quoting keeps a newline or a stray quote
|
||||
// in it from reading as a new instruction.
|
||||
g.Name, g.ID, size, today, units, fmt.Sprintf("%q", g.Name+" — <year>"))
|
||||
g.Name, g.ID, size, today, units, notes, fmt.Sprintf("%q", g.Name+" — <year>"))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user