Skip to content

feat(mcp): place_design creates a validated design on today's hosts (AK-02 2/3) - #953

Merged
Aymericr merged 10 commits into
mainfrom
fidelity/ak-02b-place-design
Sep 28, 2026
Merged

Aymericr merged 10 commits into
mainfrom
fidelity/ak-02b-place-design

Conversation

@Aymericr

@Aymericr Aymericr commented Sep 27, 2026 •

Copy link
Copy Markdown
Contributor

Merge as one unit (coordinator ruling on the Codex R8 finding): #948, #953 and the private AK-02 3/3 PR (chat tools and scene programs) merge the same day, so there is no window where MCP has design tools that chat and programs lack. The owner's AD-6 exception covers today's surfaces.

Stacked on #948 (AK-02 1/3). Until #948 merges, this diff also shows its commit; the change here is commits dde17bf, 2036305 and 30de136 (rebased onto editor main with #938 merged, 5e7e128), plus the review fixes.

What

Agents can now create a design they validated on today's hosts, with typed refusals.

  • Core: planDesignPlacement(nodes, request).
    1. It validates the design with validateDesign, the same authority as validate_design. The design can be an object or a JSON string.
    2. It picks the host from the design's mounting, using today's hosting fields only:
      • floor designs go on a level, or on a slab or zone of it (the level becomes the parent). They can also go on a named surface of a placed design (attachments[childId] = surfaceId);
      • wall-side designs go on a straight wall face (wallId/side, with position = along, height and offset of the reference);
      • ceiling designs go under a ceiling (plan x/z).
    3. It builds the node and runs validateProceduralRelations directly. The mount checks therefore hold without the node registry: before this, the trial saw ceiling and wall designs accepted as level children when builtinPlugin was not loaded.
    4. It returns { node, parentId, hostUpdate? } or throws DesignPlacementError. The codes are invalid_design (with diagnostics), invalid_placement, host_not_found, wrong_host, unknown_surface and does_not_fit, plus the apply_patch guard codes. A wall misfit says where the reference fits, for example "fits along 0.317–3.683 m and at height 0.47–2.23 m on this wall".
  • MCP: place_design wraps it and only creates.
    • The node, plus the host's attachment update for a surface, goes through applyPatch.
    • A refusal is a tool error whose JSON text is { code, message, diagnostics? }; the message starts with the code.
    • It adds one line to the agent guide and one README row. The annotation inventory goes from 50 to 51 tools (additive).

