AbstractRuntime 0.2.0__py3-none-any.whl → 0.4.1__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.
- abstractruntime/__init__.py +83 -3
- abstractruntime/core/config.py +82 -2
- abstractruntime/core/event_keys.py +62 -0
- abstractruntime/core/models.py +17 -1
- abstractruntime/core/policy.py +74 -3
- abstractruntime/core/runtime.py +3334 -28
- abstractruntime/core/vars.py +103 -2
- abstractruntime/evidence/__init__.py +10 -0
- abstractruntime/evidence/recorder.py +325 -0
- abstractruntime/history_bundle.py +772 -0
- abstractruntime/integrations/abstractcore/__init__.py +6 -0
- abstractruntime/integrations/abstractcore/constants.py +19 -0
- abstractruntime/integrations/abstractcore/default_tools.py +258 -0
- abstractruntime/integrations/abstractcore/effect_handlers.py +2622 -32
- abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
- abstractruntime/integrations/abstractcore/factory.py +149 -16
- abstractruntime/integrations/abstractcore/llm_client.py +891 -55
- abstractruntime/integrations/abstractcore/mcp_worker.py +587 -0
- abstractruntime/integrations/abstractcore/observability.py +80 -0
- abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
- abstractruntime/integrations/abstractcore/summarizer.py +154 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +509 -31
- abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
- abstractruntime/integrations/abstractmemory/__init__.py +3 -0
- abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
- abstractruntime/memory/__init__.py +21 -0
- abstractruntime/memory/active_context.py +751 -0
- abstractruntime/memory/active_memory.py +452 -0
- abstractruntime/memory/compaction.py +105 -0
- abstractruntime/memory/kg_packets.py +164 -0
- abstractruntime/memory/memact_composer.py +175 -0
- abstractruntime/memory/recall_levels.py +163 -0
- abstractruntime/memory/token_budget.py +86 -0
- abstractruntime/rendering/__init__.py +17 -0
- abstractruntime/rendering/agent_trace_report.py +256 -0
- abstractruntime/rendering/json_stringify.py +136 -0
- abstractruntime/scheduler/scheduler.py +93 -2
- abstractruntime/storage/__init__.py +7 -2
- abstractruntime/storage/artifacts.py +175 -32
- abstractruntime/storage/base.py +17 -1
- abstractruntime/storage/commands.py +339 -0
- abstractruntime/storage/in_memory.py +41 -1
- abstractruntime/storage/json_files.py +210 -14
- abstractruntime/storage/observable.py +136 -0
- abstractruntime/storage/offloading.py +433 -0
- abstractruntime/storage/sqlite.py +836 -0
- abstractruntime/visualflow_compiler/__init__.py +29 -0
- abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
- abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
- abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
- abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
- abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
- abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
- abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
- abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
- abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
- abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
- abstractruntime/visualflow_compiler/compiler.py +3832 -0
- abstractruntime/visualflow_compiler/flow.py +247 -0
- abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
- abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
- abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
- abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
- abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
- abstractruntime/visualflow_compiler/visual/models.py +211 -0
- abstractruntime/workflow_bundle/__init__.py +52 -0
- abstractruntime/workflow_bundle/models.py +236 -0
- abstractruntime/workflow_bundle/packer.py +317 -0
- abstractruntime/workflow_bundle/reader.py +87 -0
- abstractruntime/workflow_bundle/registry.py +587 -0
- abstractruntime-0.4.1.dist-info/METADATA +177 -0
- abstractruntime-0.4.1.dist-info/RECORD +86 -0
- abstractruntime-0.4.1.dist-info/entry_points.txt +2 -0
- abstractruntime-0.2.0.dist-info/METADATA +0 -163
- abstractruntime-0.2.0.dist-info/RECORD +0 -32
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
- {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Stdlib-only models for the VisualFlow JSON schema.
|
|
2
|
+
|
|
3
|
+
These are intentionally minimal and permissive:
|
|
4
|
+
- They accept unknown/extra fields (ignored).
|
|
5
|
+
- `type` is stored as a string for forward compatibility.
|
|
6
|
+
|
|
7
|
+
The compiler/executor code is responsible for interpreting node types.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from typing import Any, Dict, List, Optional
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class NodeType(str, Enum):
|
|
18
|
+
# Event/Trigger nodes (entry points)
|
|
19
|
+
ON_FLOW_START = "on_flow_start"
|
|
20
|
+
ON_USER_REQUEST = "on_user_request"
|
|
21
|
+
ON_AGENT_MESSAGE = "on_agent_message"
|
|
22
|
+
ON_SCHEDULE = "on_schedule"
|
|
23
|
+
ON_EVENT = "on_event"
|
|
24
|
+
# Flow IO nodes
|
|
25
|
+
ON_FLOW_END = "on_flow_end"
|
|
26
|
+
# Core execution nodes
|
|
27
|
+
AGENT = "agent"
|
|
28
|
+
FUNCTION = "function"
|
|
29
|
+
CODE = "code"
|
|
30
|
+
SUBFLOW = "subflow"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass(frozen=True)
|
|
34
|
+
class VisualNode:
|
|
35
|
+
id: str
|
|
36
|
+
type: str
|
|
37
|
+
data: Dict[str, Any] = field(default_factory=dict)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(frozen=True)
|
|
41
|
+
class VisualEdge:
|
|
42
|
+
source: str
|
|
43
|
+
target: str
|
|
44
|
+
sourceHandle: str
|
|
45
|
+
targetHandle: str
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class VisualFlow:
|
|
50
|
+
id: str
|
|
51
|
+
name: str = ""
|
|
52
|
+
description: str = ""
|
|
53
|
+
interfaces: List[str] = field(default_factory=list)
|
|
54
|
+
nodes: List[VisualNode] = field(default_factory=list)
|
|
55
|
+
edges: List[VisualEdge] = field(default_factory=list)
|
|
56
|
+
entryNode: Optional[str] = None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _coerce_type(value: Any) -> str:
|
|
60
|
+
if isinstance(value, str):
|
|
61
|
+
s = value.strip()
|
|
62
|
+
# Pydantic (and other serializers) may stringify enums as "NodeType.X".
|
|
63
|
+
# Normalize to the underlying value-like form ("x") for downstream matching.
|
|
64
|
+
if s.startswith("NodeType.") and "." in s:
|
|
65
|
+
member = s.split(".", 1)[1].strip()
|
|
66
|
+
if member:
|
|
67
|
+
return member.lower()
|
|
68
|
+
return s
|
|
69
|
+
if isinstance(value, Enum):
|
|
70
|
+
return str(value.value)
|
|
71
|
+
if isinstance(value, dict):
|
|
72
|
+
# Best-effort support for enum-like objects serialized as {"value": "..."}.
|
|
73
|
+
v = value.get("value")
|
|
74
|
+
if isinstance(v, str):
|
|
75
|
+
return v
|
|
76
|
+
return str(value or "")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _rename_pin_ids(pins: Any, renames: Dict[str, str]) -> Any:
|
|
80
|
+
"""Best-effort pin id migration for VisualFlow JSON (in-place-ish).
|
|
81
|
+
|
|
82
|
+
`pins` is expected to be a list of dicts like: {"id": "...", "label": "...", ...}.
|
|
83
|
+
Returns a normalized list with stable ordering and de-duplicated ids.
|
|
84
|
+
"""
|
|
85
|
+
if not isinstance(pins, list) or not renames:
|
|
86
|
+
return pins
|
|
87
|
+
out: list[Any] = []
|
|
88
|
+
seen: set[str] = set()
|
|
89
|
+
for p in pins:
|
|
90
|
+
if not isinstance(p, dict):
|
|
91
|
+
out.append(p)
|
|
92
|
+
continue
|
|
93
|
+
pid = p.get("id")
|
|
94
|
+
pid_str = pid if isinstance(pid, str) else None
|
|
95
|
+
next_id = renames.get(pid_str, pid_str) if pid_str else pid_str
|
|
96
|
+
if isinstance(next_id, str) and next_id:
|
|
97
|
+
if next_id in seen:
|
|
98
|
+
continue
|
|
99
|
+
seen.add(next_id)
|
|
100
|
+
if next_id and next_id != pid_str:
|
|
101
|
+
p2 = dict(p)
|
|
102
|
+
p2["id"] = next_id
|
|
103
|
+
if p2.get("label") == pid_str:
|
|
104
|
+
p2["label"] = next_id
|
|
105
|
+
out.append(p2)
|
|
106
|
+
else:
|
|
107
|
+
out.append(p)
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _rename_pin_defaults(pin_defaults: Any, renames: Dict[str, str]) -> Any:
|
|
112
|
+
if not isinstance(pin_defaults, dict) or not renames:
|
|
113
|
+
return pin_defaults
|
|
114
|
+
out = dict(pin_defaults)
|
|
115
|
+
for old, new in renames.items():
|
|
116
|
+
if old not in out:
|
|
117
|
+
continue
|
|
118
|
+
if new not in out:
|
|
119
|
+
out[new] = out[old]
|
|
120
|
+
out.pop(old, None)
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def load_visualflow_json(raw: Any) -> VisualFlow:
|
|
125
|
+
"""Parse a VisualFlow JSON object (dict) into stdlib dataclasses.
|
|
126
|
+
|
|
127
|
+
Also accepts Pydantic-like models by calling `model_dump()` or `dict()`.
|
|
128
|
+
"""
|
|
129
|
+
if hasattr(raw, "model_dump"):
|
|
130
|
+
# Prefer JSON mode so enums are dumped as their values (not "NodeType.X").
|
|
131
|
+
try:
|
|
132
|
+
raw = raw.model_dump(mode="json") # type: ignore[assignment]
|
|
133
|
+
except TypeError:
|
|
134
|
+
raw = raw.model_dump() # type: ignore[assignment]
|
|
135
|
+
elif hasattr(raw, "dict"):
|
|
136
|
+
raw = raw.dict() # type: ignore[assignment]
|
|
137
|
+
|
|
138
|
+
if not isinstance(raw, dict):
|
|
139
|
+
raise TypeError("VisualFlow must be a JSON object (dict)")
|
|
140
|
+
|
|
141
|
+
fid = str(raw.get("id") or raw.get("flow_id") or raw.get("workflow_id") or "").strip()
|
|
142
|
+
if not fid:
|
|
143
|
+
raise ValueError("VisualFlow missing required 'id'")
|
|
144
|
+
|
|
145
|
+
name = str(raw.get("name") or "")
|
|
146
|
+
description = str(raw.get("description") or "")
|
|
147
|
+
|
|
148
|
+
interfaces_raw = raw.get("interfaces")
|
|
149
|
+
interfaces: list[str] = []
|
|
150
|
+
if isinstance(interfaces_raw, list):
|
|
151
|
+
for it in interfaces_raw:
|
|
152
|
+
if isinstance(it, str) and it.strip():
|
|
153
|
+
interfaces.append(it.strip())
|
|
154
|
+
|
|
155
|
+
nodes_raw = raw.get("nodes")
|
|
156
|
+
nodes: list[VisualNode] = []
|
|
157
|
+
node_types_by_id: Dict[str, str] = {}
|
|
158
|
+
if isinstance(nodes_raw, list):
|
|
159
|
+
for n in nodes_raw:
|
|
160
|
+
if not isinstance(n, dict):
|
|
161
|
+
continue
|
|
162
|
+
nid = str(n.get("id") or "").strip()
|
|
163
|
+
if not nid:
|
|
164
|
+
continue
|
|
165
|
+
t = _coerce_type(n.get("type"))
|
|
166
|
+
node_types_by_id[nid] = str(t)
|
|
167
|
+
data = n.get("data")
|
|
168
|
+
data_d = dict(data) if isinstance(data, dict) else {}
|
|
169
|
+
nodes.append(VisualNode(id=nid, type=str(t), data=data_d))
|
|
170
|
+
|
|
171
|
+
edges_raw = raw.get("edges")
|
|
172
|
+
edges: list[VisualEdge] = []
|
|
173
|
+
if isinstance(edges_raw, list):
|
|
174
|
+
for e in edges_raw:
|
|
175
|
+
if not isinstance(e, dict):
|
|
176
|
+
continue
|
|
177
|
+
src = str(e.get("source") or "").strip()
|
|
178
|
+
tgt = str(e.get("target") or "").strip()
|
|
179
|
+
if not src or not tgt:
|
|
180
|
+
continue
|
|
181
|
+
sh = e.get("sourceHandle")
|
|
182
|
+
th = e.get("targetHandle")
|
|
183
|
+
# VisualFlow edges are defined by their pin handles; skip malformed edges.
|
|
184
|
+
if not isinstance(sh, str) or not sh.strip():
|
|
185
|
+
continue
|
|
186
|
+
if not isinstance(th, str) or not th.strip():
|
|
187
|
+
continue
|
|
188
|
+
sh_s = sh.strip()
|
|
189
|
+
th_s = th.strip()
|
|
190
|
+
|
|
191
|
+
edges.append(
|
|
192
|
+
VisualEdge(
|
|
193
|
+
source=src,
|
|
194
|
+
target=tgt,
|
|
195
|
+
sourceHandle=sh_s,
|
|
196
|
+
targetHandle=th_s,
|
|
197
|
+
)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
entry = raw.get("entryNode")
|
|
201
|
+
entry_node = str(entry).strip() if isinstance(entry, str) and entry.strip() else None
|
|
202
|
+
|
|
203
|
+
return VisualFlow(
|
|
204
|
+
id=fid,
|
|
205
|
+
name=name,
|
|
206
|
+
description=description,
|
|
207
|
+
interfaces=interfaces,
|
|
208
|
+
nodes=nodes,
|
|
209
|
+
edges=edges,
|
|
210
|
+
entryNode=entry_node,
|
|
211
|
+
)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
abstractruntime.workflow_bundle
|
|
3
|
+
|
|
4
|
+
WorkflowBundle (.flow) support (portable distribution unit).
|
|
5
|
+
|
|
6
|
+
Design intent:
|
|
7
|
+
- Stdlib-only (no extra deps) to keep AbstractRuntime minimal.
|
|
8
|
+
- Bundles are transport/content; hosts decide trust and execution policy.
|
|
9
|
+
- Bundle IDs are used by hosts to namespace workflow ids and avoid collisions.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .models import (
|
|
13
|
+
WORKFLOW_BUNDLE_FORMAT_VERSION_V1,
|
|
14
|
+
WorkflowBundleEntrypoint,
|
|
15
|
+
WorkflowBundleError,
|
|
16
|
+
WorkflowBundleManifest,
|
|
17
|
+
workflow_bundle_manifest_from_dict,
|
|
18
|
+
workflow_bundle_manifest_to_dict,
|
|
19
|
+
)
|
|
20
|
+
from .packer import PackedWorkflowBundle, inspect_workflow_bundle, pack_workflow_bundle, unpack_workflow_bundle
|
|
21
|
+
from .reader import WorkflowBundle, open_workflow_bundle
|
|
22
|
+
from .registry import (
|
|
23
|
+
InstalledWorkflowBundle,
|
|
24
|
+
WorkflowBundleRegistry,
|
|
25
|
+
WorkflowBundleRegistryError,
|
|
26
|
+
WorkflowEntrypointRef,
|
|
27
|
+
default_workflow_bundles_dir,
|
|
28
|
+
sanitize_bundle_id,
|
|
29
|
+
sanitize_bundle_version,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"WORKFLOW_BUNDLE_FORMAT_VERSION_V1",
|
|
34
|
+
"WorkflowBundleError",
|
|
35
|
+
"WorkflowBundleEntrypoint",
|
|
36
|
+
"WorkflowBundleManifest",
|
|
37
|
+
"workflow_bundle_manifest_from_dict",
|
|
38
|
+
"workflow_bundle_manifest_to_dict",
|
|
39
|
+
"PackedWorkflowBundle",
|
|
40
|
+
"pack_workflow_bundle",
|
|
41
|
+
"inspect_workflow_bundle",
|
|
42
|
+
"unpack_workflow_bundle",
|
|
43
|
+
"WorkflowBundle",
|
|
44
|
+
"open_workflow_bundle",
|
|
45
|
+
"InstalledWorkflowBundle",
|
|
46
|
+
"WorkflowBundleRegistry",
|
|
47
|
+
"WorkflowBundleRegistryError",
|
|
48
|
+
"WorkflowEntrypointRef",
|
|
49
|
+
"default_workflow_bundles_dir",
|
|
50
|
+
"sanitize_bundle_id",
|
|
51
|
+
"sanitize_bundle_version",
|
|
52
|
+
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""WorkflowBundle manifest models (stdlib-only).
|
|
2
|
+
|
|
3
|
+
Bundle layout (zip):
|
|
4
|
+
manifest.json
|
|
5
|
+
flows/<id>.json (VisualFlow JSON sources)
|
|
6
|
+
assets/... (optional)
|
|
7
|
+
|
|
8
|
+
Notes:
|
|
9
|
+
- A bundle is a *distribution unit*. Hosts may namespace workflow ids at load time.
|
|
10
|
+
- `manifest.artifacts` remains as a legacy field for backward compatibility, but modern
|
|
11
|
+
hosts compile from `manifest.flows` using the AbstractRuntime VisualFlow compiler.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from typing import Any, Dict, List, Optional
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
WORKFLOW_BUNDLE_FORMAT_VERSION_V1 = "1"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class WorkflowBundleError(ValueError):
|
|
24
|
+
"""Raised when a WorkflowBundle manifest is invalid or unsupported."""
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _is_safe_relpath(p: str) -> bool:
|
|
28
|
+
s = str(p or "").strip()
|
|
29
|
+
if not s:
|
|
30
|
+
return False
|
|
31
|
+
# Disallow absolute paths and traversal.
|
|
32
|
+
if s.startswith(("/", "\\")):
|
|
33
|
+
return False
|
|
34
|
+
if ":" in s.split("/", 1)[0]:
|
|
35
|
+
# Prevent "C:\..." style and other drive-like prefixes.
|
|
36
|
+
return False
|
|
37
|
+
parts = [x for x in s.replace("\\", "/").split("/") if x]
|
|
38
|
+
if any(x in {".", ".."} for x in parts):
|
|
39
|
+
return False
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class WorkflowBundleEntrypoint:
|
|
45
|
+
"""An entrypoint workflow exposed by a bundle."""
|
|
46
|
+
|
|
47
|
+
flow_id: str
|
|
48
|
+
name: Optional[str] = None
|
|
49
|
+
description: str = ""
|
|
50
|
+
interfaces: List[str] = field(default_factory=list)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@dataclass(frozen=True)
|
|
54
|
+
class WorkflowBundleManifest:
|
|
55
|
+
"""Bundle manifest (manifest.json)."""
|
|
56
|
+
|
|
57
|
+
bundle_format_version: str
|
|
58
|
+
bundle_id: str
|
|
59
|
+
bundle_version: str = "0.0.0"
|
|
60
|
+
created_at: str = ""
|
|
61
|
+
|
|
62
|
+
entrypoints: List[WorkflowBundleEntrypoint] = field(default_factory=list)
|
|
63
|
+
# Optional explicit default entrypoint (flow id). If omitted:
|
|
64
|
+
# - single-entrypoint bundles imply a default
|
|
65
|
+
# - multi-entrypoint bundles require callers to specify flow_id (unless a host chooses otherwise)
|
|
66
|
+
default_entrypoint: Optional[str] = None
|
|
67
|
+
|
|
68
|
+
# Maps flow_id -> relative path in the bundle (optional; used by UIs).
|
|
69
|
+
flows: Dict[str, str] = field(default_factory=dict)
|
|
70
|
+
# Legacy/deprecated: maps flow_id -> relative path to WorkflowArtifact JSON.
|
|
71
|
+
artifacts: Dict[str, str] = field(default_factory=dict)
|
|
72
|
+
# Optional assets mapping (logical name -> relative path).
|
|
73
|
+
assets: Dict[str, str] = field(default_factory=dict)
|
|
74
|
+
|
|
75
|
+
# Free-form JSON-safe metadata (tags, author, etc.)
|
|
76
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
77
|
+
|
|
78
|
+
def validate(self) -> None:
|
|
79
|
+
if str(self.bundle_format_version or "").strip() != WORKFLOW_BUNDLE_FORMAT_VERSION_V1:
|
|
80
|
+
raise WorkflowBundleError(
|
|
81
|
+
f"Unsupported WorkflowBundle bundle_format_version '{self.bundle_format_version}'. "
|
|
82
|
+
f"Supported: {WORKFLOW_BUNDLE_FORMAT_VERSION_V1}"
|
|
83
|
+
)
|
|
84
|
+
if not isinstance(self.bundle_id, str) or not self.bundle_id.strip():
|
|
85
|
+
raise WorkflowBundleError("bundle_id must be a non-empty string")
|
|
86
|
+
if not isinstance(self.bundle_version, str) or not self.bundle_version.strip():
|
|
87
|
+
raise WorkflowBundleError("bundle_version must be a non-empty string")
|
|
88
|
+
|
|
89
|
+
if not isinstance(self.entrypoints, list) or not self.entrypoints:
|
|
90
|
+
raise WorkflowBundleError("manifest.entrypoints must be a non-empty list")
|
|
91
|
+
|
|
92
|
+
seen: set[str] = set()
|
|
93
|
+
for ep in self.entrypoints:
|
|
94
|
+
if not isinstance(ep.flow_id, str) or not ep.flow_id.strip():
|
|
95
|
+
raise WorkflowBundleError("entrypoint.flow_id must be a non-empty string")
|
|
96
|
+
fid = ep.flow_id.strip()
|
|
97
|
+
if fid in seen:
|
|
98
|
+
raise WorkflowBundleError(f"Duplicate entrypoint flow_id '{fid}'")
|
|
99
|
+
seen.add(fid)
|
|
100
|
+
if not isinstance(ep.description, str):
|
|
101
|
+
raise WorkflowBundleError(f"entrypoint '{fid}' description must be a string")
|
|
102
|
+
if not isinstance(ep.interfaces, list):
|
|
103
|
+
raise WorkflowBundleError(f"entrypoint '{fid}' interfaces must be a list")
|
|
104
|
+
for it in ep.interfaces:
|
|
105
|
+
if not isinstance(it, str):
|
|
106
|
+
raise WorkflowBundleError(f"entrypoint '{fid}' interfaces must be strings")
|
|
107
|
+
|
|
108
|
+
if self.default_entrypoint is not None:
|
|
109
|
+
de = str(self.default_entrypoint or "").strip()
|
|
110
|
+
if de and de not in seen:
|
|
111
|
+
raise WorkflowBundleError(f"default_entrypoint '{de}' must match an entrypoint.flow_id")
|
|
112
|
+
|
|
113
|
+
for mapping_name, mapping in (("flows", self.flows), ("artifacts", self.artifacts), ("assets", self.assets)):
|
|
114
|
+
if not isinstance(mapping, dict):
|
|
115
|
+
raise WorkflowBundleError(f"manifest.{mapping_name} must be a dict")
|
|
116
|
+
for k, v in mapping.items():
|
|
117
|
+
if not isinstance(k, str) or not k.strip():
|
|
118
|
+
raise WorkflowBundleError(f"manifest.{mapping_name} keys must be non-empty strings")
|
|
119
|
+
if not isinstance(v, str) or not _is_safe_relpath(v):
|
|
120
|
+
raise WorkflowBundleError(f"manifest.{mapping_name} entry '{k}' has unsafe path '{v}'")
|
|
121
|
+
|
|
122
|
+
if not isinstance(self.metadata, dict):
|
|
123
|
+
raise WorkflowBundleError("manifest.metadata must be an object")
|
|
124
|
+
|
|
125
|
+
def artifact_path_for(self, flow_id: str) -> Optional[str]:
|
|
126
|
+
fid = str(flow_id or "").strip()
|
|
127
|
+
if not fid:
|
|
128
|
+
return None
|
|
129
|
+
p = self.artifacts.get(fid)
|
|
130
|
+
return str(p) if isinstance(p, str) and p.strip() else None
|
|
131
|
+
|
|
132
|
+
def flow_path_for(self, flow_id: str) -> Optional[str]:
|
|
133
|
+
fid = str(flow_id or "").strip()
|
|
134
|
+
if not fid:
|
|
135
|
+
return None
|
|
136
|
+
p = self.flows.get(fid)
|
|
137
|
+
return str(p) if isinstance(p, str) and p.strip() else None
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def workflow_bundle_manifest_from_dict(raw: Dict[str, Any]) -> WorkflowBundleManifest:
|
|
141
|
+
if not isinstance(raw, dict):
|
|
142
|
+
raise WorkflowBundleError("manifest.json must be a JSON object")
|
|
143
|
+
|
|
144
|
+
version = str(raw.get("bundle_format_version") or raw.get("bundleFormatVersion") or "").strip()
|
|
145
|
+
bundle_id = str(raw.get("bundle_id") or raw.get("bundleId") or "").strip()
|
|
146
|
+
bundle_version = str(raw.get("bundle_version") or raw.get("bundleVersion") or "0.0.0").strip() or "0.0.0"
|
|
147
|
+
created_at = str(raw.get("created_at") or raw.get("createdAt") or "")
|
|
148
|
+
|
|
149
|
+
default_entrypoint_raw = (
|
|
150
|
+
raw.get("default_entrypoint") or raw.get("defaultEntrypoint") or raw.get("default_entrypoint_flow_id") or raw.get("defaultEntrypointFlowId")
|
|
151
|
+
)
|
|
152
|
+
default_entrypoint = (
|
|
153
|
+
str(default_entrypoint_raw).strip()
|
|
154
|
+
if isinstance(default_entrypoint_raw, str) and str(default_entrypoint_raw).strip()
|
|
155
|
+
else None
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
entrypoints_raw = raw.get("entrypoints")
|
|
159
|
+
entrypoints: list[WorkflowBundleEntrypoint] = []
|
|
160
|
+
if isinstance(entrypoints_raw, list):
|
|
161
|
+
for ep in entrypoints_raw:
|
|
162
|
+
if not isinstance(ep, dict):
|
|
163
|
+
continue
|
|
164
|
+
flow_id = str(ep.get("flow_id") or ep.get("flowId") or "").strip()
|
|
165
|
+
name = ep.get("name")
|
|
166
|
+
name_s = str(name).strip() if isinstance(name, str) and name.strip() else None
|
|
167
|
+
description = str(ep.get("description") or "")
|
|
168
|
+
interfaces_raw = ep.get("interfaces")
|
|
169
|
+
interfaces: list[str] = []
|
|
170
|
+
if isinstance(interfaces_raw, list):
|
|
171
|
+
for x in interfaces_raw:
|
|
172
|
+
if isinstance(x, str) and x.strip():
|
|
173
|
+
interfaces.append(x.strip())
|
|
174
|
+
entrypoints.append(
|
|
175
|
+
WorkflowBundleEntrypoint(
|
|
176
|
+
flow_id=flow_id,
|
|
177
|
+
name=name_s,
|
|
178
|
+
description=description,
|
|
179
|
+
interfaces=interfaces,
|
|
180
|
+
)
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
flows_raw = raw.get("flows")
|
|
184
|
+
flows: Dict[str, str] = dict(flows_raw) if isinstance(flows_raw, dict) else {}
|
|
185
|
+
|
|
186
|
+
artifacts_raw = raw.get("artifacts")
|
|
187
|
+
artifacts: Dict[str, str] = dict(artifacts_raw) if isinstance(artifacts_raw, dict) else {}
|
|
188
|
+
|
|
189
|
+
assets_raw = raw.get("assets")
|
|
190
|
+
assets: Dict[str, str] = dict(assets_raw) if isinstance(assets_raw, dict) else {}
|
|
191
|
+
|
|
192
|
+
metadata_raw = raw.get("metadata")
|
|
193
|
+
metadata: Dict[str, Any] = dict(metadata_raw) if isinstance(metadata_raw, dict) else {}
|
|
194
|
+
|
|
195
|
+
man = WorkflowBundleManifest(
|
|
196
|
+
bundle_format_version=version,
|
|
197
|
+
bundle_id=bundle_id,
|
|
198
|
+
bundle_version=bundle_version,
|
|
199
|
+
created_at=created_at,
|
|
200
|
+
entrypoints=entrypoints,
|
|
201
|
+
default_entrypoint=default_entrypoint,
|
|
202
|
+
flows=flows,
|
|
203
|
+
artifacts=artifacts,
|
|
204
|
+
assets=assets,
|
|
205
|
+
metadata=metadata,
|
|
206
|
+
)
|
|
207
|
+
man.validate()
|
|
208
|
+
return man
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def workflow_bundle_manifest_to_dict(manifest: WorkflowBundleManifest) -> Dict[str, Any]:
|
|
212
|
+
manifest.validate()
|
|
213
|
+
out = {
|
|
214
|
+
"bundle_format_version": str(manifest.bundle_format_version),
|
|
215
|
+
"bundle_id": str(manifest.bundle_id),
|
|
216
|
+
"bundle_version": str(manifest.bundle_version),
|
|
217
|
+
"created_at": str(manifest.created_at or ""),
|
|
218
|
+
"entrypoints": [
|
|
219
|
+
{
|
|
220
|
+
"flow_id": ep.flow_id,
|
|
221
|
+
"name": ep.name,
|
|
222
|
+
"description": str(ep.description or ""),
|
|
223
|
+
"interfaces": list(ep.interfaces or []),
|
|
224
|
+
}
|
|
225
|
+
for ep in list(manifest.entrypoints or [])
|
|
226
|
+
],
|
|
227
|
+
"default_entrypoint": str(manifest.default_entrypoint) if manifest.default_entrypoint else None,
|
|
228
|
+
"flows": dict(manifest.flows or {}),
|
|
229
|
+
"artifacts": dict(manifest.artifacts or {}),
|
|
230
|
+
"assets": dict(manifest.assets or {}),
|
|
231
|
+
"metadata": dict(manifest.metadata or {}),
|
|
232
|
+
}
|
|
233
|
+
# Keep output stable: omit null default_entrypoint.
|
|
234
|
+
if out.get("default_entrypoint") is None:
|
|
235
|
+
out.pop("default_entrypoint", None)
|
|
236
|
+
return out
|