Chapters

Working with an AI agent

Describe a part, let the agent build it in 3DCraft while you watch, and check what it made.

An agent connected to 3DCraft (Install › Connect an AI agent) works the way a careful draughtsman would. It names the sizes as variables, adds features one by one, and reads back what it built.

Your part is to say what you want, to watch, and to check. You do not need to know the commands: the agent looks them up itself.

What to ask

Ask for a part the way you would describe it to a colleague, with the numbers that matter and what they are measured from:

Make a bolt flange: a disc 80 mm across and 10 mm thick with a 30 mm bore, and six 8 mm bolt holes on a 60 mm pitch circle. Drive it by variables, so that I can change the number of holes. Assert its volume. Save it as flange.3dcraft and export a STEP file.

A few words from that request, for readers new to CAD:

Word Means
Bore The large hole through the middle, for a shaft or a pipe
Pitch circle (or bolt circle) The circle the centres of the bolt holes lie on
Variable A named size, such as n for the number of holes, that the model follows when it changes
Assert Keep a check in the design: here, that its volume is what a formula says
Fillet (or round), chamfer An edge rounded off, or cut off at an angle
STEP The file format other CAD programs read: exact solids, not triangles

Things that help:

More things to ask

You say The agent …
"Round the outside edges of the top by 2 mm." adds a fillet on the edges it finds by their shape and place
"Make the hole I selected 6.5 mm." reads your selection, finds the size that drives that hole and changes it
"Why did the fillet fail?" reads the reason 3DCraft gave (often with the largest size that fits) and proposes a fix
"Show me the history step by step." plays the history in your window
"Make this a Jig with width, depth and height between 50 and 200 mm." publishes those variables as parameters and exports a .3dcraftjig (Making Jigs)
"Export the outline of the top for the laser cutter." writes a DXF or SVG of that face at true size (Files, export and drawings)
"Something went wrong; give me the bug report." returns the report of the last failed command (Troubleshooting)

A full session

This is what an agent sent for the request above, and what came back. The results are shortened (...), but every key and number shown is real; a test runs this session again at every change of 3DCraft. The agent ran headless here, craft3d-cli mcp --automation-read-root work --automation-write-root work.

1. Learn the commands. command_list with a filter, for the commands it needs; the parameters come with each. (brief: true gives every command with its parameters and the first sentence of its summary, a fraction of the size; id then gives one command whole.)

command_list {"filter": "pattern"}
-> {"commands": [{"id": "sketch.pattern", ...}, {"id": "feature.pattern",
     "params": "{\"source\": id or name, \"count\": expr, \"step\": [expr, expr, expr] or, with angle, expr, \"angle\": expr, ...}",
     "summary": "...", "enabled": false, "disabledReason": "no document is open; run file.new or file.open first"}]}

2. A document, then the variables, an assertion and the features in one batch. The assertion comes first: it names Flange before that exists, is in error until it does, and passes once it is built.

command_run {"id": "file.new", "params": {"units": "mm"}}
-> {"document": 0, "units": "mm"}

command_batch {"terse": true, "steps": [
  {"id": "variable.set", "params": {"name": "flange_d", "expr": "80 mm"}},
  {"id": "variable.set", "params": {"name": "thickness", "expr": "10 mm"}},
  {"id": "variable.set", "params": {"name": "bore_d", "expr": "30 mm"}},
  {"id": "variable.set", "params": {"name": "pcd", "expr": "60 mm"}},
  {"id": "variable.set", "params": {"name": "hole_d", "expr": "8 mm"}},
  {"id": "variable.set", "params": {"name": "n", "expr": "6"}},
  {"id": "assert.add", "params": {"name": "volume", "measure": "volume", "of": "Flange",
      "eq": "pi * ((flange_d / 2)^2 - (bore_d / 2)^2 - n * (hole_d / 2)^2) * thickness"}},
  {"id": "feature.cylinder", "params": {"radius": "flange_d / 2", "height": "thickness", "name": "Disc"}},
  {"id": "feature.cylinder", "params": {"radius": "bore_d / 2", "height": "thickness + 2 mm",
      "position": [0, 0, "-1 mm"], "name": "Bore"}},
  {"id": "feature.boolean", "params": {"op": "subtract", "target": "Disc", "tool": "Bore", "name": "Bored"}},
  {"id": "feature.cylinder", "params": {"radius": "hole_d / 2", "height": "thickness + 2 mm",
      "position": ["pcd / 2", 0, "-1 mm"], "name": "BoltHole"}},
  {"id": "feature.pattern", "params": {"source": "BoltHole", "count": "n", "angle": "360 deg / n", "name": "BoltHoles"}},
  {"id": "feature.boolean", "params": {"op": "subtract", "target": "Bored", "tool": "BoltHoles", "name": "Flange"}}]}