Codex review fixes (failing cases in the commit before 96b80a3): SceneBridge.applyPatch commits a whole patch as one history step, so a design-surface placement undoes in one step; designs above 24 KiB are refused with design_too_large (R7, as #947); an explicit existing id is refused with node_exists before the surface host's shadow graph is built. The known limit about two undo steps no longer applies.

Why

Owner decision AD-6: agent design tools ship now on today's surfaces. This is an explicit, owner-approved exception to charter R8, which otherwise waits for the P-04 tool kernel. It is kernel-shaped: one core function, and the MCP adapter adds only transport. Following the CHALLENGE correction, it uses v1 inline recipes and today's hosting fields; pin and Mount arguments wait for AK-01/P-05 and the Mount decision.

Create-only goes through #938's patch guards. place_design runs assertPatchKeepsIdentity over its create (and a surface host's attachment update) before applyPatch, so an explicit existing id is refused with apply_patch's own node_exists code and message; planDesignPlacement no longer carries its own check (30de136). Refusals are tool errors with JSON { code, message, diagnostics? }, like apply_patch's, because an McpError keeps only its message.

Failing case before this PR: no tool placed a design.

  • tools/call place_design returns "tool not found".
  • Through apply_patch, an agent had to hand-write the node, and without builtinPlugin a wall-side or ceiling design was accepted as a level child (trial.md, "mount relations are enforced only when the process has loaded builtinPlugin").

Proof

Command Result
bun test src/procedural-items/design-placement.test.ts (core) 18 pass, 0 fail
bun test src (core) 3012 pass, 0 fail
bun test (mcp) 388 pass, 0 fail
bun test scripts/openai-tool-annotation-policy.test.ts, bun run skills:validate 5 pass; 69 pass
bun run check, core check-types, mcp tsgo --noEmit clean (6 pre-existing infos)

The tests use the trial fixtures:

  • the E5 louver on both faces of a wall, and refused when too high, with the fit range;
  • the E2 air handler under a ceiling, and refused outside its polygon;
  • the example table on a level and on a slab, then a vase on its support surface: the attachment is written, an overlapping second vase is refused, and an unknown surface lists the valid ones;
  • 11 refusal cases, each asserting the code prefix, plus node refusals that name the field (invalid_placement: node: Unknown slot paint);
  • through MCP: the scene is unchanged after every refusal, and validate_scene stays valid after placements.

House effect: no change. This is an agent-facing tool, and the converter, viewer and bake never call it.

Eval (AK-02 3/3, cap-design-louver)

Latest run (2026-09-28, on the review-fix heads #948 eaf8c7cec / #953 96b80a3b6, prompt with "place exactly one vent", MCP lane with read_resource and prompt mcp-lane-v2; records bench/ai-chat/runs/eval-2026-09-28T04-35-16-586Z direct, …T04-44-47-852Z mcp):

Lane Valid wall-side louver of the right size Exactly one placed Notes
direct 3/3 0/3 (+6, +7, +2) The no-duplicate line did not help: agents re-place after each re-validation. Two runs hit the 40-segment ceiling; 21–30 validate_design calls
mcp 0/3 2/3 Every run read pascal://schema/design first and placed within 27–87 s, but both placed louvers are 0.45–0.48 m deep instead of ~0.08 m; the third run never placed

Net: the design tools work end to end on both lanes. Duplicate placement in chat and depth control via MCP are prompt and task findings for a follow-up, not tool defects.

R1–R9

Rule Check
R1 API v1 and node schemas unchanged; additive exports
R3 The one writer path validates the final graph for the new node and its surface host (validateProceduralRelations) before applyPatch; no new reference kinds
R5 Agents can place designs on MCP; chat and scene programs follow in AK-02 3/3 over the same function
R7 Inline v1 recipes; pins wait for AK-01/P-05
R8 AD-6 exception (above): typed input, coded refusals, annotation inventory. The eval and the second adapter come in AK-02 3/3
R2, R4, R6, R9 Unaffected

For Wassim to review

design-placement.ts reuses validateProceduralRelations and proceduralLocalPose and mirrors prepareProceduralPlacement. It also adds slab and zone resolution and design-surface attachments. Check that the wall position semantics (reference along, height and offset) match how the move tool writes them.

Known limits

  • Hosted collaboration projects still refuse MCP whole-scene writes (P-05/P-08). Inline designs above the 64 KiB operation cap wait for AK-01.
  • Floor designs get no floor-fit or collision check, the same as place_item; check_collisions reports them.

🤖 Generated with Claude Code


Note

Medium Risk
Adds a new agent-driven scene mutation path and placement rules (wall/ceiling hosts, attachments) that must stay consistent with existing procedural relations; patch history grouping changes undo behavior for mixed batches.

Overview
Agents get a design validation and placement pipeline for procedural-item recipes: core validateDesign / describeDesignSchema (schema, sweeps, measurements, coded diagnostics) and planDesignPlacement, which validates the recipe, resolves hosts from mounting (floor/slab/zone/design surface, wall-side, ceiling), runs validateProceduralRelations without relying on the node registry, and returns create plans or DesignPlacementError codes (including wall fit ranges and a 24 KiB inline cap).

MCP exposes read-only validate_design, create-only place_design (via applyPatch + patch identity guards), and resource pascal://schema/design, with agent-guide/README updates. Trial HVAC/louver/stair-guard fixtures back core and MCP tests.

Renderer alignment: shared PRIMITIVE_TESSELLATION, shapeTriangles, and shapeBounds so validation counts match buildProceduralGeometry. apply_patch is wrapped in runAsSingleSceneHistoryStep so a create plus surface attachment update undo together. OpenAI tool annotation inventory bumps 49 → 51.

Reviewed by Cursor Bugbot for commit 96b80a3. Bugbot is set up for automated code reviews on this repo. Configure here.

@pascal

pascal Bot commented Sep 27, 2026

Copy link
Copy Markdown

I hit an error while handling your request (Model unavailable on AI Gateway free tier: Free tier users do not have access to this model. Upgrade to paid credits at https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dtop-up for unrestricted…).

Please try again, rephrase, or reach out if it keeps failing.

Error id: 045136d6-d42f-4d3c-a6d1-0051f452ef15

@Aymericr
Aymericr force-pushed the fidelity/ak-02b-place-design branch 3 times, most recently from 96c2bab to 87d84be Compare September 27, 2026 22:54
Aymericr and others added 3 commits September 27, 2026 19:50
…_design (AK-02 1/3)

Core gains describeDesignSchema() and validateDesign(design, { parameters }).
The schema is generated from RecipeSchema with z.toJSONSchema (one named
recursive $def, Expr), because the node's recipe field is an opaque z.custom.
validateDesign is the authority over the rules JSON Schema cannot express: it
parses, sweeps the parameter ranges and measures per-part bounds and instances,
the triangles the renderer builds, draw groups per slot, datum contact and
balance, and connected components (floating parts). It accepts the design as
an object or a JSON string and never throws.

MCP serves the contract as pascal://schema/design and wraps validateDesign as
the read-only validate_design tool; the annotation inventory grows to 50 tools.
A nodes test pins the triangle and draw-group counts to buildProceduralGeometry.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…passes behind the wall

The rules tell agents to keep wall-side geometry in front of the reference
plane, but a design whose shapes reached behind it validated silently (the
contact test even counted the penetration as touching). It now carries a
behind_wall warning naming the parts and the depth. It stays a warning: the
recipe and placement contracts accept such designs today (found by Bugbot).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rors agents hit most

The AK-02 eval showed agents looping on validate_design: 29–34 calls in the
direct lane and every MCP-lane place_design refused with "Mounting requires
one named, non-repeated reference surface", a message that does not say what
to declare. Rule and sweep diagnostics now carry a hint built from the design
itself: the reference and the declared surfaces plus a surface snippet for the
mounting kind, the declared slots, parts or parameters for unknown references,
and the y >= 0 and shape-budget repairs.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Aymericr
Aymericr force-pushed the fidelity/ak-02b-place-design branch from 87d84be to 30de136 Compare September 27, 2026 23:51
Aymericr and others added 7 commits September 28, 2026 00:14
…te_design

Four cases that fail on the current head:
- trim 1 m behind the wall reference counts as touching it;
- 64 draw groups pass without a draw-budget diagnostic;
- a cone beside its base is weighed as a full cylinder, so balance fails;
- triangle counts are not derived from a shared tessellation (no export).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ance, one tessellation source

From the Codex review of AK-02 1/3; the failing cases are the previous commit.
- Datum contact needs the shape to reach the plane within tolerance, so geometry
  wholly behind a wall (or above a ceiling) no longer counts as touching it.
- A draw_budget warning fires above 16 draw calls (slot × motion group) per
  instance, and measurements report the budget.
- Balance weighs a tapered cylinder as the frustum it is: π/12·(1 + t + t²).
- PRIMITIVE_TESSELLATION and shapeTriangles(primitive, topScale) live in core's
  recipe module; the nodes renderer builds with those segments, so the counts
  cannot drift from the geometry across package versions.
- The publishing evidence records the date and suite it was checked with.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…AK-02 2/3)

Core gains planDesignPlacement(nodes, request): it validates the design with
validateDesign, picks the host from the design's mounting (level, or a slab or
zone of it, or a named design surface for floor designs; a straight wall face
for wall-side designs; a ceiling for ceiling designs), builds the node with
today's hosting fields (parentId, wallId/side, attachments) and runs the
procedural relation checks directly, so they hold without the node registry.
Refusals are typed (invalid_design, invalid_placement, node_exists,
host_not_found, wrong_host, unknown_surface, does_not_fit); a wall misfit says
where the reference fits.

