A run keeps the inputs its flow declared at submit
Run.declared (migration 2e7ca7d10044) snapshots the declarations on submit and import. The run detail's Inputs panel reads them, and a run from before falls back to the flow's current declarations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WgBfkm4rCmtd1T4vHWjEPx
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
"""run.declared
|
||||
|
||||
The inputs the flow declared when a run was submitted, so its record reads
|
||||
against that version of the flow and not against whatever the flow declares
|
||||
now.
|
||||
|
||||
Revision ID: 2e7ca7d10044
|
||||
Revises: 930ffa094aad
|
||||
Create Date: 2026-10-15
|
||||
|
||||
"""
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "2e7ca7d10044"
|
||||
down_revision = "930ffa094aad"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
# Nullable: a run submitted before this kept no snapshot, and its readers
|
||||
# fall back to the flow's current declarations.
|
||||
op.add_column("run", sa.Column("declared", sa.JSON(), nullable=True))
|
||||
|
||||
|
||||
def downgrade():
|
||||
op.drop_column("run", "declared")
|
||||
@@ -210,6 +210,10 @@ class RunRow(BaseModel):
|
||||
class RunDetail(RunRow):
|
||||
result: dict[str, Any] = Field(default_factory=dict)
|
||||
flow_version: int = 1
|
||||
#: The inputs the flow declared when this was submitted, with their
|
||||
#: declared values. Null for a run from before this was kept, which reads
|
||||
#: against the flow's current declarations instead.
|
||||
declared: dict[str, Any] | None = None
|
||||
nodes: list[RunNodeRow] = Field(default_factory=list)
|
||||
artifacts: list[ArtifactRow] = Field(default_factory=list)
|
||||
|
||||
|
||||
@@ -454,6 +454,14 @@ def seed_values(
|
||||
return values
|
||||
|
||||
|
||||
def declared_inputs(flow: FlowDef) -> dict[str, Any]:
|
||||
"""What a flow declares it can be given, by name, with the declared values.
|
||||
|
||||
Kept on each run, since the flow can change after it.
|
||||
"""
|
||||
return {one.spec.name: one.initial for one in flow.inputs}
|
||||
|
||||
|
||||
def collect_result(flow: FlowDef, state: StateBackend) -> dict[str, Any]:
|
||||
"""What the run produced, keyed by message name without the flow prefix."""
|
||||
prefix = f"{flow.name}."
|
||||
@@ -925,6 +933,7 @@ class RunService:
|
||||
code_digest=run_digest(flow),
|
||||
params=params,
|
||||
passed=named if passed is None else passed,
|
||||
declared=declared_inputs(flow),
|
||||
params_digest=digest_of(params, seed),
|
||||
seed=seed,
|
||||
group_id=group_id,
|
||||
@@ -1047,6 +1056,7 @@ class RunService:
|
||||
flow_version=flow.version,
|
||||
params=params,
|
||||
passed=passed,
|
||||
declared=declared_inputs(flow),
|
||||
params_digest=digest_of(params, seed),
|
||||
seed=seed,
|
||||
group_id=entry["group_id"],
|
||||
|
||||
@@ -362,6 +362,9 @@ class Run(SQLModel, table=True):
|
||||
#: The names in `params` the caller chose; the rest are the flow's declared
|
||||
#: values. Null for a run recorded before this was kept.
|
||||
passed: list[str] | None = Field(sa_column=Column(JSON), default=None)
|
||||
#: The inputs the flow declared when this was submitted, by name, with
|
||||
#: their declared values. Null for a run recorded before this was kept.
|
||||
declared: dict[str, Any] | None = Field(sa_column=Column(JSON), default=None)
|
||||
#: sha256 over params and seed. Two runs of the same thing share it, which
|
||||
#: is what makes "have I already run this?" a lookup.
|
||||
params_digest: str = Field(default="", index=True, max_length=64)
|
||||
|
||||
@@ -456,6 +456,9 @@ def test_a_run_records_the_inputs_it_actually_starts_from():
|
||||
assert spelled.params_digest == defaulted.params_digest
|
||||
# Only what the caller chose says which value was the flow's own.
|
||||
assert (defaulted.passed, spelled.passed) == (["epochs"], ["epochs", "lr"])
|
||||
# And what the flow declared then, so an input it adds later is not
|
||||
# drawn on this run as a default it never received.
|
||||
assert defaulted.declared == {"lr": 0.01, "epochs": None}
|
||||
finally:
|
||||
with Session(db_engine) as session:
|
||||
for run in session.exec(select(Run).where(col(Run.id).in_(made))).all():
|
||||
|
||||
@@ -183,7 +183,8 @@ cached stages. Cancel active runs before retrying them.
|
||||
|
||||
The **Runs** screen filters by flow, status or sweep. Each run shows its
|
||||
parameters, per-node timings and logs, traceback, metrics, artifacts and
|
||||
result.
|
||||
result. Its inputs are listed as the flow declared them when the run was
|
||||
submitted, and the ones it took from the flow are marked as defaults.
|
||||
|
||||
Select two or more runs to compare curves and the parameters that differ.
|
||||
Plot against step, elapsed seconds or another metric from the same runs.
|
||||
|
||||
@@ -3498,6 +3498,18 @@ export const RunDetailSchema = {
|
||||
title: 'Flow Version',
|
||||
default: 1
|
||||
},
|
||||
declared: {
|
||||
anyOf: [
|
||||
{
|
||||
additionalProperties: true,
|
||||
type: 'object'
|
||||
},
|
||||
{
|
||||
type: 'null'
|
||||
}
|
||||
],
|
||||
title: 'Declared'
|
||||
},
|
||||
nodes: {
|
||||
items: {
|
||||
'$ref': '#/components/schemas/RunNodeRow'
|
||||
|
||||
@@ -1189,6 +1189,9 @@ export type RunDetail = {
|
||||
[key: string]: unknown;
|
||||
};
|
||||
flow_version?: number;
|
||||
declared?: ({
|
||||
[key: string]: unknown;
|
||||
} | null);
|
||||
nodes?: Array<RunNodeRow>;
|
||||
artifacts?: Array<ArtifactRow>;
|
||||
};
|
||||
|
||||
@@ -44,7 +44,7 @@ export function RunDetail({ id }: { id: string }) {
|
||||
const cancel = useCancelRun()
|
||||
const retry = useRetryRun()
|
||||
const names = useMetricNames(id)
|
||||
const { declared, gone } = useFlowInputs(run?.flow)
|
||||
const { declared: current, gone } = useFlowInputs(run?.flow)
|
||||
const [metric, setMetric] = useState("")
|
||||
|
||||
if (isPending || !run) return <Skeleton className="h-96 w-full rounded-lg" />
|
||||
@@ -58,6 +58,11 @@ export function RunDetail({ id }: { id: string }) {
|
||||
// Said once, where it applies.
|
||||
const cached = nodes.some((node) => node.status === "cached")
|
||||
|
||||
// What the flow declared when this run was submitted. A run from before
|
||||
// that was kept reads against the flow's current declarations.
|
||||
const declared = run.declared
|
||||
? new Map(Object.entries(run.declared))
|
||||
: current
|
||||
// Declared order first, so the panel reads the way the flow does; anything
|
||||
// the run carries that the flow no longer declares still shows, since it is
|
||||
// what the run was actually given.
|
||||
@@ -173,7 +178,7 @@ export function RunDetail({ id }: { id: string }) {
|
||||
<h2 className="font-medium text-sm">Inputs</h2>
|
||||
{fed.length === 0 ? (
|
||||
<p className="text-muted-foreground text-sm">
|
||||
{gone
|
||||
{gone && !run.declared
|
||||
? "The run carried no parameters of its own, and its flow is gone, so what it took from the flow cannot be read back."
|
||||
: "This flow declares no inputs, so there was nothing to choose."}
|
||||
</p>
|
||||
|
||||
@@ -187,9 +187,9 @@ const isMissing = (error: unknown) =>
|
||||
* ones it names and takes the flow's own for the rest — so this is what turns
|
||||
* `run.params` from "what was passed" into "what the run actually ran with".
|
||||
*
|
||||
* The declarations are the flow's *current* ones, while a run carries the
|
||||
* `flow_version` it was submitted against. An input added since is shown on an
|
||||
* older run as a default it never actually received. And the flow may not be
|
||||
* The declarations are the flow's *current* ones. A run keeps the ones it was
|
||||
* submitted against in `run.declared`, and the run detail reads those, so this
|
||||
* is the fallback for a run from before that was kept. And the flow may not be
|
||||
* there at all: deleting one leaves its runs standing, so `gone` is an ordinary
|
||||
* answer here rather than a fault, and the screen says so instead of drawing a
|
||||
* run that declares nothing.
|
||||
|
||||
Reference in New Issue
Block a user