Skip to content

CxODEV-1884: carry a diagnostic message on the structured_error contract - #218

Merged
ognjenkatic merged 1 commit into
masterfrom
CxODEV-1884-message
Aug 26, 2026
Merged

CxODEV-1884: carry a diagnostic message on the structured_error contract#218
ognjenkatic merged 1 commit into
masterfrom
CxODEV-1884-message

Conversation

@ognjenkatic

@ognjenkatic ognjenkatic commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator

Closes CxODEV-1884.

Problem

StructuredErrorException carries only code and reason. A worker that needs both a short, stable reason and a detailed explanation has nowhere to put the detail, so it gets folded into reason. That makes reason unusable as something consumers can match on, and leaves them with no separate diagnostic field.

Change

StructuredError gains a Message property, populated from the exception.

No Message property is added to the exception. It already has one by virtue of being an Exception; the two new constructors set it through base(message ?? reason). Adding a shadowing property would have split one name across two values selected by static type — the catch block reads exception.Message off the base type three lines from where it narrows to StructuredErrorException.

The detail has to live inside the structured_error subtree to be reachable at all. TryParse presence-checks that single key and deserializes nothing else, so consumers never see the sibling error_message when a structured payload is present.

Parameter order

message trails referenceError rather than following reason. Overload resolution cannot choose between (code, reason, referenceError, message) and the existing (code, reason, referenceError, innerException) when the fourth argument is an untyped null:

error CS0121: The call is ambiguous between the following methods or properties:
'StructuredErrorException(string, string, string, Exception)' and
'StructuredErrorException(string, string, string, string)'

With message in third position that fires on (code, reason, message, null) — a message with no drill-down URI, which is the common case. Putting the nullable parameter third instead leaves only (code, reason, referenceError, null) ambiguous, an explicitly-null inner exception that the three-argument constructor already expresses. The reasoning is in the XML doc so it does not get "fixed" back.

Backward compatibility

  • The mapping emits message only when it differs from reason, so payloads from existing call sites are byte-identical.
  • NullValueHandling.Ignore omits the key entirely when unset; MissingMemberHandling defaults to Ignore, so older consumers skip it without throwing.
  • CurrentVersion stays at 1. The field is purely additive, and bumping it would be harmful — TryParse ignores version but a consumer may not.
  • Existing constructors are untouched; new ones are added as overloads. Editing the existing signature to insert message would have silently rebound every three-argument call site, sending the referenceError URI into message with no compiler complaint.

Deduplication

Both execution managers carried a hand-copied exception-to-payload mapping, and the test mirrored it rather than calling it — so a new field could pass tests while being silently dropped by the type-poll path. Extracted into StructuredError.FromException; both managers and the tests now call it.

Verification

62/62 tests pass (dotnet test ConductorSharp.sln), full solution builds with no new warnings. New coverage: message present under the snake_case key, omitted when unsupplied, omitted when it only repeats reason, surviving the TryParse round trip, reaching error_message/ReasonForIncompletion, and a message-only payload still degrading to the generic path.

Related

Ships alongside #219, which backports the identical patch to the 3.8 line (3.8.1) for consumers pinned there. The five touched files were byte-identical between v3.8.0 and master.

Note for signal-based producers: StructuredErrorSerializer.ToOutputData picks the new field up automatically, but callers that build the payload themselves must populate it.

🤖 Generated with Claude Code

StructuredErrorException supported only code and reason, so callers that
need both a short, stable reason and a detailed explanation had nowhere to
put the detail and had to fold it into reason. That defeats matching on
reason downstream, and leaves consumers with no separate diagnostic field.

Add StructuredError.Message, populated from the exception. No Message
property is added to the exception itself -- it already has one by virtue
of being an Exception, and the new constructors set it via base(). The
mapping only emits message when it differs from reason, so payloads from
existing call sites are byte-identical and the field is omitted entirely
(NullValueHandling.Ignore), keeping the shape at version 1.

message trails referenceError in the new constructors rather than
following reason. Overload resolution cannot pick between
(code, reason, referenceError, message) and the existing
(code, reason, referenceError, innerException) when the fourth argument is
an untyped null -- and (code, reason, message, null), a message with no
drill-down URI, is the common case. Placing the nullable parameter third
leaves only (code, reason, referenceError, null) ambiguous, which the
three-argument constructor already expresses.

Both execution managers had a hand-copied exception-to-payload mapping and
the test mirrored rather than called it, so a new field could pass tests
while being silently dropped by the type-poll path. Extract the mapping
into StructuredError.FromException and point all three at it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@ognjenkatic
ognjenkatic merged commit 0df08c6 into master Aug 26, 2026
1 check passed
@ognjenkatic
ognjenkatic deleted the CxODEV-1884-message branch August 26, 2026 11:23
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