The MCP tool place_design wraps it create-only through applyPatch; the
annotation inventory grows to 51 tools.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… about

A slot or id the node schema rejects came back as the raw zod issue JSON; it is
now one line per issue, e.g. "invalid_placement: node: Unknown slot paint".

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rds (#938)

With #938 merged, place_design runs assertPatchKeepsIdentity over its create
(and the design-surface host's attachment update) before applyPatch, so an
explicit id that exists is refused with the same node_exists code and message
as apply_patch. The duplicate check planDesignPlacement carried until then is
gone. Refusals become tool errors with a JSON { code, message, diagnostics? }
body, as apply_patch returns them: an McpError keeps only its message, which
dropped the codes and diagnostics.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three cases that fail on the current head:
- a design-surface placement adds two history entries, and one undo leaves the
  child in place without its attachment;
- a 25 KiB inline design is placed instead of being refused under R7's 24 KiB;
- reusing the surface host's id reports does_not_fit instead of node_exists.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…B, checks ids first

From the Codex review of AK-02 2/3; the failing cases are the previous commit.
- SceneBridge.applyPatch commits the whole patch as one history step
  (runAsSingleSceneHistoryStep), as apply_patch's description already promised,
  so a design and its surface host's attachment undo together.
- planDesignPlacement refuses designs above 24 KiB of JSON with
  design_too_large (R7: larger designs wait for pinned definitions).
- An explicit id that exists is refused with node_exists before the surface
  host's shadow graph is built, so reusing the host's own id no longer reads as
  does_not_fit. The apply_patch identity guard still runs over the final patch.
The oversize fixture in the failing-case commit was too small (14.8 KiB); it now
builds a valid ~60 KiB design.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Aymericr
Aymericr force-pushed the fidelity/ak-02b-place-design branch from 30de136 to 96b80a3 Compare September 28, 2026 04:17
@Aymericr
Aymericr merged commit af059b0 into main Sep 28, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant