Artifacts: bytes a node produced, addressed by their content

A checkpoint is not a message. DType.ARTIFACT carries a reference — digest,
size, media type, name — so everything on the wire stays JSON and thirty
megabytes never sit in Redis, which answers the vision's open binary-payload
question by narrowing it: inline codecs would only serve payloads too small to
be worth a round trip, and nothing asks for that.

The store is content-addressed rather than per-run, for three reasons that all
pay later: a sweep whose fifty configs share one preprocessed input stores it
once, a reference stays valid however it is passed around because it names
content instead of a location, and the digest is what a stage cache will
compare — so building it in now is what keeps that from being a change to the
message contract.

Node code calls fluksio.save_artifact/load_artifact and cannot tell whether it
is writing the engine's own directory or putting bytes over HTTP, which is
what will let the same flow run on a remote worker unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AD8SfVhzXBG2nAfFcVh3iD
This commit is contained in:
2026-08-18 17:09:27 +02:00
co-authored by Claude Fable 5
parent 9b48f1593e
commit d27704a2bc
14 changed files with 1481 additions and 93 deletions
+552 -62
View File
@@ -57,6 +57,59 @@ export const ApplyResultSchema = {
title: 'ApplyResult'
} as const;
export const ArtifactRefSchema = {
properties: {
digest: {
type: 'string',
title: 'Digest'
},
size: {
type: 'integer',
title: 'Size'
},
media_type: {
type: 'string',
title: 'Media Type'
},
name: {
type: 'string',
title: 'Name',
default: ''
}
},
type: 'object',
required: ['digest', 'size', 'media_type'],
title: 'ArtifactRef'
} as const;
export const ArtifactRowSchema = {
properties: {
name: {
type: 'string',
title: 'Name'
},
node: {
type: 'string',
title: 'Node'
},
digest: {
type: 'string',
title: 'Digest'
},
size: {
type: 'integer',
title: 'Size'
},
media_type: {
type: 'string',
title: 'Media Type'
}
},
type: 'object',
required: ['name', 'node', 'digest', 'size', 'media_type'],
title: 'ArtifactRow'
} as const;
export const Body_login_login_access_tokenSchema = {
properties: {
grant_type: {
@@ -320,7 +373,7 @@ export const ChannelSchema = {
export const DTypeSchema = {
type: 'string',
enum: ['float', 'int', 'str', 'bool', 'json', 'series', 'record', 'list'],
enum: ['float', 'int', 'str', 'bool', 'json', 'series', 'record', 'list', 'artifact'],
title: 'DType',
description: `Serializable payload types.
@@ -329,8 +382,13 @@ are declared shapes rather than "some JSON": a widget or a downstream node
knows what it is getting before anything runs, which is what lets the
dashboard picker offer a message and refuse a wrong binding.
Binary payloads (tensors, images) will arrive later as explicitly declared
codec fields; until then everything on the wire is JSON.`
Binary payloads tensors, checkpoints, images — travel as \`\`artifact\`\`:
the bytes go to the artifact store and the message carries a reference to
them. That keeps everything on the wire JSON, which is what the state
backend, the queue and the worker protocol all rely on, and it means a
thirty-megabyte checkpoint never sits in Redis. Inline codecs would only be
needed for payloads too small to be worth a round trip, and nothing asks
for that yet.`
} as const;
export const DashboardDef_InputSchema = {
@@ -642,6 +700,21 @@ export const FlowDef_InputSchema = {
type: 'integer',
title: 'Version',
default: 1
},
mode: {
type: 'string',
enum: ['live', 'batch'],
title: 'Mode',
description: 'A live flow reacts to what arrives: its subscriptions, schedules and webhooks run until it is stopped. A batch flow only runs when a run asks it to, from its inputs to its outputs, and is never activated.',
default: 'live'
},
outputs: {
items: {
type: 'string'
},
type: 'array',
title: 'Outputs',
description: 'Messages a batch run reports as its result, unqualified. Empty means every message the flow ends up holding.'
}
},
type: 'object',
@@ -679,6 +752,21 @@ export const FlowDef_OutputSchema = {
type: 'integer',
title: 'Version',
default: 1
},
mode: {
type: 'string',
enum: ['live', 'batch'],
title: 'Mode',
description: 'A live flow reacts to what arrives: its subscriptions, schedules and webhooks run until it is stopped. A batch flow only runs when a run asks it to, from its inputs to its outputs, and is never activated.',
default: 'live'
},
outputs: {
items: {
type: 'string'
},
type: 'array',
title: 'Outputs',
description: 'Messages a batch run reports as its result, unqualified. Empty means every message the flow ends up holding.'
}
},
type: 'object',
@@ -1220,6 +1308,49 @@ export const MessagesPublicSchema = {
title: 'MessagesPublic'
} as const;
export const MetricPointSchema = {
properties: {
step: {
type: 'integer',
title: 'Step'
},
ts: {
type: 'number',
title: 'Ts'
},
value: {
type: 'number',
title: 'Value'
}
},
type: 'object',
required: ['step', 'ts', 'value'],
title: 'MetricPoint'
} as const;
export const MetricSeriesSchema = {
properties: {
label: {
type: 'string',
title: 'Label'
},
points: {
items: {
items: {
type: 'number'
},
type: 'array'
},
type: 'array',
title: 'Points'
}
},
type: 'object',
required: ['label'],
title: 'MetricSeries',
description: 'The shape a chart widget already draws, so comparing runs is a binding.'
} as const;
export const ModulePackageSchema = {
properties: {
name: {
@@ -1347,7 +1478,26 @@ export const NodeDef_InputSchema = {
}
],
title: 'Timeout',
description: "Seconds this node's code may run before it is stopped. This covers the first call's imports, which can be much slower than the body. Above 60 the engine may deliver its work again while it is still running."
description: "Seconds this node's code may run before it is stopped. This covers the first call's imports, which can be much slower than the body. Above 60 the engine may deliver its work again while it is still running — in a batch run, which never redelivers, it is an idle timeout instead: silence this long is a kill."
},
device: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Device',
description: "Label of the worker this node's code must run on, such as 'gpu'. Empty means the engine's own workers. A run needing a label no attached worker carries waits rather than failing."
},
device_policy: {
type: 'string',
enum: ['require', 'prefer'],
title: 'Device Policy',
description: 'What to do when no worker carries `device`: wait for one, or run locally anyway.',
default: 'require'
}
},
type: 'object',
@@ -1417,7 +1567,26 @@ export const NodeDef_OutputSchema = {
}
],
title: 'Timeout',
description: "Seconds this node's code may run before it is stopped. This covers the first call's imports, which can be much slower than the body. Above 60 the engine may deliver its work again while it is still running."
description: "Seconds this node's code may run before it is stopped. This covers the first call's imports, which can be much slower than the body. Above 60 the engine may deliver its work again while it is still running — in a batch run, which never redelivers, it is an idle timeout instead: silence this long is a kill."
},
device: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Device',
description: "Label of the worker this node's code must run on, such as 'gpu'. Empty means the engine's own workers. A run needing a label no attached worker carries waits rather than failing."
},
device_policy: {
type: 'string',
enum: ['require', 'prefer'],
title: 'Device Policy',
description: 'What to do when no worker carries `device`: wait for one, or run locally anyway.',
default: 'require'
}
},
type: 'object',
@@ -1893,6 +2062,182 @@ export const RuleSchema = {
description: 'Which events go to which channels.'
} as const;
export const RunCreateSchema = {
properties: {
params: {
additionalProperties: true,
type: 'object',
title: 'Params'
},
seed: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Seed'
},
draft: {
type: 'boolean',
title: 'Draft',
default: false
}
},
type: 'object',
title: 'RunCreate'
} as const;
export const RunDetailSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
flow: {
type: 'string',
title: 'Flow'
},
status: {
type: 'string',
title: 'Status'
},
status_reason: {
type: 'string',
title: 'Status Reason'
},
cause: {
type: 'string',
title: 'Cause'
},
params: {
additionalProperties: true,
type: 'object',
title: 'Params'
},
params_digest: {
type: 'string',
title: 'Params Digest'
},
seed: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Seed'
},
group_id: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Group Id'
},
labels: {
items: {
type: 'string'
},
type: 'array',
title: 'Labels'
},
created_at: {
title: 'Created At'
},
started_at: {
title: 'Started At'
},
finished_at: {
title: 'Finished At'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
actor: {
type: 'string',
title: 'Actor'
},
result: {
additionalProperties: true,
type: 'object',
title: 'Result'
},
commit: {
type: 'string',
title: 'Commit',
default: ''
},
flow_version: {
type: 'integer',
title: 'Flow Version',
default: 1
},
nodes: {
items: {
'$ref': '#/components/schemas/RunNodeRow'
},
type: 'array',
title: 'Nodes'
},
artifacts: {
items: {
'$ref': '#/components/schemas/ArtifactRow'
},
type: 'array',
title: 'Artifacts'
}
},
type: 'object',
required: ['id', 'flow', 'status', 'status_reason', 'cause', 'params', 'params_digest', 'seed', 'group_id', 'labels', 'created_at', 'duration_ms', 'actor'],
title: 'RunDetail'
} as const;
export const RunNodeRowSchema = {
properties: {
node: {
type: 'string',
title: 'Node'
},
status: {
type: 'string',
title: 'Status'
},
attempt: {
type: 'integer',
title: 'Attempt'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
worker: {
type: 'string',
title: 'Worker'
},
error: {
type: 'string',
title: 'Error'
},
logs: {
type: 'string',
title: 'Logs'
}
},
type: 'object',
required: ['node', 'status', 'attempt', 'duration_ms', 'worker', 'error', 'logs'],
title: 'RunNodeRow'
} as const;
export const RunRequestSchema = {
properties: {
inputs: {
@@ -1906,63 +2251,6 @@ export const RunRequestSchema = {
title: 'RunRequest'
} as const;
export const RunRowSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
flow: {
type: 'string',
title: 'Flow'
},
source: {
type: 'string',
title: 'Source'
},
status: {
type: 'string',
title: 'Status'
},
started_at: {
type: 'string',
format: 'date-time',
title: 'Started At'
},
finished_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Finished At'
},
nodes: {
type: 'integer',
title: 'Nodes'
},
errors: {
type: 'integer',
title: 'Errors'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
deliveries: {
type: 'integer',
title: 'Deliveries'
}
},
type: 'object',
required: ['id', 'flow', 'source', 'status', 'started_at', 'nodes', 'errors', 'duration_ms', 'deliveries'],
title: 'RunRow'
} as const;
export const SecretNamesSchema = {
properties: {
data: {
@@ -2044,6 +2332,25 @@ export const SectionDef_OutputSchema = {
description: 'A grid of widgets under a heading.'
} as const;
export const SeriesAnswerSchema = {
properties: {
metric: {
type: 'string',
title: 'Metric'
},
lines: {
items: {
'$ref': '#/components/schemas/MetricSeries'
},
type: 'array',
title: 'Lines'
}
},
type: 'object',
required: ['metric'],
title: 'SeriesAnswer'
} as const;
export const SeriesPointSchema = {
properties: {
ts: {
@@ -2092,6 +2399,48 @@ export const ShareRequestSchema = {
title: 'ShareRequest'
} as const;
export const SweepCreateSchema = {
properties: {
runs: {
items: {
'$ref': '#/components/schemas/SweepEntry'
},
type: 'array',
title: 'Runs'
},
draft: {
type: 'boolean',
title: 'Draft',
default: false
}
},
type: 'object',
title: 'SweepCreate'
} as const;
export const SweepEntrySchema = {
properties: {
params: {
additionalProperties: true,
type: 'object',
title: 'Params'
},
seed: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Seed'
}
},
type: 'object',
title: 'SweepEntry'
} as const;
export const TokenSchema = {
properties: {
access_token: {
@@ -2635,6 +2984,147 @@ export const app__api__routes__messages__PublishRequestSchema = {
title: 'PublishRequest'
} as const;
export const app__api__routes__observability__RunRowSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
flow: {
type: 'string',
title: 'Flow'
},
source: {
type: 'string',
title: 'Source'
},
status: {
type: 'string',
title: 'Status'
},
started_at: {
type: 'string',
format: 'date-time',
title: 'Started At'
},
finished_at: {
anyOf: [
{
type: 'string',
format: 'date-time'
},
{
type: 'null'
}
],
title: 'Finished At'
},
nodes: {
type: 'integer',
title: 'Nodes'
},
errors: {
type: 'integer',
title: 'Errors'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
deliveries: {
type: 'integer',
title: 'Deliveries'
}
},
type: 'object',
required: ['id', 'flow', 'source', 'status', 'started_at', 'nodes', 'errors', 'duration_ms', 'deliveries'],
title: 'RunRow'
} as const;
export const app__api__routes__runs__RunRowSchema = {
properties: {
id: {
type: 'string',
title: 'Id'
},
flow: {
type: 'string',
title: 'Flow'
},
status: {
type: 'string',
title: 'Status'
},
status_reason: {
type: 'string',
title: 'Status Reason'
},
cause: {
type: 'string',
title: 'Cause'
},
params: {
additionalProperties: true,
type: 'object',
title: 'Params'
},
params_digest: {
type: 'string',
title: 'Params Digest'
},
seed: {
anyOf: [
{
type: 'integer'
},
{
type: 'null'
}
],
title: 'Seed'
},
group_id: {
anyOf: [
{
type: 'string'
},
{
type: 'null'
}
],
title: 'Group Id'
},
labels: {
items: {
type: 'string'
},
type: 'array',
title: 'Labels'
},
created_at: {
title: 'Created At'
},
started_at: {
title: 'Started At'
},
finished_at: {
title: 'Finished At'
},
duration_ms: {
type: 'number',
title: 'Duration Ms'
},
actor: {
type: 'string',
title: 'Actor'
}
},
type: 'object',
required: ['id', 'flow', 'status', 'status_reason', 'cause', 'params', 'params_digest', 'seed', 'group_id', 'labels', 'created_at', 'duration_ms', 'actor'],
title: 'RunRow',
description: 'A run without its result, which is the part that can be large.'
} as const;
export const app__flow__schemas__MessageValueSchema = {
properties: {
value: {