A node's numbers leave through its ports, not a logging call
The first cut had node code call fluksio.log_metric, which was a second, undeclared way for data to leave a node: invisible to validation, absent from the canvas, and stored where the graph could not see it. That is precisely the MLflow discrepancy this framework exists to avoid, so it is gone. A node that produces values over time is a generator. Every yield is a dict keyed by output port, published the instant it happens — same port, same type check, same place on the canvas as any other value — and what it returns is its result. A port doing this declares stream: true, and a run keeps every number one takes, so experiment tracking is a consequence of the graph rather than an API beside it: a chart binds to a training curve the way it binds to a temperature. fluksio.emit writes the same ports imperatively, for where a yield cannot reach — inside a training framework's callback. In a live flow an emission also wakes what is downstream, as a subscriber publishing does; in a run it does not, because a run's graph is scheduled once and mid-node cascades would leave 'finished' with nothing to mean. The enqueued item carries no payload: the value is already in state, and one carrying it would re-apply an old emission after the node returned. Verified on the stack: 30 loss values arrived live on the flow socket during a run, attributed to the node that produced them, and the same node run on the remote worker streamed its curve back across the socket. Also caches remote compile results per worker, so attaching a GPU box does not put a network round trip in every rebuild. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"
|
||||
import { Maximize2, Minimize2, X } from "lucide-react"
|
||||
import { Activity, Maximize2, Minimize2, X } from "lucide-react"
|
||||
import {
|
||||
type ComponentProps,
|
||||
lazy,
|
||||
@@ -216,6 +216,7 @@ function PortList({
|
||||
suggestions,
|
||||
onChange,
|
||||
onRenamed,
|
||||
streamable = false,
|
||||
}: {
|
||||
title: string
|
||||
specs: MessageSpec[]
|
||||
@@ -224,6 +225,8 @@ function PortList({
|
||||
suggestions: string[]
|
||||
onChange: (next: MessageSpec[]) => void
|
||||
onRenamed?: (previous: string, next: string) => void
|
||||
/** Outputs only: a port a node publishes on repeatedly while it runs. */
|
||||
streamable?: boolean
|
||||
}) {
|
||||
// The port just added, so its name field can take focus.
|
||||
const [freshIndex, setFreshIndex] = useState<number | null>(null)
|
||||
@@ -325,6 +328,25 @@ function PortList({
|
||||
update(index, { interval: Number(event.target.value) || 0 })
|
||||
}
|
||||
/>
|
||||
{streamable ? (
|
||||
<Button
|
||||
variant={spec.stream ? "secondary" : "ghost"}
|
||||
size="icon-sm"
|
||||
aria-label="Streaming output"
|
||||
aria-pressed={spec.stream ?? false}
|
||||
title={
|
||||
"Published repeatedly while the node runs — a curve rather " +
|
||||
"than a result. A run keeps every value it takes."
|
||||
}
|
||||
className={cn(
|
||||
"shrink-0",
|
||||
!spec.stream && "text-muted-foreground",
|
||||
)}
|
||||
onClick={() => update(index, { stream: !spec.stream })}
|
||||
>
|
||||
<Activity />
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
@@ -961,6 +983,7 @@ function PanelBody({
|
||||
emptyHint="Nothing yet. Add a message this node publishes."
|
||||
suggestions={suggestions.provides}
|
||||
onChange={(provides) => editNode({ ...node, provides })}
|
||||
streamable
|
||||
// Only the publishing side names a message; an input is as often
|
||||
// re-pointed at a different one as it is renamed.
|
||||
onRenamed={onRenameMessage}
|
||||
|
||||
Reference in New Issue
Block a user