-> {"completed": 13, "failed": 0, "errors": [],
    "last": {"step": 12, "result": {"id": "F6", "name": "Flange", "status": "ok",
             "assertionSummary": {"count": 1, "passed": 1, "broken": [], "unproven": [], "pending": 0}}}}

3. Check. assert.list says what was measured against what was expected:

command_run {"id": "assert.list", "params": {}}
-> {"ok": true, "count": 1, "passed": 1, "failed": 0, "notProven": 0, "errors": 0,
    "assertions": [{"name": "volume", "status": "passed", "measured": 40180.97003941347,
                    "expected": "40180.97 mm^3 ± 0.04018097", ...}]}

4. Find the holes and what drives them. select.query with context lists every hole with its radius, its axis and the edit that changes it:

command_run {"id": "select.query", "params": {"kind": "face", "where": {"hole": true}, "context": true}}
-> {"count": 7, "items": [
     {"name": "F6:copy(0)/F3:copy(1)/F2:face(0)", "origin": {"name": "Bore", "type": "cylinder"},
      "surface": {"type": "cylinder", "radius": 15.0, "hole": true, ...},
      "drivers": [{"param": "radius", "expr": "bore_d / 2",
                   "edit": {"command": "feature.edit", "params": {"feature": "F2", "radius": "bore_d / 2"}}}]},
     {"name": "F6:copy(1)/F4:face(0)", "copyOf": [{"name": "BoltHoles", "instance": 0}], ...}, ...]}

5. Change it. Eight holes instead of six. The result of the edit carries the assertions' summary: still passing, because the expected volume is written in n too.

command_run {"id": "variable.set", "params": {"name": "n", "expr": "8"}}
-> {"name": "n", "value": 8.0, "assertionSummary": {"count": 1, "passed": 1, "broken": [], ...}}

command_run {"id": "measure.thickness", "params": {"of": "Flange"}}
-> {"thickness": 6.0, "atLeast": 5.99999999999968,
    "at": {"face": "F6:copy(1)/F4:face(0)", "point": [34.0, ...]},
    "to": {"face": "F6:copy(0)/F1:face(0)", "point": [40.0, ...]}}

The thinnest wall is 6 mm, between a bolt hole and the rim. atLeast is what is proven: no wall is thinner than that anywhere (Measuring and assertions).

6. A refusal, and undo. Asked for a 20 mm round on the rim of a 10 mm disc, the fillet is added but fails. command_run does not report an error, so the agent reads status:

command_run {"id": "feature.fillet", "params": {"edges": [["F1:face(0)", "F1:face(2)"]], "radius": "20 mm", "name": "Round"}}
-> {"id": "F7", "name": "Round", "status": "failed",
    "error": "radius: not built for edges.0 (between F6:copy(0)/F1:face(0) and F6:copy(0)/F1:face(2)): with a radius of 20 mm the fillet face would be a torus that reaches its own axis ...which is not built yet; so it is for every radius from 20 mm up to 40 mm..."}

command_run {"id": "edit.undo", "params": {}}
-> {"undone": "Add fillet"}

7. Look, save, export.

command_run {"id": "document.check", "params": {}}
-> {"ok": true, "problems": [], "bodiesChecked": 1, "assertionSummary": {"passed": 1, ...}}
doc_render_preview {"view": "iso"}
-> (a PNG image) and {"coverage": ..., "triangles": ..., "bodies": [...]}
command_run {"id": "file.save", "params": {"path": "flange.3dcraft"}}
-> {"path": "flange.3dcraft"}
command_run {"id": "file.export", "params": {"path": "flange.step"}}
-> {"format": "step", "bodies": 1, "solids": 1, "faces": 12, "unit": "mm", ...}

The flange lesson is the same part with a chamfer and a named set of bolt holes.

Checking the agent's work

Do not take "done" on trust. 3DCraft gives you the means to check, without reading code:

  1. The feature list in the window (The window): no row is red, and the line under the filter says N rebuilt · M checks pass.
  2. The checks (assertions): they are the agent's claims about the part, written as numbers you can read ("volume = π((d/2)² − …) t"). assert.list shows them; ask for the ones you care about.
  3. Your own numbers: ask for measure.distance, measure.thickness or measure.through between the faces you point at, or for the volume and bounding box document.inspect gives, and compare them with what you expect.
  4. Change a variable and see that the part follows, and the checks still pass.
  5. Look: the window, or doc_render_preview with projection: "orthographic" and a view such as top for a picture to scale, or mode: "ids" (one colour per face) to see which face is which.
  6. Reopen the file (file.open): a .3dcraft file rebuilds bit for bit to the same bodies.
  7. Play the history (the player under the feature list, or ask the agent to play it): each step should make sense on its own.

Tips for a good session

What the agent gets back

You do not need this to work with an agent, but it helps to read along. Every command answers with JSON.

Programs on 3DCraft › What a command answers has the full rules, for people who write their own programs or read an agent's work closely.

Try this first: five tasks

Five tasks to start with, each with the answer 3DCraft should give. Each takes 5 to 20 minutes. Give the agent the text in quotes, in your own words if you like, and in your own language if you prefer. Then check its work as above: look at the window, click faces, and ask for the volume.

A refusal ("not built", "does not fit") is not a failure of the task. It says what 3DCraft cannot do yet, and that is worth reporting (Troubleshooting).

Task 1: a block, to see the connection work. "Make a 40 × 20 × 10 mm block in 3DCraft, with a corner at the origin. Tell me its volume." The block appears in the window, and the answer is 8000 mm³.

Task 2: an L-bracket. "In 3DCraft, make an L-shaped angle bracket of 4 mm stock, 25 mm wide: one leg lies flat on the XY plane and runs 40 mm along +X, the other stands up along +Z to a height of 30 mm, both legs measured to the outside of the corner, which is at the origin. Drive it by the variables leg_x, leg_z, thickness and width. Tell me its volume." The right answer is 6600 mm³. Then ask it to round the inside corner with a 4 mm radius, and to make the bracket 30 mm wide: the rounding should follow.

Task 3: a strip with a counted row of holes. "Make a flat strip 120 × 20 × 3 mm (corner at the origin, along +X, +Y, +Z) with a row of 5 mm holes along its centre line at a 10 mm pitch: as many holes as fit with every hole centre at least 7.5 mm from the ends, the row centred on the strip. The number of holes must follow the length." That is 11 holes and 6552.05 mm³. Now ask for a strip of 150 mm: 14 holes and 8175.33 mm³, without the agent rebuilding anything by hand.

This is one way to build it, as a script (The command line and scripts runs it with craft3d-cli run --new '{"units":"mm"}' --script strip.json --out strip.3dcraft). Its two assertions check the volume and the number of holes at every change:

[
 ["variable.set", {"name": "length", "expr": "120 mm"}],
 ["variable.set", {"name": "width", "expr": "20 mm"}],
 ["variable.set", {"name": "thickness", "expr": "3 mm"}],
 ["variable.set", {"name": "hole_d", "expr": "5 mm"}],
 ["variable.set", {"name": "pitch", "expr": "10 mm"}],
 ["variable.set", {"name": "margin", "expr": "7.5 mm"}],
 ["variable.set", {"name": "n", "expr": "floor((length - 2 * margin) / pitch) + 1"}],
 ["feature.block", {"size": ["length", "width", "thickness"], "name": "Strip"}],
 ["feature.cylinder", {"radius": "hole_d / 2", "height": "thickness", "position": ["(length - (n - 1) * pitch) / 2", "width / 2", 0], "name": "HoleTool"}],
 ["feature.pattern", {"source": "HoleTool", "count": "n", "step": ["pitch", 0, 0], "name": "HoleTools"}],
 ["feature.boolean", {"op": "subtract", "target": "Strip", "tool": "HoleTools", "name": "Perforated"}],
 ["assert.add", {"name": "volume", "measure": "volume", "of": "Perforated", "eq": "(length * width - n * pi / 4 * hole_d^2) * thickness"}],
 ["assert.add", {"name": "holes", "measure": "count", "of": {"kind": "face", "where": {"hole": true}}, "eq": "n"}]
]

Task 4: a case, and a rule that guards it. "Make a moulded case: outside 70 × 50 × 25 mm (corner at the origin), the top edges and the four upright corners rounded with a 2 mm radius, the bottom edges sharp, hollowed to a 2 mm wall and open at the bottom. Drive it by variables. Then add an assertion that the thinnest wall is at least 1.5 mm." The volume is 17385.50 mm³, and at 2 mm the assertion passes: 3DCraft proves the wall is at least 1.5 mm everywhere, round corners included. Now ask for a 1 mm wall: the window shows the check failing, and the agent should tell you so (Measuring and assertions).

Task 5: make a Jig of it. "Publish length and pitch of the strip from task 3 as parameters (length from 40 to 300 mm, pitch from 8 to 20 mm) and export it as a Jig." Open the .3dcraftjig in 3DCraft: the Parameters tab shows the two values, under the variables' own names length and pitch unless the agent gave them labels (ask for labels such as Length and Pitch if you want them). Drag them and watch the holes follow, then export an STL, or a DXF of the top face for a laser cutter (Making Jigs). A block's face 1 is its top (Modelling features › Primitives), so in the script of task 3 the top face is F1:face(1).

After these, try a part of your own: something from your workshop, your classroom or your hobby.

Next: The window, or straight to Using a Jig.

This page on GitHub