Agent tools: plant lookup, plant creation, journal entries (#55) #69

Merged
steve merged 2 commits from feat/agent-plant-tools into main 2026-07-21 06:05:06 +00:00
Owner

Closes #55. Opens Phase 2 of #58.

"Change the garlic garden bed to instead be cucumbers this year" was nearly executable already: describe_garden resolves "the garlic bed" to an object id because it reports the names of what's planted, clear_object empties it, fill_region replants it.

Step three had no way to get its plantId. The toolbox had no plant tool at all — the agent could see the string "Garlic" come out of describe_garden and had no route from "cucumbers" to an id. One missing tool blocked the entire flagship interaction.

find_plant returns candidates, not a guess

"garlic" against a catalog holding both Garlic and German Red Garlic is genuinely ambiguous, and a caller holding the surrounding conversation is far better placed to disambiguate than a fuzzy-match heuristic here. So it returns several.

Ranked exact → prefix → substring → category, and ordered stably: a tool whose results reshuffle between calls is one a model can't reason about across turns.

Each result also reports how much seed is left of that plant across the actor's lots (from #50), so the agent can say "you only have enough for half that bed" instead of confidently planting seed that doesn't exist. Omitted rather than zeroed when there are no lots, and the unit is dropped when lots disagree — summing seeds and grams would be a lie.

create_plant and add_journal_entry

create_plant lets a variety be named in conversation without leaving the chat. It's user-scoped, not garden-scoped — someone with no editable garden can still name a plant — which the tests assert deliberately rather than assume, per the issue.

add_journal_entry arrived with #52: "note that the west bed has mildew" is squarely the kind of thing you say out loud while walking around.

Tool descriptions are the model's only documentation

Reviewed all seven existing ones. fill_region's region vocabulary now says north is the top of the garden, gives a worked example of the replant sequence (clear_object then fill_region with region=all), and notes that filling twice is safe because it skips covered spots. clear_object says the removal is soft and undoable. find_plant tells the model to prefer it over create_plant — creating a duplicate of something that already exists is worse than reusing it.

Verification

The demo sequence is now a test: describe_gardenfind_plant("cucumber")clear_objectfill_region, ending with a bed of cucumbers and no active garlic. Plus: ambiguous lookup returning both varieties with the exact match first, create_plant staying in the acting user's catalog and out of everyone else's, and the journal tool writing a correctly-dated entry attributed to the right author.

go test -tags majordomo ./internal/agent/ — 5 passing (verified against local majordomo via a temporary replace, then reverted).

go.mod is deliberately untouched. majordomo stays out of the module until #56 drops the build tag, so the default go test ./... and the image build are unaffected — which the issue asks for explicitly.

🤖 Generated with Claude Code

https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ

Closes #55. Opens Phase 2 of #58. *"Change the garlic garden bed to instead be cucumbers this year"* was **nearly** executable already: `describe_garden` resolves "the garlic bed" to an object id because it reports the *names* of what's planted, `clear_object` empties it, `fill_region` replants it. **Step three had no way to get its `plantId`.** The toolbox had no plant tool at all — the agent could see the string "Garlic" come out of `describe_garden` and had no route from "cucumbers" to an id. One missing tool blocked the entire flagship interaction. ### `find_plant` returns candidates, not a guess "garlic" against a catalog holding both **Garlic** and **German Red Garlic** is genuinely ambiguous, and a caller holding the surrounding conversation is far better placed to disambiguate than a fuzzy-match heuristic here. So it returns several. Ranked exact → prefix → substring → category, and ordered **stably**: a tool whose results reshuffle between calls is one a model can't reason about across turns. Each result also reports **how much seed is left** of that plant across the actor's lots (from #50), so the agent can say *"you only have enough for half that bed"* instead of confidently planting seed that doesn't exist. Omitted rather than zeroed when there are no lots, and the unit is dropped when lots disagree — summing seeds and grams would be a lie. ### `create_plant` and `add_journal_entry` `create_plant` lets a variety be named in conversation without leaving the chat. It's **user-scoped, not garden-scoped** — someone with no editable garden can still name a plant — which the tests assert deliberately rather than assume, per the issue. `add_journal_entry` arrived with #52: *"note that the west bed has mildew"* is squarely the kind of thing you say out loud while walking around. ### Tool descriptions are the model's only documentation Reviewed all seven existing ones. `fill_region`'s region vocabulary now says north is the top of the garden, gives a worked example of the replant sequence (`clear_object` then `fill_region` with `region=all`), and notes that filling twice is safe because it skips covered spots. `clear_object` says the removal is soft and undoable. `find_plant` tells the model to prefer it over `create_plant` — creating a duplicate of something that already exists is worse than reusing it. ### Verification The demo sequence is now a test: `describe_garden` → `find_plant("cucumber")` → `clear_object` → `fill_region`, ending with a bed of cucumbers and no active garlic. Plus: ambiguous lookup returning both varieties with the exact match first, `create_plant` staying in the acting user's catalog and out of everyone else's, and the journal tool writing a correctly-dated entry attributed to the right author. `go test -tags majordomo ./internal/agent/` — 5 passing (verified against local majordomo via a temporary replace, then reverted). **`go.mod` is deliberately untouched.** majordomo stays out of the module until #56 drops the build tag, so the default `go test ./...` and the image build are unaffected — which the issue asks for explicitly. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ

🪰 Gadfly — live review status

2/5 reviewers finished · updated 2026-07-21 06:04:47Z

claude-code/sonnet · claude-code — done

  • security — No material issues found
  • correctness — Minor issues
  • maintainability — Minor issues
  • performance — No material issues found
  • error-handling — Minor issues

glm-5.2:cloud · ollama-cloud — 3/5 lenses

  • security — No material issues found
  • 🔄 correctness — running
  • maintainability — Minor issues
  • performance — Minor issues
  • 🔄 error-handling — running

kimi-k2.6:cloud · ollama-cloud — done

  • security — No material issues found
  • correctness — No material issues found
  • maintainability — No material issues found
  • performance — No material issues found
  • error-handling — Minor issues

opencode/glm-5.2:cloud · opencode — 3/5 lenses

  • security — No material issues found
  • 🔄 correctness — running
  • 🔄 maintainability — running
  • performance — No material issues found
  • error-handling — Minor issues

opencode/kimi-k2.6:cloud · opencode — 2/5 lenses

  • security — No material issues found
  • 🔄 correctness — running
  • 🔄 maintainability — running
  • performance — Minor issues
  • 🔄 error-handling — running

Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.

<!-- gadfly-status-board --> ## 🪰 Gadfly — live review status 2/5 reviewers finished · updated 2026-07-21 06:04:47Z #### `claude-code/sonnet` · claude-code — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — Minor issues - ✅ **maintainability** — Minor issues - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues #### `glm-5.2:cloud` · ollama-cloud — ⏳ 3/5 lenses - ✅ **security** — No material issues found - 🔄 **correctness** — running - ✅ **maintainability** — Minor issues - ✅ **performance** — Minor issues - 🔄 **error-handling** — running #### `kimi-k2.6:cloud` · ollama-cloud — ✅ done - ✅ **security** — No material issues found - ✅ **correctness** — No material issues found - ✅ **maintainability** — No material issues found - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues #### `opencode/glm-5.2:cloud` · opencode — ⏳ 3/5 lenses - ✅ **security** — No material issues found - 🔄 **correctness** — running - 🔄 **maintainability** — running - ✅ **performance** — No material issues found - ✅ **error-handling** — Minor issues #### `opencode/kimi-k2.6:cloud` · opencode — ⏳ 2/5 lenses - ✅ **security** — No material issues found - 🔄 **correctness** — running - 🔄 **maintainability** — running - ✅ **performance** — Minor issues - 🔄 **error-handling** — running <sub>Live status board. Findings are posted in each model's own comment. Advisory only — does not block merge.</sub>
steve added 2 commits 2026-07-21 06:04:44 +00:00
"Change the garlic garden bed to instead be cucumbers this year" was nearly
executable already — describe_garden resolves "the garlic bed" to an object id
because it reports the NAMES of what's planted, clear_object empties it, and
fill_region replants it. Step three had no way to get its plantId. The toolbox
had no plant tool at all, so the agent could see the string "Garlic" come out of
describe_garden and had no route from "cucumbers" to an id. One missing tool
blocked the whole flagship interaction.

find_plant matches the actor's visible catalog and deliberately returns SEVERAL
candidates rather than one guess. "garlic" against a catalog holding both
"Garlic" and "German Red Garlic" is genuinely ambiguous, and a caller holding
the surrounding conversation is far better placed to disambiguate than a
fuzzy-match heuristic here. Results are ranked exact, then prefix, then
substring, then category, and ordered stably — a tool whose results reshuffle
between calls is one a model can't reason about across turns.

Each result also reports how much seed is left of that plant across the actor's
lots, so the agent can say "you only have enough for half that bed" instead of
confidently planting seed that doesn't exist. Omitted rather than zeroed when
there are no lots, and dropped when lots disagree about the unit, since summing
seeds and grams would be a lie.

create_plant lets a variety be named in conversation without leaving the chat.
It's user-scoped, not garden-scoped — someone with no editable garden can still
name a plant — which the tests assert deliberately rather than assume.

add_journal_entry arrived with #52: "note that the west bed has mildew" is
squarely the kind of thing you say out loud while walking around.

Also reviewed the descriptions on the existing seven tools, since they are the
model's only documentation. fill_region's region vocabulary now says north is
the top of the garden, gives a worked example of the replant sequence, and notes
that filling twice is safe.

The demo sequence is now a test: describe_garden → find_plant("cucumber") →
clear_object → fill_region, ending with a bed of cucumbers and no active garlic.

go.mod is deliberately untouched — majordomo stays out of the module until #56
drops the build tag, so the default build is unaffected.

Closes #55

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
Address Gadfly review on the agent plant tools
Build image / build-and-push (push) Successful in 7s
aec3bb52d1
attachSeedRemaining summed Remaining across lots in different units and then
dropped only the unit LABEL, leaving a bare number that reads as a quantity and
isn't one — 50 seeds plus 50 grams reported as "100". Worse for a model than for
a person, since it has nothing else to go on. The total is now omitted with the
label, and SeedLots reports how many lots there are, so "several lots, no single
total" stays distinguishable from "no seed at all".

Also folded TestToolboxScenario's inline setup into the newAgentTestService
helper the new tests use, rather than leaving two copies of the same six lines.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01H3zbym8Doka2d7D48maSgZ
steve force-pushed feat/agent-plant-tools from 7c042ebfc9 to aec3bb52d1 2026-07-21 06:04:44 +00:00 Compare
Author
Owner

Fixed in 8b1a2c9. The unit finding is right and I'd half-committed the same mistake in two places.

attachSeedRemaining summed Remaining across lots in different units and then dropped only the unit label — leaving a bare number that reads as a quantity and isn't one. 50 seeds plus 50 grams reported as 100. That's worse for a model than for a person: a person sees a suspiciously round number with no unit and gets suspicious; a model has nothing else to go on and will happily tell you that you have 100 of something.

The total is now omitted with the label, and seedLots reports how many lots there are — so "several lots, no single total" stays distinguishable from "no seed at all", which was the reason I'd been reluctant to drop the number. Test covers it.

(The frontend's summarizeLots had the same shape of decision and got it right — it drops the unit and lets the per-lot rows speak — so this was inconsistency as much as a bug.)

Also folded TestToolboxScenario's inline setup into the newAgentTestService helper the new tests use, rather than leaving two copies of the same six lines.

go test ./... green, go test -tags majordomo ./internal/agent/ — 5 passing. go.mod still untouched.

Fixed in `8b1a2c9`. The unit finding is right and I'd half-committed the same mistake in two places. `attachSeedRemaining` summed `Remaining` across lots in **different units** and then dropped only the unit *label* — leaving a bare number that reads as a quantity and isn't one. 50 seeds plus 50 grams reported as `100`. That's worse for a model than for a person: a person sees a suspiciously round number with no unit and gets suspicious; a model has nothing else to go on and will happily tell you that you have 100 of something. The total is now omitted with the label, and **`seedLots` reports how many lots there are** — so "several lots, no single total" stays distinguishable from "no seed at all", which was the reason I'd been reluctant to drop the number. Test covers it. (The frontend's `summarizeLots` had the same shape of decision and got it right — it drops the unit and lets the per-lot rows speak — so this was inconsistency as much as a bug.) Also folded `TestToolboxScenario`'s inline setup into the `newAgentTestService` helper the new tests use, rather than leaving two copies of the same six lines. `go test ./...` green, `go test -tags majordomo ./internal/agent/` — 5 passing. `go.mod` still untouched.
steve merged commit a1bb8ec463 into main 2026-07-21 06:05:06 +00:00
steve deleted branch feat/agent-plant-tools 2026-07-21 06:05:06 +00:00
Sign in to join this conversation.