Skip to content

Unify the GraphQL execution seam: one result type, one protocol #354

Description

@Danielmoraisg

Context

PR #349 introduced GraphQL partial-success handling for automations.executionMetrics: a response can carry readable data alongside per-node errors (e.g. PERMISSION_DENIED) in one payload, and we want one denied automation not to drop the rest of the batch.

That PR modelled "partial" as a second capability on the execution seam:

  • two execute verbs on the adapter: execute_query (all-or-nothing, returns dict) and execute_query_allow_partial (returns PartialQueryResult)
  • two protocols: GraphQLExecutor and PartialGraphQLExecutor extends GraphQLExecutor

A follow-up already landed on that PR (Rung 1): the fatal-vs-partial decision was de-smeared. execute_query_allow_partial no longer classifies. It always returns a PartialQueryResult (a fully null response yields data={}), and the single "no readable data means failure" decision now lives in one pure function, _normalize_execution_metrics_result. This issue tracks the remaining structural cleanup.

Problem

Modelling partial-success as a separate transport capability is a false dichotomy. Every GraphQL response can carry data and errors together; all-or-nothing is just "get the result, then raise if any errors." Baking that split into the seam means:

  • the seam has two verbs and two protocols where one of each would do
  • PartialGraphQLExecutor is a second Protocol with exactly one implementation and one test fake, i.e. speculative interface segregation
  • the next query that needs partial handling must widen its service to PartialGraphQLExecutor and reach for the second verb, instead of the pattern being the default

Proposed change (Rung 2)

Collapse to one result type and one protocol.

  • The primitive (e.g. execute returning GraphQLResult{data, errors}) is the only thing the seam really exposes. It performs the effect and never decides what the errors mean. Genuine transport/network failures still raise.
  • execute_query stays as the raise-on-error convenience the other ~152 tools use, reimplemented as a thin wrapper: run the primitive, raise if errors, return data.
  • Delete PartialGraphQLExecutor. Services depend on the one protocol; a service that wants partials reads errors off the result and hands it to its own pure classifier.
  • Each domain that needs partial handling owns a pure classifier (as observability already does with _normalize_execution_metrics_result).

The _on_graphql_error hook (InternalApiClient's [code=...][correlation_id=...] envelope) lands cleanly in this shape: the raising wrapper applies it; the primitive returns raw error dicts.

Why later, not now

This touches the shared executor contract and every caller of execute_query, so it deserves its own PR and review rather than riding on the execution-metrics feature. Rung 1 is a strict subset of this, so nothing here is blocked by having shipped it first.

Acceptance

  • one execution protocol; PartialGraphQLExecutor removed
  • one primitive returning data + errors; execute_query reimplemented over it with unchanged external behavior for existing callers
  • _on_graphql_error behavior preserved for InternalApiClient
  • observability partial-success path unchanged from a caller's perspective; full SDK + MCP suites green

References

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions