fancy-flow 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fancy_flow/__init__.py +97 -0
- fancy_flow/capabilities/__init__.py +212 -0
- fancy_flow/contracts.py +62 -0
- fancy_flow/durable/__init__.py +51 -0
- fancy_flow/durable/coordinator.py +330 -0
- fancy_flow/durable/frontier.py +145 -0
- fancy_flow/durable/human.py +138 -0
- fancy_flow/durable/replay.py +126 -0
- fancy_flow/durable/retry.py +78 -0
- fancy_flow/durable/state.py +192 -0
- fancy_flow/engine/__init__.py +5 -0
- fancy_flow/engine/runner.py +399 -0
- fancy_flow/exceptions.py +49 -0
- fancy_flow/executors.py +189 -0
- fancy_flow/marketplace/__init__.py +28 -0
- fancy_flow/marketplace/manifest.py +425 -0
- fancy_flow/nodes/__init__.py +9 -0
- fancy_flow/nodes/ai.py +266 -0
- fancy_flow/nodes/data.py +97 -0
- fancy_flow/nodes/human.py +56 -0
- fancy_flow/nodes/io_.py +61 -0
- fancy_flow/nodes/logic.py +130 -0
- fancy_flow/nodes/output.py +24 -0
- fancy_flow/nodes/structural.py +240 -0
- fancy_flow/nodes/support/__init__.py +36 -0
- fancy_flow/nodes/support/clients.py +182 -0
- fancy_flow/nodes/support/deps.py +39 -0
- fancy_flow/nodes/support/expr.py +202 -0
- fancy_flow/nodes/support/structured.py +221 -0
- fancy_flow/nodes/trigger.py +44 -0
- fancy_flow/py.typed +0 -0
- fancy_flow/registry/__init__.py +21 -0
- fancy_flow/registry/builtin.py +1029 -0
- fancy_flow/registry/kind_id.py +78 -0
- fancy_flow/registry/node_kind.py +203 -0
- fancy_flow/registry/registry.py +160 -0
- fancy_flow/runtime/__init__.py +24 -0
- fancy_flow/runtime/abort.py +42 -0
- fancy_flow/runtime/context.py +90 -0
- fancy_flow/runtime/events.py +119 -0
- fancy_flow/runtime/identity.py +202 -0
- fancy_flow/runtime/options.py +63 -0
- fancy_flow/runtime/pause.py +119 -0
- fancy_flow/runtime/ports.py +30 -0
- fancy_flow/schema/__init__.py +16 -0
- fancy_flow/schema/graph.py +154 -0
- fancy_flow/schema/issues.py +65 -0
- fancy_flow/security/__init__.py +5 -0
- fancy_flow/security/policy.py +333 -0
- fancy_flow/workflow.py +207 -0
- fancy_flow-0.1.0.dist-info/METADATA +157 -0
- fancy_flow-0.1.0.dist-info/RECORD +54 -0
- fancy_flow-0.1.0.dist-info/WHEEL +4 -0
- fancy_flow-0.1.0.dist-info/licenses/LICENSE +21 -0
fancy_flow/__init__.py
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""fancy-flow for Python -- the third runtime for fancy-flow workflow graphs.
|
|
2
|
+
|
|
3
|
+
The guarantee, and the only thing that matters: **the same WorkflowSchema JSON
|
|
4
|
+
in produces the same RunResult.outputs out**, on Node, on PHP, and here. This
|
|
5
|
+
is a faithful PORT, not a redesign; behaviour questions are settled against
|
|
6
|
+
``@particle-academy/fancy-flow`` and ``particle-academy/fancy-flow-php``, and
|
|
7
|
+
parity is asserted by shared fixture tables rather than claimed.
|
|
8
|
+
|
|
9
|
+
Getting started::
|
|
10
|
+
|
|
11
|
+
from fancy_flow import FlowRunner, RunOptions, builtin, import_workflow
|
|
12
|
+
|
|
13
|
+
builtin.register() # install the built-in kinds
|
|
14
|
+
result = import_workflow(schema_json)
|
|
15
|
+
run = FlowRunner().run(
|
|
16
|
+
result.graph,
|
|
17
|
+
builtin.executors(),
|
|
18
|
+
options=RunOptions(initial_inputs={"trigger-1": {"payload": body}}),
|
|
19
|
+
)
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from . import capabilities
|
|
25
|
+
from .contracts import NativeResolver, NodeExecutor, Resolver, TriggerGuard
|
|
26
|
+
from .engine.runner import FlowRunner
|
|
27
|
+
from .exceptions import FlowError, RunAborted, UnsafeGraph
|
|
28
|
+
from .executors import ExecutorRegistry
|
|
29
|
+
from .registry import builtin
|
|
30
|
+
from .registry.node_kind import ConfigField, NodeKind
|
|
31
|
+
from .registry.registry import NodeKindRegistry, default_registry, reset_default_registry
|
|
32
|
+
from .runtime import (
|
|
33
|
+
AbortController,
|
|
34
|
+
AbortSignal,
|
|
35
|
+
ExecutionContext,
|
|
36
|
+
NodeStatus,
|
|
37
|
+
Pause,
|
|
38
|
+
PauseSignal,
|
|
39
|
+
Port,
|
|
40
|
+
RunEvent,
|
|
41
|
+
RunOptions,
|
|
42
|
+
RunResult,
|
|
43
|
+
)
|
|
44
|
+
from .schema import (
|
|
45
|
+
FlowEdge,
|
|
46
|
+
FlowGraph,
|
|
47
|
+
FlowNode,
|
|
48
|
+
ImportIssue,
|
|
49
|
+
ImportResult,
|
|
50
|
+
PortDescriptor,
|
|
51
|
+
WorkflowMetadata,
|
|
52
|
+
)
|
|
53
|
+
from .workflow import SCHEMA_URL, SCHEMA_VERSION, export_workflow, import_workflow, to_json
|
|
54
|
+
|
|
55
|
+
__version__ = "0.1.0"
|
|
56
|
+
|
|
57
|
+
__all__ = [
|
|
58
|
+
"SCHEMA_URL",
|
|
59
|
+
"SCHEMA_VERSION",
|
|
60
|
+
"AbortController",
|
|
61
|
+
"AbortSignal",
|
|
62
|
+
"ConfigField",
|
|
63
|
+
"ExecutionContext",
|
|
64
|
+
"ExecutorRegistry",
|
|
65
|
+
"FlowEdge",
|
|
66
|
+
"FlowError",
|
|
67
|
+
"FlowGraph",
|
|
68
|
+
"FlowNode",
|
|
69
|
+
"FlowRunner",
|
|
70
|
+
"ImportIssue",
|
|
71
|
+
"ImportResult",
|
|
72
|
+
"NativeResolver",
|
|
73
|
+
"NodeExecutor",
|
|
74
|
+
"NodeKind",
|
|
75
|
+
"NodeKindRegistry",
|
|
76
|
+
"NodeStatus",
|
|
77
|
+
"Pause",
|
|
78
|
+
"PauseSignal",
|
|
79
|
+
"Port",
|
|
80
|
+
"PortDescriptor",
|
|
81
|
+
"Resolver",
|
|
82
|
+
"RunAborted",
|
|
83
|
+
"RunEvent",
|
|
84
|
+
"RunOptions",
|
|
85
|
+
"RunResult",
|
|
86
|
+
"TriggerGuard",
|
|
87
|
+
"UnsafeGraph",
|
|
88
|
+
"WorkflowMetadata",
|
|
89
|
+
"__version__",
|
|
90
|
+
"builtin",
|
|
91
|
+
"capabilities",
|
|
92
|
+
"default_registry",
|
|
93
|
+
"export_workflow",
|
|
94
|
+
"import_workflow",
|
|
95
|
+
"reset_default_registry",
|
|
96
|
+
"to_json",
|
|
97
|
+
]
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
"""Host capabilities -- the services core nodes need but must never depend on.
|
|
2
|
+
|
|
3
|
+
A node that imports a provider SDK forces every consumer to install it: a
|
|
4
|
+
workflow app that never calls a model should not inherit an LLM dependency. So
|
|
5
|
+
core declares the CONTRACT and the host supplies the implementation.
|
|
6
|
+
|
|
7
|
+
Two ways in: the module-level setters below (so the framework-free core stays
|
|
8
|
+
usable with no container), or an adapter package that registers on the host's
|
|
9
|
+
behalf at startup.
|
|
10
|
+
|
|
11
|
+
Unlike the PHP twin there is **no auto-detection**. PHP can afford it because
|
|
12
|
+
``class_exists()`` is free; the Python equivalent is importing a candidate
|
|
13
|
+
package to find out whether it is there, which has side effects, costs start-up
|
|
14
|
+
time, and silently picks a provider the author never named. A missing client
|
|
15
|
+
therefore aborts the node with :func:`llm_unavailable_message`, which says what
|
|
16
|
+
to register -- an outcome the author can act on, rather than a guess they have
|
|
17
|
+
to discover.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from collections.abc import Callable
|
|
23
|
+
from dataclasses import dataclass
|
|
24
|
+
from typing import Any, Protocol, runtime_checkable
|
|
25
|
+
|
|
26
|
+
from ..schema.graph import FlowGraph
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"LlmClient",
|
|
30
|
+
"LlmRoute",
|
|
31
|
+
"LlmRouteChoice",
|
|
32
|
+
"LlmRouteRequest",
|
|
33
|
+
"WorkflowResolutionFailure",
|
|
34
|
+
"WorkflowResolver",
|
|
35
|
+
"llm_client",
|
|
36
|
+
"llm_unavailable_message",
|
|
37
|
+
"reset",
|
|
38
|
+
"set_llm_client",
|
|
39
|
+
"set_workflow_resolver",
|
|
40
|
+
"status",
|
|
41
|
+
"workflow_resolver",
|
|
42
|
+
]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True, slots=True)
|
|
46
|
+
class LlmRoute:
|
|
47
|
+
"""One route a model may choose between.
|
|
48
|
+
|
|
49
|
+
The description is what the model actually reads when deciding, so it is
|
|
50
|
+
the field that determines routing quality.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
port: str
|
|
54
|
+
description: str | None = None
|
|
55
|
+
|
|
56
|
+
@staticmethod
|
|
57
|
+
def from_dict(raw: dict[str, Any]) -> LlmRoute:
|
|
58
|
+
return LlmRoute(
|
|
59
|
+
port=str(raw.get("port") or "").strip(),
|
|
60
|
+
description=str(raw["description"]) if raw.get("description") is not None else None,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True, slots=True)
|
|
65
|
+
class LlmRouteRequest:
|
|
66
|
+
prompt: str
|
|
67
|
+
routes: tuple[LlmRoute, ...]
|
|
68
|
+
system: str | None = None
|
|
69
|
+
provider: str | None = None
|
|
70
|
+
model: str | None = None
|
|
71
|
+
credential: str | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True, slots=True)
|
|
75
|
+
class LlmRouteChoice:
|
|
76
|
+
"""The port the model picked, and why.
|
|
77
|
+
|
|
78
|
+
``reason`` travels with the value down the graph, so a completed run
|
|
79
|
+
explains itself without the model call being replayed.
|
|
80
|
+
"""
|
|
81
|
+
|
|
82
|
+
port: str
|
|
83
|
+
reason: str | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@runtime_checkable
|
|
87
|
+
class LlmClient(Protocol):
|
|
88
|
+
"""The only thing core asks of an LLM: given routes, pick one.
|
|
89
|
+
|
|
90
|
+
``llm_router`` is a shuttle, not an engine. It carries the routes out to
|
|
91
|
+
whatever the host registered and carries the choice back -- no provider
|
|
92
|
+
SDK, no prompt engineering, no response parsing, no retry policy. That is
|
|
93
|
+
what lets an opinionated node ship as a builtin without every consumer
|
|
94
|
+
inheriting an LLM dependency.
|
|
95
|
+
|
|
96
|
+
Implementations should constrain the model to the declared ports
|
|
97
|
+
(structured output / enum) rather than parsing a port name out of a
|
|
98
|
+
sentence.
|
|
99
|
+
"""
|
|
100
|
+
|
|
101
|
+
def choose_route(
|
|
102
|
+
self, request: LlmRouteRequest
|
|
103
|
+
) -> LlmRouteChoice: ... # pragma: no cover - protocol
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
@dataclass(frozen=True, slots=True)
|
|
107
|
+
class WorkflowResolutionFailure:
|
|
108
|
+
"""Why a ``subflow`` reference could not be honoured.
|
|
109
|
+
|
|
110
|
+
A version mismatch and a missing workflow want different errors: reporting
|
|
111
|
+
a mismatch as "not found" sends an author looking for a workflow that is
|
|
112
|
+
sitting right there.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
reason: str
|
|
116
|
+
available: int | None = None
|
|
117
|
+
message: str | None = None
|
|
118
|
+
|
|
119
|
+
VERSION_MISMATCH = "version-mismatch"
|
|
120
|
+
NOT_FOUND = "not-found"
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def is_version_mismatch(self) -> bool:
|
|
124
|
+
return self.reason == self.VERSION_MISMATCH
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@runtime_checkable
|
|
128
|
+
class WorkflowResolver(Protocol):
|
|
129
|
+
"""Resolve a workflow reference to a runnable graph.
|
|
130
|
+
|
|
131
|
+
``subflow`` NAMES another workflow rather than embedding it, so the host
|
|
132
|
+
owns where workflows live -- a database, a file, an API.
|
|
133
|
+
|
|
134
|
+
``version`` is a parameter rather than part of the reference string because
|
|
135
|
+
a stringly-typed protocol (``invoice-triage@3``) is one every host invents
|
|
136
|
+
differently. A workflow another workflow depends on is an INTERFACE, and
|
|
137
|
+
interfaces need pins: without one, a parent goes on calling
|
|
138
|
+
``invoice-triage``, someone edits that child, and the parent runs different
|
|
139
|
+
logic having reported success the whole time.
|
|
140
|
+
"""
|
|
141
|
+
|
|
142
|
+
def resolve(
|
|
143
|
+
self, ref: str, version: int | None = None
|
|
144
|
+
) -> FlowGraph | WorkflowResolutionFailure | None: ... # pragma: no cover - protocol
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# -- module state --------------------------------------------------------
|
|
148
|
+
|
|
149
|
+
_llm_client: LlmClient | None = None
|
|
150
|
+
_workflow_resolver: WorkflowResolver | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def set_llm_client(client: LlmClient | None) -> Callable[[], None]:
|
|
154
|
+
"""Install the host's LLM client. Returns an unregister callable."""
|
|
155
|
+
global _llm_client
|
|
156
|
+
_llm_client = client
|
|
157
|
+
|
|
158
|
+
def unregister() -> None:
|
|
159
|
+
global _llm_client
|
|
160
|
+
if _llm_client is client:
|
|
161
|
+
_llm_client = None
|
|
162
|
+
|
|
163
|
+
return unregister
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def llm_client() -> LlmClient | None:
|
|
167
|
+
return _llm_client
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def llm_unavailable_message() -> str:
|
|
171
|
+
"""Why no client is available, phrased as what to do about it."""
|
|
172
|
+
return (
|
|
173
|
+
"No LLM client is registered, so llm_router cannot ask a model which route to "
|
|
174
|
+
"take. Register one with fancy_flow.capabilities.set_llm_client(client) - any "
|
|
175
|
+
"object with choose_route(LlmRouteRequest) -> LlmRouteChoice will do."
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def set_workflow_resolver(resolver: WorkflowResolver | None) -> Callable[[], None]:
|
|
180
|
+
"""Install the host's workflow resolver. Returns an unregister callable."""
|
|
181
|
+
global _workflow_resolver
|
|
182
|
+
_workflow_resolver = resolver
|
|
183
|
+
|
|
184
|
+
def unregister() -> None:
|
|
185
|
+
global _workflow_resolver
|
|
186
|
+
if _workflow_resolver is resolver:
|
|
187
|
+
_workflow_resolver = None
|
|
188
|
+
|
|
189
|
+
return unregister
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def workflow_resolver() -> WorkflowResolver | None:
|
|
193
|
+
return _workflow_resolver
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def status() -> dict[str, bool]:
|
|
197
|
+
"""Which capabilities are currently satisfied.
|
|
198
|
+
|
|
199
|
+
Exists so a host -- or an agent over MCP -- can answer "what does this
|
|
200
|
+
graph need that I have not wired?" BEFORE a run fails halfway through.
|
|
201
|
+
"""
|
|
202
|
+
return {
|
|
203
|
+
"llm": _llm_client is not None,
|
|
204
|
+
"workflow_resolver": _workflow_resolver is not None,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def reset() -> None:
|
|
209
|
+
"""Clear everything. Test isolation."""
|
|
210
|
+
global _llm_client, _workflow_resolver
|
|
211
|
+
_llm_client = None
|
|
212
|
+
_workflow_resolver = None
|
fancy_flow/contracts.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""The seams a host plugs into.
|
|
2
|
+
|
|
3
|
+
Structural :class:`typing.Protocol` rather than nominal base classes, because a
|
|
4
|
+
host's existing service should be usable as an executor without inheriting from
|
|
5
|
+
us. That is the same freedom the PHP twin gets from accepting a callable, an
|
|
6
|
+
interface implementation, or a class-string interchangeably.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any, Protocol, runtime_checkable
|
|
12
|
+
|
|
13
|
+
from .runtime.context import ExecutionContext
|
|
14
|
+
|
|
15
|
+
__all__ = ["NodeExecutor", "Resolver", "TriggerGuard"]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@runtime_checkable
|
|
19
|
+
class NodeExecutor(Protocol):
|
|
20
|
+
"""Behaviour for one node kind.
|
|
21
|
+
|
|
22
|
+
An executor may equally be a plain callable taking the context; this
|
|
23
|
+
protocol exists for the class-shaped case, which is what constructor
|
|
24
|
+
injection wants.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def execute(self, ctx: ExecutionContext) -> Any: # pragma: no cover - protocol
|
|
28
|
+
...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@runtime_checkable
|
|
32
|
+
class Resolver(Protocol):
|
|
33
|
+
"""Turns a class into an instance.
|
|
34
|
+
|
|
35
|
+
The default calls the class with no arguments. A host with a DI container
|
|
36
|
+
supplies its own so executors get constructor injection -- the analogue of
|
|
37
|
+
the PHP twin's ``ContainerResolver``.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def make(self, cls: type) -> Any: # pragma: no cover - protocol
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@runtime_checkable
|
|
45
|
+
class TriggerGuard(Protocol):
|
|
46
|
+
"""The precondition a queued cohort run re-checks just before it starts.
|
|
47
|
+
|
|
48
|
+
Fails CLOSED by design: when the guard cannot answer, the run does not
|
|
49
|
+
start. Several runs fired by one event are serialized, and each re-asks
|
|
50
|
+
whether it still should happen -- because by the time the third one is
|
|
51
|
+
picked up, the first two may have made it wrong.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def should_run(self, run_key: str, context: dict[str, Any]) -> bool: # pragma: no cover
|
|
55
|
+
...
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class NativeResolver:
|
|
59
|
+
"""The default :class:`Resolver` -- constructs with no arguments."""
|
|
60
|
+
|
|
61
|
+
def make(self, cls: type) -> Any:
|
|
62
|
+
return cls()
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Durable, resumable runs -- with no queue library anywhere in sight.
|
|
2
|
+
|
|
3
|
+
The research behind this is written up in the envelope's
|
|
4
|
+
``.ai/plans/fancy-flow-py.md``; the short version is that a JSON-graph engine
|
|
5
|
+
wants **checkpoint-per-node keyed by node id**, not Temporal-style event-sourced
|
|
6
|
+
replay. Replay exists to police arbitrary user code for non-determinism; an
|
|
7
|
+
interpreter over a declarative graph is deterministic by construction, so the
|
|
8
|
+
sandbox, the history limits and the versioning tax buy nothing. And node-id
|
|
9
|
+
keying survives a graph being edited while a run is parked on an approval,
|
|
10
|
+
where an ordinal-keyed checkpoint cannot.
|
|
11
|
+
|
|
12
|
+
So durability lives here, in the pure core:
|
|
13
|
+
|
|
14
|
+
- :mod:`.state` what a run remembers, and the claim contract a database implements
|
|
15
|
+
- :mod:`.frontier` which nodes are unblocked, restated from the engine's own rule
|
|
16
|
+
- :mod:`.replay` run one node THROUGH the engine, never around it
|
|
17
|
+
- :mod:`.retry` how many attempts a node gets, per node
|
|
18
|
+
- :mod:`.human` gates that pause and cannot be walked past
|
|
19
|
+
- :mod:`.coordinator` the two operations a queue adapter dispatches
|
|
20
|
+
|
|
21
|
+
A queue adapter supplies transport and nothing else.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from .coordinator import Coordinator, DurableRunResult, NodeOutcome
|
|
25
|
+
from .frontier import Frontier, FrontierResult
|
|
26
|
+
from .human import DurableApproval, DurableUserInput, NotAwaitingHuman, Submissions
|
|
27
|
+
from .replay import BOUNDARY, ReplayResult, is_boundary, replay_up_to
|
|
28
|
+
from .retry import UNSAFE_TO_REPLAY, RetryPolicy
|
|
29
|
+
from .state import InMemoryClaimStore, NodeClaimStore, NodeRunStatus, NodeState
|
|
30
|
+
|
|
31
|
+
__all__ = [
|
|
32
|
+
"BOUNDARY",
|
|
33
|
+
"UNSAFE_TO_REPLAY",
|
|
34
|
+
"Coordinator",
|
|
35
|
+
"DurableApproval",
|
|
36
|
+
"DurableRunResult",
|
|
37
|
+
"DurableUserInput",
|
|
38
|
+
"Frontier",
|
|
39
|
+
"FrontierResult",
|
|
40
|
+
"InMemoryClaimStore",
|
|
41
|
+
"NodeClaimStore",
|
|
42
|
+
"NodeOutcome",
|
|
43
|
+
"NodeRunStatus",
|
|
44
|
+
"NodeState",
|
|
45
|
+
"NotAwaitingHuman",
|
|
46
|
+
"ReplayResult",
|
|
47
|
+
"RetryPolicy",
|
|
48
|
+
"Submissions",
|
|
49
|
+
"is_boundary",
|
|
50
|
+
"replay_up_to",
|
|
51
|
+
]
|