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.
Files changed (77) hide show
  1. abstractruntime/__init__.py +83 -3
  2. abstractruntime/core/config.py +82 -2
  3. abstractruntime/core/event_keys.py +62 -0
  4. abstractruntime/core/models.py +17 -1
  5. abstractruntime/core/policy.py +74 -3
  6. abstractruntime/core/runtime.py +3334 -28
  7. abstractruntime/core/vars.py +103 -2
  8. abstractruntime/evidence/__init__.py +10 -0
  9. abstractruntime/evidence/recorder.py +325 -0
  10. abstractruntime/history_bundle.py +772 -0
  11. abstractruntime/integrations/abstractcore/__init__.py +6 -0
  12. abstractruntime/integrations/abstractcore/constants.py +19 -0
  13. abstractruntime/integrations/abstractcore/default_tools.py +258 -0
  14. abstractruntime/integrations/abstractcore/effect_handlers.py +2622 -32
  15. abstractruntime/integrations/abstractcore/embeddings_client.py +69 -0
  16. abstractruntime/integrations/abstractcore/factory.py +149 -16
  17. abstractruntime/integrations/abstractcore/llm_client.py +891 -55
  18. abstractruntime/integrations/abstractcore/mcp_worker.py +587 -0
  19. abstractruntime/integrations/abstractcore/observability.py +80 -0
  20. abstractruntime/integrations/abstractcore/session_attachments.py +946 -0
  21. abstractruntime/integrations/abstractcore/summarizer.py +154 -0
  22. abstractruntime/integrations/abstractcore/tool_executor.py +509 -31
  23. abstractruntime/integrations/abstractcore/workspace_scoped_tools.py +561 -0
  24. abstractruntime/integrations/abstractmemory/__init__.py +3 -0
  25. abstractruntime/integrations/abstractmemory/effect_handlers.py +946 -0
  26. abstractruntime/memory/__init__.py +21 -0
  27. abstractruntime/memory/active_context.py +751 -0
  28. abstractruntime/memory/active_memory.py +452 -0
  29. abstractruntime/memory/compaction.py +105 -0
  30. abstractruntime/memory/kg_packets.py +164 -0
  31. abstractruntime/memory/memact_composer.py +175 -0
  32. abstractruntime/memory/recall_levels.py +163 -0
  33. abstractruntime/memory/token_budget.py +86 -0
  34. abstractruntime/rendering/__init__.py +17 -0
  35. abstractruntime/rendering/agent_trace_report.py +256 -0
  36. abstractruntime/rendering/json_stringify.py +136 -0
  37. abstractruntime/scheduler/scheduler.py +93 -2
  38. abstractruntime/storage/__init__.py +7 -2
  39. abstractruntime/storage/artifacts.py +175 -32
  40. abstractruntime/storage/base.py +17 -1
  41. abstractruntime/storage/commands.py +339 -0
  42. abstractruntime/storage/in_memory.py +41 -1
  43. abstractruntime/storage/json_files.py +210 -14
  44. abstractruntime/storage/observable.py +136 -0
  45. abstractruntime/storage/offloading.py +433 -0
  46. abstractruntime/storage/sqlite.py +836 -0
  47. abstractruntime/visualflow_compiler/__init__.py +29 -0
  48. abstractruntime/visualflow_compiler/adapters/__init__.py +11 -0
  49. abstractruntime/visualflow_compiler/adapters/agent_adapter.py +126 -0
  50. abstractruntime/visualflow_compiler/adapters/context_adapter.py +109 -0
  51. abstractruntime/visualflow_compiler/adapters/control_adapter.py +615 -0
  52. abstractruntime/visualflow_compiler/adapters/effect_adapter.py +1051 -0
  53. abstractruntime/visualflow_compiler/adapters/event_adapter.py +307 -0
  54. abstractruntime/visualflow_compiler/adapters/function_adapter.py +97 -0
  55. abstractruntime/visualflow_compiler/adapters/memact_adapter.py +114 -0
  56. abstractruntime/visualflow_compiler/adapters/subflow_adapter.py +74 -0
  57. abstractruntime/visualflow_compiler/adapters/variable_adapter.py +316 -0
  58. abstractruntime/visualflow_compiler/compiler.py +3832 -0
  59. abstractruntime/visualflow_compiler/flow.py +247 -0
  60. abstractruntime/visualflow_compiler/visual/__init__.py +13 -0
  61. abstractruntime/visualflow_compiler/visual/agent_ids.py +29 -0
  62. abstractruntime/visualflow_compiler/visual/builtins.py +1376 -0
  63. abstractruntime/visualflow_compiler/visual/code_executor.py +214 -0
  64. abstractruntime/visualflow_compiler/visual/executor.py +2804 -0
  65. abstractruntime/visualflow_compiler/visual/models.py +211 -0
  66. abstractruntime/workflow_bundle/__init__.py +52 -0
  67. abstractruntime/workflow_bundle/models.py +236 -0
  68. abstractruntime/workflow_bundle/packer.py +317 -0
  69. abstractruntime/workflow_bundle/reader.py +87 -0
  70. abstractruntime/workflow_bundle/registry.py +587 -0
  71. abstractruntime-0.4.1.dist-info/METADATA +177 -0
  72. abstractruntime-0.4.1.dist-info/RECORD +86 -0
  73. abstractruntime-0.4.1.dist-info/entry_points.txt +2 -0
  74. abstractruntime-0.2.0.dist-info/METADATA +0 -163
  75. abstractruntime-0.2.0.dist-info/RECORD +0 -32
  76. {abstractruntime-0.2.0.dist-info → abstractruntime-0.4.1.dist-info}/WHEEL +0 -0
  77. {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