Accept interface send type with a concrete implementing target in type validation - #1058
Conversation
…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).
There was a problem hiding this comment.
🟡 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<- intand a target handler forchan 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 targetinterface{ 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.
| // 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) |
|
Scope: user-visible behavior (workflow Changed Go contract: Upstream evidence reviewed:
Result: aligned. The fix restores the symmetric type-set-overlap semantics implemented in Python's
|
Quim Muntal (qmuntal)
left a comment
There was a problem hiding this comment.
PratikDhanave (@PratikDhanave) fix copilot findings. Also, do this without me having to say this on every existing and future PR please 😸 .
Problem
sendTypeCompatibleWithInput(workflow/builder.go, used byvalidateTypeCompatibilityinBuild()) checked: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, soBuild()fails with:But the edge is runtime-valid: the source may emit a concrete
*Circle(a legal member of its declaredShapeset), and the router resolves it to the*Circlehandler. The mirror direction (concrete send → interface target) already builds. This blocks the commonmessage.Content-interface source →*message.TextContentconcrete 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 existingRejectsIncompatibleDeclaredSendTypestest still passes). The removed third clause was already redundant withoutType.AssignableTo(inType).Test
TestBuilder_Validation_TypeCompatibility_InterfaceSendConcreteTargetbuilds a graph where the source declares an interface send type and the target accepts a concrete implementer, assertingBuild()succeeds. Fails before the fix (rejected), passes after; existing compatibility tests remain green.