Check a generator's yield at the yield that produced it

The worker held each yield one behind, because the last one is the node's
result when the generator returns nothing of its own. Only the engine knows
what ports a node declared, so the check happened when the *next* yield
arrived — a pass late, which for a training loop is however long one epoch
takes.

The worker now sends every yield as it happens and returns whatever its
generator returned; EmitSink holds the last one back and decides at the end
of the call what it was. Old "emit" frames are still handled, so a remote
agent that has not been restarted keeps working.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-26 22:39:22 +02:00
co-authored by Claude Opus 5
parent f2dd1ffc34
commit 647644ebbd
5 changed files with 170 additions and 57 deletions
+8 -1
View File
@@ -333,12 +333,19 @@ class Node:
return self._to_messages(result)
def _drain(self, generator: Iterator[Any]) -> Any:
"""Publish each yield as it happens; the end of it is the result."""
"""Publish each yield as it happens; the end of it is the result.
The last yield is the result when the generator returns nothing of its
own, and which one is last is only known once it ends — so a value is
published one behind but *checked* the moment it arrives, which is what
fails a mistyped port at the yield that produced it.
"""
pending: Any = None
have_pending = False
try:
while True:
value = next(generator)
self._to_messages(value)
if have_pending:
self.emit(pending)
pending, have_pending = value, True