Skip to content

Accept interface send type with a concrete implementing target in type validation - #1058

Open
PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:workflow-typecompat-interface-send
Open

PratikDhanave (PratikDhanave) wants to merge 1 commit into
microsoft:mainfrom
PratikDhanaveFork:workflow-typecompat-interface-send

Conversation

@PratikDhanave

Copy link
Copy Markdown
Contributor

Problem

sendTypeCompatibleWithInput (workflow/builder.go, used by validateTypeCompatibility in Build()) checked:

return outType == inType || outType.AssignableTo(inType) ||
    (inType.Kind() == reflect.Interface && outType.Implements(inType))

but not the symmetric inType.AssignableTo(outType). The validator's contract is to reject an edge only when the source's send type-set and the target's accept type-set can never share a concrete value — an overlap relation that is symmetric. When the source declares an interface send type (e.g. Shape) and the target accepts a concrete type that implements it (e.g. *Circle), none of the clauses match, so Build() fails with:

type incompatibility between executors "source" -> "target": source sends [Shape] but target accepts [*Circle]

But the edge is runtime-valid: the source may emit a concrete *Circle (a legal member of its declared Shape set), and the router resolves it to the *Circle handler. The mirror direction (concrete send → interface target) already builds. This blocks the common message.Content-interface source → *message.TextContent concrete target pattern.

Fix

Add the symmetric inType.AssignableTo(outType) clause. It does not over-accept — two unrelated concrete types are still non-assignable in both directions (verified: the existing RejectsIncompatibleDeclaredSendTypes test still passes). The removed third clause was already redundant with outType.AssignableTo(inType).

Test

TestBuilder_Validation_TypeCompatibility_InterfaceSendConcreteTarget builds a graph where the source declares an interface send type and the target accepts a concrete implementer, asserting Build() succeeds. Fails before the fix (rejected), passes after; existing compatibility tests remain green.

…e validation

sendTypeCompatibleWithInput checked outType==inType, outType.AssignableTo(inType),
and (inType is interface && outType implements inType), but not the symmetric
inType.AssignableTo(outType). Type-set overlap is symmetric, so when the source
declares an interface send type and the target accepts a concrete type that
implements it, the edge is runtime-valid (the source may emit that concrete
value, and the router resolves it to the concrete handler) - yet Build()
rejected it with a type incompatibility error. This hits the common
message.Content-source to *message.TextContent-target pattern.

Add inType.AssignableTo(outType); it does not over-accept (two unrelated
concrete types remain non-assignable either direction), and the third clause it
replaces was already redundant with outType.AssignableTo(inType).
Copilot AI lite review requested due to automatic review settings September 13, 2026 10:56
@github-actions github-actions Bot added area:workflow Changes files in the workflow area size:small At most 30 changed lines across at most 2 files labels Sep 13, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Runtime routing does not yet support the newly accepted edge, and additional assignability and interface-overlap cases remain unresolved.

Get a fresh assessment by requesting another Copilot review.

Pull request overview

Updates workflow type validation to allow interface-declared sends to concrete implementing targets.

Changes:

  • Adds symmetric assignability checking.
  • Adds regression coverage for interface-to-concrete compatibility.
File summaries
File Summary
workflow/builder.go Updates type compatibility logic.
workflow/builder_test.go Adds interface compatibility coverage.
Review details

Suppressed comments (2)

workflow/builder.go:476

  • The reverse assignability check is too broad for non-interface types. For example, with a source declaration of chan<- int and a target handler for chan int, inType.AssignableTo(outType) is true because bidirectional channels can be assigned to send-only channels, so this accepts the edge even though the source runtime value is not assignable to the target's concrete handler and will be dropped by routing. Restrict the reverse check to the intended interface-send case.
	return outType == inType || outType.AssignableTo(inType) || inType.AssignableTo(outType)

workflow/builder.go:476

  • Bidirectional assignability is not a complete implementation of the stated type-set-overlap contract for interface pairs. A source interface{ Read() } and target interface{ Write() } can share a concrete type implementing both methods, yet neither interface is assignable to the other and this still rejects the edge. Add an interface-overlap rule (including conflicting method signatures) and a regression test, or narrow the documented contract to only interface/concrete compatibility.
	return outType == inType || outType.AssignableTo(inType) || inType.AssignableTo(outType)
  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread workflow/builder.go
// target input type is assignable to the sent type - e.g. an interface send
// type with a concrete target that implements it (source may emit that
// concrete value).
return outType == inType || outType.AssignableTo(inType) || inType.AssignableTo(outType)
@github-actions github-actions Bot added kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure pending-auto-risk and removed pending-auto-risk labels Sep 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Scope: user-visible behavior (workflow Build() edge-validation outcome); no exported API surface changed.

Changed Go contract: sendTypeCompatibleWithInput (unexported, workflow/builder.go), used by validateTypeCompatibility inside the exported Builder.Build(). Behavior change: a source declaring an interface send type (e.g. Shape) and a target accepting a concrete implementing type (e.g. *Circle) now builds successfully instead of failing with a type-incompatibility error. No exported identifiers were added, removed, or renamed.

Upstream evidence reviewed:

  • Python: python/packages/core/agent_framework/_workflows/_typing_utils.py, is_type_compatible() (case 4, ~line 480-483): return issubclass(source_type, target_type) — Python's structural/nominal compatibility check is inherently directional-but-broad via issubclass, and since Python duck-types interfaces via ABC/Protocol subclassing, a concrete target type that is a subclass of (or structurally compatible with) the declared source type builds successfully. This directly parallels the new Go inType.AssignableTo(outType) clause: both let a source-declared abstract/interface type route to a concrete target that "is-a" instance of that abstraction.
  • Python: python/packages/core/agent_framework/_workflows/_validation.py, _validate_edge_type_compatibility() (~line 211-277), which calls is_type_compatible(source_type, target_type) per source/target type pair — confirms compatibility is evaluated per declared type pair exactly as the Go sendTypeCompatibleWithInput helper does per (outType, inType) pair in validateTypeCompatibility.
  • .NET: dotnet/src/Microsoft.Agents.AI.Workflows/WorkflowBuilder.cs (~line 587-601) explicitly documents that .NET does not perform this build-time type-compatibility validation at all (async executor factories and conditional/target-selector routing make it infeasible), deferring entirely to runtime routing via MessageRouter.CanHandle/FindHandler (Execution/MessageRouter.cs). This confirms .NET has no analogous static gate to diverge from, and the Go/Python behavior of accepting a valid interface→concrete edge is a strictly Go/Python-side concern.
  • Go runtime evidence: workflow/executor.go (routeMessage) dispatches by the concrete runtime type of the emitted message, so a source declaring an interface send type but emitting a concrete implementer was always able to route successfully at runtime — the previous build-time validator was overly strict relative to actual runtime behavior, mirroring the motivation behind Python's issubclass-based check.

Result: aligned. The fix restores the symmetric type-set-overlap semantics implemented in Python's is_type_compatible and removes a build-time false rejection that had no runtime counterpart; .NET has no equivalent static validation to diverge from. No exported Go API surface changed (helper and test additions are unexported), so no public-api-change label is needed. No parity issues found.

Generated by Go API Consistency Review Agent · copilot · auto · 123.8 AIC · ⌖ 5.78 AIC · ⊞ 9.6K ·

@github-actions github-actions Bot added the parity-approved Go API consistency review found no parity issues label Sep 13, 2026

@qmuntal Quim Muntal (qmuntal) left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PratikDhanave (@PratikDhanave) fix copilot findings. Also, do this without me having to say this on every existing and future PR please 😸 .

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:workflow Changes files in the workflow area kind:code Changes production behavior or code kind:tests Changes tests, fixtures, or test infrastructure parity-approved Go API consistency review found no parity issues size:small At most 30 changed lines across at most 2 files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants