From df488f9a52b41fe6d12950b3ab46946108f4984c Mon Sep 17 00:00:00 2001 From: SS-EN Date: Mon, 19 Oct 2026 11:59:49 -0400 Subject: [PATCH] MCP: leave inspect_design and tool JSON as on main (moved to their own PR) Per review, this PR keeps to long requests, honest batch outcomes and tool names in a batch. inspect_design is back to main's arguments and output (full sketches by default, no bodies filter), document.inspect's sketch entries are unchanged, and tool results are pretty-printed JSON again; the compact inspect_design moves to its own PR, off by default. The docs no longer describe agent-cursor batch behaviour that is not in this branch. An unknown argument's -32602 error still lists the tool's arguments: same code and same leading text, only more helpful. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/cmd/inspect.rs | 6 +- crates/mcp/src/tests.rs | 56 +---------- crates/mcp/src/tools.rs | 125 ++++-------------------- docs/control-protocol.md | 4 +- docs/mcp.md | 7 +- skills/solidcraft-modeling/reference.md | 2 +- 6 files changed, 29 insertions(+), 171 deletions(-) diff --git a/crates/engine/src/cmd/inspect.rs b/crates/engine/src/cmd/inspect.rs index 24e9fcdbaa..6b02a84a6f 100644 --- a/crates/engine/src/cmd/inspect.rs +++ b/crates/engine/src/cmd/inspect.rs @@ -256,11 +256,7 @@ fn inspect(s: &mut Session, p: &Value) -> Result { let sketches: Vec = st .sketches .iter() - .map(|ss| { - json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "plane": ss.plane, - "points": ss.sketch.points.len(), "curves": ss.sketch.curves.len(), "constraints": ss.sketch.constraints.len(), - "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)}) - }) + .map(|ss| json!({"id": ss.feature, "name": ss.name, "dof": ss.report.dof, "status": ss.report.status, "curves": ss.sketch.curves.len(), "profiles": ss.profiles.len(), "visible": crate::view::sketch_shown(s, ss.feature)})) .collect(); Ok(json!({ "name": s.doc.name, "units": s.doc.units, "metadata": s.doc.metadata, "path": s.path, "dirty": s.is_dirty(), "revision": s.revision, diff --git a/crates/mcp/src/tests.rs b/crates/mcp/src/tests.rs index 18e712ec8e..41ba3c680e 100644 --- a/crates/mcp/src/tests.rs +++ b/crates/mcp/src/tests.rs @@ -167,7 +167,7 @@ fn agent_builds_box_fillet_hole() { assert!((8..=9).contains(&m["bodies"][0]["faces"].as_u64().unwrap()), "{m}"); assert!(rel(m["bodies"][0]["bbox"]["max"][0].as_f64().unwrap(), 40.0) < 1e-6, "{m}"); - let d = tool(&mut s, "inspect_design", json!({"sketches": true})); + let d = tool(&mut s, "inspect_design", json!({})); assert_eq!(d["timeline"].as_array().unwrap().len(), 5, "{d}"); assert!(d["timeline"].as_array().unwrap().iter().all(|f| f["error"].is_null()), "{d}"); assert_eq!(d["sketches"][0]["curves"].as_array().unwrap().len(), 4, "{d}"); @@ -1812,65 +1812,13 @@ fn batch_steps_may_name_tools() { assert!(volume(&mut s) > 4000.0); } -/// `inspect_design` is small by default (#15): sketch summaries, compact JSON. Full sketch -/// contents only for the sketches asked for; `bodies` keeps only those bodies. -#[test] -fn inspect_design_summarises_sketches_unless_asked() { - let mut s = server(); - let points: Vec<[f64; 2]> = (0..720) - .map(|i| { - let t = f64::from(i) / 720.0 * std::f64::consts::TAU; - [30.0 * t.cos(), 30.0 * t.sin()] - }) - .collect(); - tool( - &mut s, - "batch", - json!({"commands": [ - {"command": "sketch.create", "params": {"plane": "XY"}}, - {"command": "sketch.line", "params": {"points": points, "closed": true}}, - {"command": "sketch.finish"}, - {"command": "solid.extrude", "params": {"distance": 5}}, - ]}), - ); - let text = |s: &mut Server, args: Value| { - let r = raw_tool(s, "inspect_design", args); - (r["result"]["isError"] == json!(true), r["result"]["content"][0]["text"].as_str().unwrap_or_default().to_string()) - }; - let (_, summary) = text(&mut s, json!({"measure": false})); - assert!(summary.len() < 4000, "{} bytes: {summary}", summary.len()); - assert!(!summary.contains("\n "), "compact JSON"); - let d: Value = serde_json::from_str(&summary).unwrap(); - let sk = &d["sketches"][0]; - assert!(sk["points"].as_u64().is_some_and(|n| n >= 720), "{sk}"); - assert_eq!((&sk["curves"], &sk["profiles"]), (&json!(720), &json!(1)), "{sk}"); - assert!(sk["constraints"].is_u64() && sk["dof"].is_number() && sk["plane"].is_object(), "{sk}"); - assert_eq!(text(&mut s, json!({"measure": false, "sketches": false})).1, summary); - assert_eq!(text(&mut s, json!({"measure": false, "sketches": "summary"})).1, summary); - let name = sk["name"].as_str().unwrap().to_string(); - let (_, full) = text(&mut s, json!({"measure": false, "sketches": [name]})); - assert!(full.len() > 50_000 && full.len() > 20 * summary.len(), "{} vs {}", full.len(), summary.len()); - let f: Value = serde_json::from_str(&full).unwrap(); - assert_eq!(f["sketches"][0]["curves"].as_array().map(Vec::len), Some(720)); - assert_eq!(text(&mut s, json!({"measure": false, "sketches": true})).1.len(), full.len()); - let (err, e) = text(&mut s, json!({"sketches": ["Nope"]})); - assert!(err && e.contains("no sketch") && e.contains(&name), "{e}"); - - let (_, one) = text(&mut s, json!({"bodies": ["Body1"]})); - let one: Value = serde_json::from_str(&one).unwrap(); - assert_eq!(one["bodies"].as_array().map(Vec::len), Some(1), "{one}"); - assert!(one["bodies"][0]["volume_mm3"].as_f64().is_some_and(|v| v > 0.0) && one["bodies"][0]["visible"] == true, "{one}"); - let (err, e) = text(&mut s, json!({"bodies": ["Nope"]})); - assert!(err && e.contains("no body `Nope`") && e.contains("Body1"), "{e}"); -} - /// An unknown argument is refused with the tool's real arguments listed. #[test] fn unknown_arguments_list_the_real_ones() { let mut s = server(); for (tool_name, want) in [ ("screenshot", &["view", "camera", "path", "width", "height", "source", "grid", "axes"][..]), - ("inspect_design", &["measure", "sketches", "bodies"][..]), + ("inspect_design", &["measure", "sketches"][..]), ("body_topology", &["body"][..]), ] { let args = if tool_name == "body_topology" { json!({"body": "Body1", "nonsense": 3}) } else { json!({"nonsense": 3}) }; diff --git a/crates/mcp/src/tools.rs b/crates/mcp/src/tools.rs index 884a7b794a..e8791f1f47 100644 --- a/crates/mcp/src/tools.rs +++ b/crates/mcp/src/tools.rs @@ -129,19 +129,12 @@ pub fn tool_definitions() -> Vec { "Inspect the design", "The whole design: user parameters (expression, value, errors), the timeline (features with type, suppressed, \ rolled back, error, warning, timing), bodies (volume, area, centre of mass, bounding box, face/edge/vertex \ - counts), sketches, the active sketch, selection and undo depth. Sketches are summarised by default (id, name, \ - plane, counts of points, curves, constraints and profiles, degrees of freedom, solve status); their full contents \ - (curves with ids and coordinates, points, constraints, profiles) only for the sketches named in `sketches`, or \ - all with `sketches: true` (large: a 720-point sketch is some 200 KB). Arguments: measure, sketches, bodies.", + counts), sketches (curves with ids and coordinates, points, constraints, profiles, degrees of freedom, solve \ + status), the active sketch, selection and undo depth.", obj( json!({ "measure": {"type": "boolean", "description": "Measure bodies (default true)"}, - "sketches": { - "type": ["boolean", "string", "array"], - "items": {"type": ["string", "integer"]}, - "description": "Which sketches in full: a list of sketch names or ids, true (all of them), or false / \"summary\" (none: summaries only, the default)", - }, - "bodies": {"type": "array", "items": {"type": "string"}, "description": "Only these bodies, by name (default: all)"}, + "sketches": {"type": "boolean", "description": "Full sketch contents (default true; false = summary only)"}, }), &[], ), @@ -169,8 +162,7 @@ pub fn tool_definitions() -> Vec { "List edges and faces", "The edges (index, midpoint, length, endpoints) and faces (index, name, area, centroid, plane normal, a `point` that lies \ on the face, and for a cylindrical face its `cylinder`: origin, axis, radius) of a body. Use an edge midpoint as the \ - [x,y,z] point that picks the edge for fillet and chamfer; a face's point or its name picks the face for the face commands. \ - Arguments: body (required).", + [x,y,z] point that picks the edge for fillet and chamfer; a face's point or its name picks the face for the face commands.", obj(json!({"body": string("Body name, e.g. \"Body1\"")}), &["body"]), Effect::ReadOnly, ), @@ -201,8 +193,7 @@ pub fn tool_definitions() -> Vec { Connected to the app: a screenshot of the window (source \"window\", the default) or a render of the model \ with the app's camera (source \"model\"). Both show an active section (inspect.section) with its cut face \ capped. `view` first turns the camera to a standard view or a saved named view (view.save), `camera` to any camera (as view.apply). \ - `grid: false` and `axes: false` leave the ground grid (on Z 0) and the origin's axes out of a model render. \ - Arguments: view, camera, path, width, height, source, grid, axes (all optional).", + `grid: false` and `axes: false` leave the ground grid (on Z 0) and the origin's axes out of a model render.", obj( json!({ "view": string( @@ -362,12 +353,11 @@ pub(crate) fn check_arguments(name: &str, args: &Value) -> Result<(), String> { Ok(()) } -/// A tool result: content blocks plus the error flag. JSON is compact (not pretty-printed): the -/// agent reads every byte. +/// A tool result: content blocks plus the error flag. fn text_result(v: &Value, is_error: bool) -> Value { let text = match v { Value::String(s) => s.clone(), - v => v.to_string(), + v => serde_json::to_string_pretty(v).unwrap_or_default(), }; let mut r = json!({"content": [{"type": "text", "text": text}]}); if is_error { @@ -415,7 +405,19 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map) -> Result inspect_design(b, a), + "inspect_design" => { + let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true); + let mut d = b.call("document.inspect", json!({"measure": measure}))?; + if a.get("sketches").and_then(Value::as_bool).unwrap_or(true) { + let ids: Vec = d["sketches"].as_array().map(|v| v.iter().map(|s| s["id"].clone()).collect()).unwrap_or_default(); + let full: Vec = ids + .into_iter() + .map(|id| exec(b, "sketch.inspect", json!({"sketch": id.clone()})).unwrap_or_else(|e| json!({"id": id, "error": e}))) + .collect(); + d["sketches"] = Value::Array(full); + } + Ok(d) + } "pending" => pending(b), "measure" => { if let Some(items) = a.get("items") { @@ -461,93 +463,6 @@ fn filter_commands(all: Value, filter: Option<&str>) -> Value { Value::Array(all.as_array().map(|v| v.iter().filter(|c| hit(c)).cloned().collect()).unwrap_or_default()) } -/// Names for an error message: at most 40 of them. -fn name_list(names: &[String]) -> String { - let mut out = names.iter().take(40).cloned().collect::>().join(", "); - if names.len() > 40 { - out.push_str(&format!(", … ({} in all)", names.len())); - } - if out.is_empty() { "none".into() } else { out } -} - -/// `inspect_design`: `document.inspect`, with sketch summaries unless sketches are asked for in -/// full (by name or id, or all), and only the bodies asked for. -fn inspect_design(b: &mut dyn Backend, a: &Map) -> Result { - let measure = a.get("measure").and_then(Value::as_bool).unwrap_or(true); - let bodies: Option> = match a.get("bodies").filter(|v| !v.is_null()) { - None => None, - Some(v) => Some( - v.as_array() - .and_then(|l| l.iter().map(|n| n.as_str().map(str::to_string)).collect::>>()) - .ok_or("inspect_design: `bodies` must be a list of body names")?, - ), - }; - // With `bodies`, only those are measured. - let mut d = b.call("document.inspect", json!({"measure": measure && bodies.is_none()}))?; - if let Some(want) = bodies { - let all: Vec = d["bodies"].as_array().cloned().unwrap_or_default(); - let names: Vec = all.iter().filter_map(|x| x["name"].as_str().map(str::to_string)).collect(); - if let Some(missing) = want.iter().find(|n| !names.contains(n)) { - return Err(format!("inspect_design: no body `{missing}` (bodies: {})", name_list(&names))); - } - let mut kept: Vec = all.into_iter().filter(|x| x["name"].as_str().is_some_and(|n| want.iter().any(|w| w == n))).collect(); - if measure && !want.is_empty() { - let m = exec(b, "inspect.measure", json!({"bodies": want}))?; - for body in &mut kept { - let measured = m["bodies"].as_array().and_then(|l| l.iter().find(|x| x["name"] == body["name"])).cloned(); - if let (Some(Value::Object(fields)), Some(o)) = (measured, body.as_object_mut()) { - for (k, v) in fields { - o.entry(k).or_insert(v); - } - } - } - } - d["bodies"] = Value::Array(kept); - } - let sketches: Vec = d["sketches"].as_array().cloned().unwrap_or_default(); - let full: Vec = match a.get("sketches") { - None | Some(Value::Null) | Some(Value::Bool(false)) => Vec::new(), - Some(Value::String(t)) if t == "summary" => Vec::new(), - Some(Value::Bool(true)) => sketches.iter().map(|x| x["id"].clone()).collect(), - Some(Value::String(t)) if t == "full" || t == "all" => sketches.iter().map(|x| x["id"].clone()).collect(), - Some(Value::Array(list)) => { - let mut ids = Vec::new(); - for want in list { - let found = sketches.iter().find(|x| match want { - Value::String(n) => x["name"].as_str() == Some(n.as_str()) || x["id"].as_u64().is_some_and(|id| n.parse::() == Ok(id)), - Value::Number(_) => x["id"] == *want, - _ => false, - }); - match found { - Some(x) => ids.push(x["id"].clone()), - None => { - let names: Vec = sketches.iter().filter_map(|x| x["name"].as_str().map(str::to_string)).collect(); - return Err(format!("inspect_design: no sketch {want} (sketches: {})", name_list(&names))); - } - } - } - ids - } - Some(other) => { - return Err(format!("inspect_design: `sketches` must be true, false, \"summary\" or a list of sketch names (got {other})")); - } - }; - if !full.is_empty() { - let out: Vec = sketches - .into_iter() - .map(|x| { - if full.contains(&x["id"]) { - exec(b, "sketch.inspect", json!({"sketch": x["id"].clone()})).unwrap_or_else(|e| json!({"id": x["id"], "error": e})) - } else { - x - } - }) - .collect(); - d["sketches"] = Value::Array(out); - } - Ok(d) -} - /// `pending`: the end of a request still running when its call returned (bridged only; a /// headless session runs every request to its end). fn pending(b: &mut dyn Backend) -> Result { diff --git a/docs/control-protocol.md b/docs/control-protocol.md index f0cce95ff1..198de84133 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -78,7 +78,7 @@ MCP server sends its folders this way on every request ([mcp.md](mcp.md)). Witho |---|---|---| | `engine.execute` | `command`: command id, `params`: JSON object | the command's result | | `engine.script` | `commands`: `[{command, params}, …]` | results, stops at the first error | -| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back; `rolled_back`: whether the design really is back at its checkpoint). Refused before anything runs, with an error starting `engine.batch unavailable`, when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. While the agent cursor is shown, the pointer first acts out a short summary (the toolbar buttons of a few of its commands, within 1.5 s), then the whole batch runs. Answered when done, for up to 3600 s | +| `engine.batch` | `commands`: `[{command, params}, …]` (at most 10 000), `rollback?`: bool (default true), `timings?`, `profile?`: bool (default false) | `{completed, results, undone}`, plus `failed: {index, command, error}` when a step fails, and with `timings` `timings: [{index, command, ms}]` (the failing step included) and `total_ms`; `profile` adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`. Runs the steps in one request, so within one frame, each as `engine.execute` runs it. The first failure stops the batch, and with `rollback` the design goes back to how it was before it (`undone`: the steps taken back; `rolled_back`: whether the design really is back at its checkpoint). Refused before anything runs, with an error starting `engine.batch unavailable`, while the agent cursor is shown or when a step loads a file (`doc.open`, `file.insert_step`, `file.insert_mesh`). Send such steps one by one. Answered when done, for up to 3600 s | | `engine.commands` | — | every command: id, label, tab, panel, icon, shortcut, params doc, enabled | | `document.inspect` | `measure?`: bool | parameters, timeline (errors, timings), bodies, sketches, selection | @@ -160,7 +160,7 @@ labels; `prefs:measure_axis_distances` identifies the checkbox. | `ui.prefs` | any of `default_units`, `nav` (`fusion`, `solidworks`, `inventor`, `alias`, `blender`, `tinkercad`, `openscad`, `touchpad`), `zoom_reverse`, `pan_reverse_x` (default false: horizontal scrolling pans the other way), `zoom_to_cursor`, `smooth_wheel_zoom` (default true), `trackpad_pan` (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), `orbit_around` (`view_centre` (default: what Pan moves and Fit frames), `origin` (the world origin stays still on screen), `cursor` (the model point under the pointer when the drag began); the older `orbit_cursor` boolean still sets `cursor` when true), `look_at_anim`, `look_at_speed`, `middle_double_click` (`home` (default), `fit`, `off`), `msaa`, `show_frame_rate` (default false: frames per second in the view's bottom-right corner; the view redraws every frame while it is on), `length_decimals`, `angle_decimals`, `theme` (`dark`, `light`, `system` or a theme file's id: a built-in one such as `gruvbox-dark`, or an imported one; the reply's `theme_ids` lists them), `theme_import` (a `solidcraft-theme/1` file's JSON or text: adds the theme and turns it on; the reply's `theme_imported` is its id), `theme_remove` (an imported theme's id), `theme_export: true` (the reply's `theme_file` is the theme on now as a complete file; see [colour themes](book/src/guide/ui-tour.md#colour-themes)), `language` (`en`, `ja`, `de`, `fr`, `ko`, `ms`, `ru`, `uk`, `zh`; see [localization](localization.md)), `ui_scale`, `ui_font` (an installed font family or a font file for the interface; empty for the built-in font), `up_axis` (`z`, `y`), `offer_stand_up`, `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778), `check_updates` (default true; [updates](book/src/guide/updates.md)); `open` (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; `button`: `ok`, `cancel`, `apply`, `defaults`; `stand_up: true` stands up a just-opened Y-up file, `stand_up: false` leaves it as it is ("Leave it": the offer closes and `offer_stand_up` stays on); the offer's buttons are the handles `stand_up:stand`, `stand_up:leave` and `stand_up:never` | sets preferences (kept between runs), returns them all plus `dialog` (the open page or null), `dark` and `stand_up_offer` | | `ui.capture` | `action?`: `capture`, `revert`, `cancel` | answers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without `action`, what waits | | `ui.window` | `action`: minimize, maximize, restore, toggle, close | what the title bar's caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first | -| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to its picks (at most three: a long point list is summarised by its first, middle and last point on screen), then runs and replies, within 1.5 s per request whatever its size (the glides get shorter); an `engine.batch` is acted out as one request. Later requests wait their turn. `"animate": false` on a call skips it | +| `ui.agent_cursor` | `show` (bool), `speed`: a number from 1 to 5 (times the normal speed) or `"instant"` (no animation: the command runs at once and the pointer jumps to where it acted), `follow_camera` (bool); all optional | the settings. While the cursor is shown, `engine.execute` first glides to the command's toolbar button and to each pick, then runs and replies; later requests wait their turn. `"animate": false` on a call skips it | | `ui.agent_chat` | `features?`, `toolbar_button?`, `permissions?` (`ask`, `edits`, `all`), `add_agent?` ({`name`, `kind`: `acp` or `api`, `command` or `base_url` + `model`}; no keys), `default_agent?`, `agent?` (the conversation's agent, by id), `mode?` (the agent's own mode), `model?` (one of its models), `open?`, `side?` (`left`, `right`), `minimized?`, `popped?` (in its own window), `width?`, `picker?`, `refresh?`, `connect?`, `stop?`, `attach?`: a file's path (a picture, a text file or a CAD file, for the next message; CAD files are imported at once), `remove_file?` (index of a file on the next message), `send?`: text (files alone make a message too), `attach_view?`, `permission?`: an option id or `"cancel"` (the newest question of the agent's own), `approve?`: true, false or `"always"` (the oldest request held for Allow/Deny), `cancel?` (Stop: the queue stays), `send_now?` (stop the running step, then send the queued messages), `remove_pending?` (index), `new_chat?` (clear the chat, new session), `undo?` (the last message's changes) | the chat's state: `features`, `agents` (found and added), `currentAgent`, `state` (off, starting, ready, busy, auth, exited), `popped`, `window` (its place and size), `agent`, `modes`, `mode`, `models`, `model`, `authMethods`, `toolsPort`, `pid`, `waiting`, `turn`, `pending` (queued messages, with their `files`), `attachments` (the files on the next message, by name), `loading` (pictures still being read for it, by name), `stopped`, `detection` and the conversation (`items`). With `features` off only the settings apply. Desktop only; see [Agent chat](agent-chat.md) | | `ui.resize` | `width`, `height` | | | `app.quit` | `force?` (quit without asking about unsaved designs) | `quitting` (the window closes), `asking` (a "save changes?" prompt is open) | diff --git a/docs/mcp.md b/docs/mcp.md index f0633697d8..46b61e5b1a 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -144,8 +144,8 @@ They cover the workflow, the command shapes, how to verify with `measure`, and t |---|---|---| | `list_commands` | `filter?` | The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. `filter` matches id, label, tab and panel. | | `execute` | `command`, `params?`, `verbose?` (default true) | Run any command with JSON parameters (`list_commands` shows them). Failures leave the design unchanged. A feature command replies with the feature (`feature`, `name`), every body (`bodies`) and the bodies it created, changed and removed (`created_bodies`, `changed_bodies`, `removed_bodies`). `verbose: false` uses the same compact reply as `batch`. | -| `batch` | `commands: [{command, params}]`, `rollback?` (default true), `verbose?` (default true), `timings?`, `profile?` (default false) | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`), every result before it, `rolled_back` (true only when the design really went back to where the batch started), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, filtered by `filter` as the tool filters it), `execute` (`{command, params}`), `measure`, `inspect_design` (`document.inspect`, summaries), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch` and `pending` can't be steps: such a batch fails before anything is sent. `verbose: false` keeps the reply small in large designs: feature steps leave out the full body list (`created_bodies`, `changed_bodies`, `removed_bodies` stay) and sketch steps the ids they repeat outside `created`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app), also while the app shows the agent cursor (the pointer acts out a short summary of the batch first). Older apps get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. | -| `inspect_design` | `measure?` (default true), `sketches?` (a list of sketch names or ids, `true` for all, or `false` / `"summary"`: the default), `bodies?` (names; default all) | Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches, selection, undo depth. Each sketch is summarised (`id`, `name`, `plane`, the number of `points`, `curves`, `constraints` and `profiles`, `dof`, `status`, `visible`); its full contents (curves with ids and coordinates, points, constraints, profiles), as `execute` `sketch.inspect` gives them, only when `sketches` names it (or is `true`): a sketch of 720 points is some 200 KB in full. `bodies` keeps only the bodies named (and measures only them); an unknown sketch or body name is an error that lists the names there are. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). | +| `batch` | `commands: [{command, params}]`, `rollback?` (default true), `verbose?` (default true), `timings?`, `profile?` (default false) | Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (`failed.index`, `failed.command`, `failed.error`), every result before it, `rolled_back` (true only when the design really went back to where the batch started), `undone`, `applied` (the indices of the steps whose changes are in the design) and `not_run` (the steps never sent). A step may name a tool instead of a command, with that tool's arguments as `params`: `set_parameter` (runs `parameters.change`; `value` or `expression`), `list_commands` (`engine.commands`, filtered by `filter` as the tool filters it), `execute` (`{command, params}`), `measure`, `inspect_design` (`document.inspect`: each sketch as its counts, not its full contents), `undo`, `redo`, `export`, `save`, `open`, `new_design` and `crash_reports`. `screenshot`, `body_topology` (use `model.edges` and `model.faces`), `batch` and `pending` can't be steps: such a batch fails before anything is sent. `verbose: false` keeps the reply small in large designs: feature steps leave out the full body list (`created_bodies`, `changed_bodies`, `removed_bodies` stay) and sketch steps the ids they repeat outside `created`. Bridged to an app that has `engine.batch`, the whole batch is one control request (one frame of the app). Older apps, and apps showing the agent cursor, get the steps one by one. `timings: true` adds `timings: [{index, command, ms}]` for every step run, the failing one included, and `total_ms`. `profile: true` also adds each step's `profile: {boolean_ms, tessellate_ms, other_ms}`, its time in kernel booleans, in tessellation and the rest. Steps are timed where they run: in the app when bridged. An app without `engine.batch` is timed step by step, without `profile`. | +| `inspect_design` | `measure?` (default true), `sketches?` (default true) | Design properties (`metadata`: project, author… and other tools' keys; set them with `execute` `document.metadata.set`), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; `removed_later: true` while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches (curves, points, constraints, profiles, DOF, solve status, visible), selection, undo depth. Hide helper sketches, bodies or construction planes with `execute` `browser.visibility` (`{items, visible}` or `{folder, visible}`), and joint glyphs with `{joints: [id or name], visible}` or `{folder: "joints", visible}` (a hidden joint still constrains and drives; `joint.list` reports each joint's `visible`). | | `measure` | `body?` or `items?` | Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. With `items` (one or two selections as `execute` takes them: `face`, `edge`, `vertex`, `sketch_curve`, `profile`), the picked geometry instead: see below. | | `body_topology` | `body` | Edges (index, midpoint, length, ends) and faces (index, name, area, centroid, normal, `point`, and for a cylindrical face its `cylinder`: `origin`, `axis`, `radius`). An edge midpoint is the `[x,y,z]` that picks that edge for fillet and chamfer. A face's `point` lies on it and picks it (its centroid may not: that of a face with a hole is in the hole); a face's `name` picks it for the commands that take faces (`face.delete`, `solid.shell`, `solid.draft`, `solid.offset_face`, `solid.split_face`, `solid.replace_face`, and `face` of `sheet.convert`). | | `set_parameter` | `name`, `value` or `expression` (number or expression), `unit?`, `comment?` | Create or change a user parameter and recompute. `expression` is another name for `value` (the one `parameters.change` uses): give one of them, or both only when they agree; neither, or two that differ, is an error. Reports `recomputed` (features rebuilt), `restored` (features whose earlier result for the same inputs was reused, e.g. a value set back) and features that now fail. | @@ -187,8 +187,7 @@ Resources: `solidcraft://design` (inspect JSON with measurements) and `solidcraf Errors: an unknown tool or arguments that don't match the tool's schema are JSON-RPC `-32602` errors (an unknown argument's error lists the tool's arguments); an engine failure (bad command parameters, a feature that fails) is a tool result with `isError: true` and the -engine's message, which the model can read and correct. Tool results that are JSON are compact -(not pretty-printed). +engine's message, which the model can read and correct. ### Long-running requests diff --git a/skills/solidcraft-modeling/reference.md b/skills/solidcraft-modeling/reference.md index f847e47f87..27624cdfe1 100644 --- a/skills/solidcraft-modeling/reference.md +++ b/skills/solidcraft-modeling/reference.md @@ -133,7 +133,7 @@ After `set_parameter L=120` the volume is 35335.2 mm³, and the holes stay in th |---|---| | `list_commands {}` | **Over 100 KB.** Never call it unfiltered; filter on a distinctive word such as `"hole"` or `"slot"`. | | `body_topology` | 25-45 KB | -| `inspect_design {}` | 12 KB; sketches are summaries (counts, DOF, plane) unless named in `sketches` | +| `inspect_design {}` | 12 KB, and it grows with the sketches | | `inspect_design {"measure":false,"sketches":false}` | 4 KB: parameters, timeline errors, bodies | | `sketch.inspect` | 5 KB per small sketch | | `parameters.list {"filter":"feature"}` | 3 KB |