-
Notifications
You must be signed in to change notification settings - Fork 282
Expand file tree
/
Copy pathworkflows.py
More file actions
289 lines (239 loc) · 9.71 KB
/
Copy pathworkflows.py
File metadata and controls
289 lines (239 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
"""Portable workflow status and metadata contracts for Basic Memory runtimes."""
from __future__ import annotations
from collections.abc import Mapping
from dataclasses import dataclass
from typing import Literal, Self
from uuid import UUID
type WorkflowId = UUID
type RuntimeWorkflowCheckpoint = Mapping[str, object]
type RuntimeWorkflowMetadata = Mapping[str, object]
type RuntimeWorkflowMetadataPatch = Mapping[str, object]
type RuntimeWorkflowStatus = str
type RuntimeWorkflowPhase = str
type RuntimeWorkflowProgress = str
type RuntimeWorkflowResult = Mapping[str, object]
type RuntimeJobStatusType = Literal[
"queued",
"in_progress",
"complete",
"failed",
"deferred",
"not_found",
"unknown",
"cancelled",
]
WORKFLOW_EVENT_TEXT_MAX_CHARS = 4096
RUNTIME_ACTIVE_WORKFLOW_STATUSES: frozenset[RuntimeWorkflowStatus] = frozenset(
{"queued", "running"}
)
RUNTIME_TERMINAL_WORKFLOW_STATUSES: frozenset[RuntimeWorkflowStatus] = frozenset(
{"completed", "failed", "cancelled"}
)
def runtime_job_status_from_workflow_status(
workflow_status: RuntimeWorkflowStatus,
) -> RuntimeJobStatusType:
"""Translate durable workflow states to the portable job-status vocabulary."""
match workflow_status:
case "queued":
return "queued"
case "running":
return "in_progress"
case "completed":
return "complete"
case "failed":
return "failed"
case "cancelled":
return "cancelled"
case _:
return "unknown"
def parse_runtime_workflow_id(job_id: str) -> WorkflowId | None:
"""Best-effort workflow-id parsing for durable runtime status lookups."""
try:
return UUID(job_id)
except ValueError:
return None
@dataclass(frozen=True, slots=True)
class RuntimeWorkflowMetadataView:
"""Typed read view over durable workflow metadata."""
metadata: RuntimeWorkflowMetadata
@classmethod
def from_metadata(cls, metadata: RuntimeWorkflowMetadata | None) -> Self:
"""Build a read view from optional persisted workflow metadata."""
return cls(metadata=dict(metadata or {}))
@property
def phase(self) -> RuntimeWorkflowPhase | None:
"""Return the latest machine-readable workflow phase."""
phase = self.metadata.get("phase")
return phase if isinstance(phase, str) else None
@property
def progress(self) -> RuntimeWorkflowProgress | None:
"""Return human-readable workflow progress, falling back to phase."""
progress = self.metadata.get("progress")
if isinstance(progress, str) and progress:
return progress
return self.phase
@property
def checkpoint(self) -> dict[str, object] | None:
"""Return copied checkpoint metadata for resumable workflow jobs."""
checkpoint = self.metadata.get("checkpoint")
return runtime_workflow_metadata_dict_value(checkpoint, field_name="checkpoint")
@property
def result(self) -> dict[str, object] | None:
"""Return copied structured result data from workflow metadata."""
result = self.metadata.get("result")
return runtime_workflow_metadata_dict_value(result, field_name="result")
def truncate_runtime_workflow_text(
value: str,
*,
max_chars: int = WORKFLOW_EVENT_TEXT_MAX_CHARS,
) -> str:
"""Return a stable workflow text preview for durable metadata and event streams."""
if max_chars < 0:
raise ValueError("max_chars must be non-negative")
if len(value) <= max_chars:
return value
omitted_chars = len(value) - max_chars
suffix = f"... [truncated {omitted_chars} chars]"
available_chars = max(max_chars - len(suffix), 0)
return f"{value[:available_chars]}{suffix}"
def merge_runtime_workflow_metadata_patch(
base: Mapping[str, object],
metadata_patch: RuntimeWorkflowMetadataPatch | None,
) -> dict[str, object]:
"""Recursively merge adapter-specific workflow metadata patches."""
patch = dict(base)
if metadata_patch is not None:
for key, value in metadata_patch.items():
current = patch.get(key)
current_mapping = runtime_workflow_metadata_mapping_value(current, field_name=key)
value_mapping = runtime_workflow_metadata_mapping_value(value, field_name=key)
if current_mapping is not None and value_mapping is not None:
patch[key] = merge_runtime_workflow_metadata_patch(
current_mapping,
value_mapping,
)
continue
patch[key] = value
return patch
def runtime_workflow_metadata_mapping_value(
value: object,
*,
field_name: str,
) -> dict[str, object] | None:
"""Return a copied workflow metadata mapping value with string keys."""
if not isinstance(value, Mapping):
return None
copied: dict[str, object] = {}
for key, item in value.items():
if not isinstance(key, str):
raise TypeError(f"Workflow {field_name} metadata keys must be strings")
copied[key] = item
return copied
def runtime_workflow_metadata_dict_value(
value: object,
*,
field_name: str,
) -> dict[str, object] | None:
"""Return a copied workflow metadata object value with string keys."""
if not isinstance(value, dict):
return None
return runtime_workflow_metadata_mapping_value(value, field_name=field_name)
@dataclass(frozen=True, slots=True)
class RuntimeWorkflowAttemptMetadata:
"""Workflow metadata and event data for a started runtime attempt."""
progress: RuntimeWorkflowProgress
metadata_patch: RuntimeWorkflowMetadataPatch | None = None
phase: RuntimeWorkflowPhase = "running"
def workflow_metadata_patch(self) -> dict[str, object]:
"""Serialize to the existing durable attempt metadata patch shape."""
return merge_runtime_workflow_metadata_patch(
{
"phase": self.phase,
"progress": self.progress,
},
self.metadata_patch,
)
def attempt_started_event_data(
self,
*,
attempt_number: int,
transport_event_data: Mapping[str, object] | None,
) -> dict[str, object]:
"""Serialize to the existing attempt-started event payload shape.
Transport identity is opaque to core: runtimes that track queue-level
job ids merge them in via ``transport_event_data`` (inserted between
``attempt_number`` and ``phase`` to keep persisted event shapes stable).
"""
return {
"attempt_number": attempt_number,
**dict(transport_event_data or {}),
"phase": self.phase,
"progress": self.progress,
}
@dataclass(frozen=True, slots=True)
class RuntimeWorkflowProgressMetadata:
"""Workflow metadata and event data for an in-flight progress update."""
progress: RuntimeWorkflowProgress
phase: RuntimeWorkflowPhase | None = None
metadata_patch: RuntimeWorkflowMetadataPatch | None = None
def workflow_metadata_patch(self) -> dict[str, object]:
"""Serialize to the existing durable progress metadata patch shape."""
base: dict[str, object] = {"progress": self.progress}
if self.phase is not None:
base["phase"] = self.phase
return merge_runtime_workflow_metadata_patch(base, self.metadata_patch)
def progress_event_data(self) -> dict[str, object]:
"""Serialize to the existing progress event payload shape."""
return {
"phase": self.phase,
"progress": self.progress,
}
@dataclass(frozen=True, slots=True)
class RuntimeWorkflowCompletionMetadata:
"""Workflow metadata and event data for a completed runtime workflow."""
result: RuntimeWorkflowResult | None = None
metadata_patch: RuntimeWorkflowMetadataPatch | None = None
progress: RuntimeWorkflowProgress = "completed"
def workflow_metadata_patch(self) -> dict[str, object]:
"""Serialize to the existing durable completion metadata patch shape."""
base: dict[str, object] = {
"phase": "completed",
"progress": self.progress,
}
if self.result is not None:
base["result"] = dict(self.result)
return merge_runtime_workflow_metadata_patch(base, self.metadata_patch)
def completed_event_data(self) -> dict[str, object]:
"""Serialize to the existing completed event payload shape."""
return {
"phase": "completed",
"progress": self.progress,
"result": dict(self.result) if self.result is not None else None,
}
@dataclass(frozen=True, slots=True)
class RuntimeWorkflowFailureMetadata:
"""Workflow metadata and event data for a failed runtime workflow."""
error_message: str
progress: RuntimeWorkflowProgress = "failed"
metadata_patch: RuntimeWorkflowMetadataPatch | None = None
@property
def error_preview(self) -> str:
"""Return the stored/evented error preview without exposing provider details."""
return truncate_runtime_workflow_text(self.error_message)
def workflow_metadata_patch(self) -> dict[str, object]:
"""Serialize to the existing durable failure metadata patch shape."""
return merge_runtime_workflow_metadata_patch(
{
"phase": "failed",
"progress": self.progress,
"error_message": self.error_preview,
},
self.metadata_patch,
)
def failed_event_data(self) -> dict[str, object]:
"""Serialize to the existing failed event payload shape."""
return {
"phase": "failed",
"progress": self.progress,
"error_message": self.error_preview,
}