Add agent seam: bulk ops + majordomo tool wrappers (#19)
Bulk, natural-language-shaped service operations (ACL-enforced like every other op, so agent tools inherit permissions for free): - ops.go: Region + NamedRegion (nw/ne/sw/se corners, north/south/east/west and top/bottom/left/right halves, "all"; -y is north in the local frame). FillRegion hex-packs plops at 2×radius pitch (radius = #15's max(1.5·spacing, 15cm)) clipped to the region, skipping any candidate that would sit entirely inside an existing active plop. ClearObject soft-removes all active plops in one UPDATE. DescribeGarden returns a structured summary (dims, objects+version, plantings with plant/effective-count/rough compass location). - store/plantings.go: ListActivePlantingsForObject + ClearObjectPlantings. Agent toolbox (internal/agent), deliberately isolated: - doc.go (untagged) keeps the package in the default build; tools.go is behind the `majordomo` build tag and NOT in go.mod, so `go build/test ./...` and the server binary carry no majordomo/LLM deps. Built + tested locally against real majordomo (go test -tags majordomo ./internal/agent/). - NewToolbox(svc, actorID) exposes list_gardens, describe_garden, create_object, move_object, place_planting, fill_region, clear_object as llm.DefineTool wrappers — thin typed adapters over the service, each running as the bound actor. Tests: NamedRegion for all names + unknown→ErrInvalidInput; deterministic hex-packing count (60×60 bed → 4 plops) + re-fill skips covered; rotated bed fills the correct LOCAL corner; ClearObject; viewer→ErrForbidden on fill/clear but can describe; and the DESIGN corner/half scenario (garlic NE, basil NW, beans south) verified via DescribeGarden. A tagged demo drives the same scenario through the toolbox (JSON args → tool → service) and confirms a viewer is refused. GOWORK=off go build/vet/test ./internal/... green (majordomo-free). Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01JdQpdYYsTgtkJBxbcpAszi
This commit is contained in:
@@ -56,6 +56,53 @@ func (d *DB) ListActivePlantingsForGarden(ctx context.Context, gardenID int64) (
|
||||
return plantings, nil
|
||||
}
|
||||
|
||||
// ListActivePlantingsForObject returns an object's currently-planted plops
|
||||
// (removed_at IS NULL). Always a non-nil slice. Used by FillRegion to avoid
|
||||
// stacking new plops inside existing ones.
|
||||
func (d *DB) ListActivePlantingsForObject(ctx context.Context, objectID int64) ([]domain.Planting, error) {
|
||||
rows, err := d.sql.QueryContext(ctx,
|
||||
`SELECT `+plantingColumns+` FROM plantings WHERE object_id = ? AND removed_at IS NULL ORDER BY id`,
|
||||
objectID,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: list object plantings: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
plantings := []domain.Planting{}
|
||||
for rows.Next() {
|
||||
p, err := scanPlanting(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("store: scan planting: %w", err)
|
||||
}
|
||||
plantings = append(plantings, *p)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("store: iterate plantings: %w", err)
|
||||
}
|
||||
return plantings, nil
|
||||
}
|
||||
|
||||
// ClearObjectPlantings soft-removes every active plop in an object in one UPDATE
|
||||
// (sets removed_at=date, bumps version) and returns how many rows it affected.
|
||||
func (d *DB) ClearObjectPlantings(ctx context.Context, objectID int64, date string) (int, error) {
|
||||
res, err := d.sql.ExecContext(ctx,
|
||||
`UPDATE plantings
|
||||
SET removed_at = ?, version = version + 1,
|
||||
updated_at = strftime('%Y-%m-%dT%H:%M:%SZ', 'now')
|
||||
WHERE object_id = ? AND removed_at IS NULL`,
|
||||
date, objectID,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("store: clear object plantings: %w", err)
|
||||
}
|
||||
n, err := res.RowsAffected()
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("store: clear plantings rows: %w", err)
|
||||
}
|
||||
return int(n), nil
|
||||
}
|
||||
|
||||
// GetPlanting returns the planting with the given id, or domain.ErrNotFound.
|
||||
func (d *DB) GetPlanting(ctx context.Context, id int64) (*domain.Planting, error) {
|
||||
p, err := scanPlanting(d.sql.QueryRowContext(ctx,
|
||||
|
||||
Reference in New Issue
Block a user