Refuse what a node cannot publish, and stop timing out work that is fine

Four things the python SDK turned up, each fixed where every client sees it.

A key no port declares is now an error rather than a silent drop, on the
return, the yield and the emit alike — the contract the docs already stated.
The SDK reads literal yields at sync time, so a typo fails before anything
runs, and an emission of one fails the call rather than being logged where
nobody looks.

NaN and infinity are refused at the port. JSON cannot spell either, so one
that travelled came back as a 500, a socket frame that stopped the canvas, or
a metric batch the database dropped whole.

An artifact input takes `@run:<id>.<output>` or a bare digest, resolved on the
engine — so the CLI, the run dialog and a python caller mean the same thing,
and a sweep can pass one at all.

Node timeouts are off by default. The clock measured silence, which a training
node is full of, and remote workers had already stopped enforcing it — their
heartbeat reset it. Now a heartbeat proves the agent rather than the node,
ninety seconds of nothing fails the call either way, and the engine touches
work it is still running so a long node is not redelivered at sixty seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019V5bsYGNxcgPs4xXmTPx69
This commit is contained in:
2026-08-25 07:30:14 +02:00
co-authored by Claude Opus 5
parent c33fa404a4
commit 93374a310e
32 changed files with 968 additions and 67 deletions
+80
View File
@@ -236,3 +236,83 @@ def test_the_decorators_leave_the_function_alone():
assert prepare(limit=2)["rows"] == 2
assert [step["loss"] for step in fit(None, 0.5, epochs=2)] == [1.0, 0.5]
assert evaluate(None) == 0.5
# -----------------------------------------------------------------------------
# What a function emits against what it declares
#
# The engine refuses an undeclared key at the first yield, which is right but
# late — a sweep can be an hour in. A literal one is a typo, and a typo is
# readable from the source.
# -----------------------------------------------------------------------------
def trains_with_a_typo(steps=3):
for _ in range(steps):
yield {"lss": 0.5}
return {"final_loss": 0.5}
def trains(steps=3):
for _ in range(steps):
yield {"loss": 0.5}
return {"final_loss": 0.5}
def emits_a_typo(steps=3):
import fluksio
fluksio.emit(lss=0.5)
return {"final_loss": 0.5}
def yields_a_name_it_computes(steps=3):
for index in range(steps):
yield {f"loss_{index}": 0.5}
return {"final_loss": 0.5}
def trains_beside_a_helper(steps=3):
def every_pair():
yield {"internal": 1}
for _ in range(steps):
yield {"loss": 0.5}
return {"final_loss": 0.5}
def test_a_yielded_key_no_port_declares_is_refused():
with pytest.raises(SyncError, match="lss"):
node(provides=[Port("loss", "float"), Port("final_loss", "float")])(
trains_with_a_typo
)
def test_an_emitted_key_no_port_declares_is_refused():
with pytest.raises(SyncError, match="lss"):
node(provides=[Port("final_loss", "float")])(emits_a_typo)
def test_declared_keys_pass():
assert node(provides=[Port("loss", "float"), Port("final_loss", "float")])(trains)
def test_a_key_the_code_computes_is_left_to_the_engine():
"""Only literals are readable here; the rest is checked where it runs."""
assert node(provides=[Port("final_loss", "float")])(yields_a_name_it_computes)
def test_a_helper_defined_inside_the_node_is_not_the_nodes_ports():
assert node(provides=[Port("loss", "float"), Port("final_loss", "float")])(
trains_beside_a_helper
)
def test_a_negative_timeout_is_refused():
with pytest.raises(SyncError, match="0 or more"):
node(requires=["a"], timeout=-1)(one_default)
def test_a_zero_timeout_means_no_limit():
decorated = node(requires=["a"], timeout=0)(one_default)
assert decorated.__fluksio__.timeout == 0