Skip to content

Result Contract Stability

Kadyapam edited this page Sep 8, 2026 · 1 revision

ToolResult is #[non_exhaustive] — and why

Since noetl-tools v4.0.0 (noetl/ai-meta#330).

The change

ToolResult is sealed. Downstream crates can no longer build one with a struct literal:

// no longer compiles outside this crate
let r = ToolResult { status, data, error, /* … */ };

// use the constructors + builders instead
let r = ToolResult::error("boom")
    .with_data(json!({"k": "v"}))
    .with_exit_code(1)
    .with_duration(5)
    .with_pending_callback(true)
    .with_child_execution_id("42");

Every field is reachable through a builder — a test asserts it, because sealing without that would move the breakage from "downstream cannot compile" to "downstream cannot express what it needs".

What it prevents

Adding a public field to a struct that downstream crates construct by literal is a breaking change by Rust's rules — and nothing in the release pipeline knows that. semantic-release reads the commit prefix, so a feat: ships it as a minor.

That is exactly what 3.27.0 did with child_execution_id: it did not compile in noetl-executor, and the breakage was latent — waiting for whoever next resolved a caret range, far from the change that caused it.

One breaking release buys non-breaking field additions forever.

The guard, and why it is a source guard

tool_result_is_sealed_against_downstream_literals reads the source rather than exercising behaviour. It has to: the property is about what other crates may do, and inside the defining crate a literal is still legal — so a unit test passes with or without the attribute.

⚠ Do not "simplify" it into a substring search. The first version did exactly that, and the doc comment on ToolResult explains the attribute by name — so a mutation removing #[non_exhaustive] left the test green, matching prose. It now strips comment lines and requires the attribute on a line of its own.

Adding a field now

Adding a field is a minor. Add a with_* builder for it in the same change, or the field is unreachable from outside this crate.

Related

  • Central record: noetl/ai-meta#330.
  • child_execution_id itself: noetl/ai-meta#328 — the playbook tool records which execution it spawned, set on all return paths, carried beside data so the child's result contract is untouched.

Clone this wiki locally