AbstractRuntime 0.0.0__py3-none-any.whl → 0.2.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.
- abstractruntime/__init__.py +104 -2
- abstractruntime/core/__init__.py +26 -0
- abstractruntime/core/config.py +101 -0
- abstractruntime/core/models.py +282 -0
- abstractruntime/core/policy.py +166 -0
- abstractruntime/core/runtime.py +736 -0
- abstractruntime/core/spec.py +53 -0
- abstractruntime/core/vars.py +94 -0
- abstractruntime/identity/__init__.py +7 -0
- abstractruntime/identity/fingerprint.py +57 -0
- abstractruntime/integrations/__init__.py +11 -0
- abstractruntime/integrations/abstractcore/__init__.py +47 -0
- abstractruntime/integrations/abstractcore/effect_handlers.py +119 -0
- abstractruntime/integrations/abstractcore/factory.py +187 -0
- abstractruntime/integrations/abstractcore/llm_client.py +397 -0
- abstractruntime/integrations/abstractcore/logging.py +27 -0
- abstractruntime/integrations/abstractcore/tool_executor.py +168 -0
- abstractruntime/scheduler/__init__.py +13 -0
- abstractruntime/scheduler/convenience.py +324 -0
- abstractruntime/scheduler/registry.py +101 -0
- abstractruntime/scheduler/scheduler.py +431 -0
- abstractruntime/storage/__init__.py +25 -0
- abstractruntime/storage/artifacts.py +519 -0
- abstractruntime/storage/base.py +107 -0
- abstractruntime/storage/in_memory.py +119 -0
- abstractruntime/storage/json_files.py +208 -0
- abstractruntime/storage/ledger_chain.py +153 -0
- abstractruntime/storage/snapshots.py +217 -0
- abstractruntime-0.2.0.dist-info/METADATA +163 -0
- abstractruntime-0.2.0.dist-info/RECORD +32 -0
- {abstractruntime-0.0.0.dist-info → abstractruntime-0.2.0.dist-info}/licenses/LICENSE +3 -1
- abstractruntime-0.0.0.dist-info/METADATA +0 -89
- abstractruntime-0.0.0.dist-info/RECORD +0 -5
- {abstractruntime-0.0.0.dist-info → abstractruntime-0.2.0.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""abstractruntime.scheduler.convenience
|
|
2
|
+
|
|
3
|
+
Convenience functions for zero-config scheduler setup.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from ..core.models import RunState, RunStatus, WaitReason
|
|
12
|
+
from ..core.runtime import Runtime
|
|
13
|
+
from ..core.spec import WorkflowSpec
|
|
14
|
+
from ..storage.base import LedgerStore, RunStore
|
|
15
|
+
from .registry import WorkflowRegistry
|
|
16
|
+
from .scheduler import Scheduler, SchedulerStats
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass
|
|
20
|
+
class ScheduledRuntime:
|
|
21
|
+
"""A Runtime bundled with a Scheduler for zero-config operation.
|
|
22
|
+
|
|
23
|
+
This is a convenience wrapper that provides a simpler API for common use cases.
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
# Create with convenience function
|
|
27
|
+
sr = create_scheduled_runtime(
|
|
28
|
+
run_store=InMemoryRunStore(),
|
|
29
|
+
ledger_store=InMemoryLedgerStore(),
|
|
30
|
+
workflows=[my_workflow],
|
|
31
|
+
auto_start=True,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Simplest usage: run() does start + tick in one call
|
|
35
|
+
run_id, state = sr.run(my_workflow)
|
|
36
|
+
|
|
37
|
+
# If waiting for user input, respond
|
|
38
|
+
if state.status == RunStatus.WAITING:
|
|
39
|
+
state = sr.respond(run_id, {"answer": "yes"})
|
|
40
|
+
|
|
41
|
+
# Stop when done
|
|
42
|
+
sr.stop()
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
runtime: Runtime
|
|
46
|
+
scheduler: Scheduler
|
|
47
|
+
registry: WorkflowRegistry
|
|
48
|
+
|
|
49
|
+
def run(
|
|
50
|
+
self,
|
|
51
|
+
workflow: WorkflowSpec,
|
|
52
|
+
*,
|
|
53
|
+
vars: Optional[Dict[str, Any]] = None,
|
|
54
|
+
actor_id: Optional[str] = None,
|
|
55
|
+
max_steps: int = 100,
|
|
56
|
+
) -> tuple[str, RunState]:
|
|
57
|
+
"""Start and run a workflow until it blocks or completes.
|
|
58
|
+
|
|
59
|
+
This is the simplest way to execute a workflow. It:
|
|
60
|
+
1. Registers the workflow (if not already registered)
|
|
61
|
+
2. Starts a new run
|
|
62
|
+
3. Ticks until WAITING, COMPLETED, or FAILED
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
workflow: The workflow to run.
|
|
66
|
+
vars: Optional initial variables.
|
|
67
|
+
actor_id: Optional actor ID for provenance.
|
|
68
|
+
max_steps: Maximum steps before stopping (default: 100).
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
Tuple of (run_id, final_state).
|
|
72
|
+
|
|
73
|
+
Example:
|
|
74
|
+
run_id, state = sr.run(my_workflow)
|
|
75
|
+
if state.status == RunStatus.COMPLETED:
|
|
76
|
+
print(state.output)
|
|
77
|
+
"""
|
|
78
|
+
# Auto-register
|
|
79
|
+
if workflow.workflow_id not in self.registry:
|
|
80
|
+
self.registry.register(workflow)
|
|
81
|
+
|
|
82
|
+
run_id = self.runtime.start(workflow=workflow, vars=vars, actor_id=actor_id)
|
|
83
|
+
state = self.runtime.tick(workflow=workflow, run_id=run_id, max_steps=max_steps)
|
|
84
|
+
return run_id, state
|
|
85
|
+
|
|
86
|
+
def respond(
|
|
87
|
+
self,
|
|
88
|
+
run_id: str,
|
|
89
|
+
payload: Dict[str, Any],
|
|
90
|
+
) -> RunState:
|
|
91
|
+
"""Respond to a waiting run (user input or event).
|
|
92
|
+
|
|
93
|
+
This is the simplest way to resume a waiting run. It automatically
|
|
94
|
+
finds the wait_key from the run's waiting state.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
run_id: The run to respond to.
|
|
98
|
+
payload: The response payload.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The updated RunState.
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If the run is not waiting.
|
|
105
|
+
|
|
106
|
+
Example:
|
|
107
|
+
state = sr.respond(run_id, {"answer": "yes"})
|
|
108
|
+
"""
|
|
109
|
+
state = self.runtime.get_state(run_id)
|
|
110
|
+
|
|
111
|
+
if state.status != RunStatus.WAITING:
|
|
112
|
+
raise ValueError(f"Run '{run_id}' is not waiting (status={state.status.value})")
|
|
113
|
+
|
|
114
|
+
if state.waiting is None:
|
|
115
|
+
raise ValueError(f"Run '{run_id}' has no waiting state")
|
|
116
|
+
|
|
117
|
+
wait_key = state.waiting.wait_key
|
|
118
|
+
if wait_key is None:
|
|
119
|
+
raise ValueError(f"Run '{run_id}' has no wait_key")
|
|
120
|
+
|
|
121
|
+
return self.scheduler.resume_event(
|
|
122
|
+
run_id=run_id,
|
|
123
|
+
wait_key=wait_key,
|
|
124
|
+
payload=payload,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
def start(
|
|
128
|
+
self,
|
|
129
|
+
*,
|
|
130
|
+
workflow: WorkflowSpec,
|
|
131
|
+
vars: Optional[Dict[str, Any]] = None,
|
|
132
|
+
actor_id: Optional[str] = None,
|
|
133
|
+
) -> str:
|
|
134
|
+
"""Start a new run. Delegates to runtime.start()."""
|
|
135
|
+
# Auto-register workflow if not already registered
|
|
136
|
+
if workflow.workflow_id not in self.registry:
|
|
137
|
+
self.registry.register(workflow)
|
|
138
|
+
return self.runtime.start(workflow=workflow, vars=vars, actor_id=actor_id)
|
|
139
|
+
|
|
140
|
+
def tick(
|
|
141
|
+
self,
|
|
142
|
+
run_id: str,
|
|
143
|
+
*,
|
|
144
|
+
max_steps: int = 100,
|
|
145
|
+
) -> RunState:
|
|
146
|
+
"""Progress a run. Looks up workflow from registry."""
|
|
147
|
+
state = self.runtime.get_state(run_id)
|
|
148
|
+
workflow = self.registry.get_or_raise(state.workflow_id)
|
|
149
|
+
return self.runtime.tick(workflow=workflow, run_id=run_id, max_steps=max_steps)
|
|
150
|
+
|
|
151
|
+
def resume_event(
|
|
152
|
+
self,
|
|
153
|
+
*,
|
|
154
|
+
run_id: str,
|
|
155
|
+
wait_key: str,
|
|
156
|
+
payload: Dict[str, Any],
|
|
157
|
+
) -> RunState:
|
|
158
|
+
"""Resume a run waiting for an event. Delegates to scheduler.resume_event()."""
|
|
159
|
+
return self.scheduler.resume_event(
|
|
160
|
+
run_id=run_id,
|
|
161
|
+
wait_key=wait_key,
|
|
162
|
+
payload=payload,
|
|
163
|
+
)
|
|
164
|
+
|
|
165
|
+
def get_state(self, run_id: str) -> RunState:
|
|
166
|
+
"""Get run state. Delegates to runtime.get_state()."""
|
|
167
|
+
return self.runtime.get_state(run_id)
|
|
168
|
+
|
|
169
|
+
def get_ledger(self, run_id: str) -> list[dict[str, Any]]:
|
|
170
|
+
"""Get run ledger. Delegates to runtime.get_ledger()."""
|
|
171
|
+
return self.runtime.get_ledger(run_id)
|
|
172
|
+
|
|
173
|
+
def find_waiting_runs(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
wait_reason: Optional[WaitReason] = None,
|
|
177
|
+
workflow_id: Optional[str] = None,
|
|
178
|
+
limit: int = 100,
|
|
179
|
+
) -> List[RunState]:
|
|
180
|
+
"""Find waiting runs. Delegates to scheduler.find_waiting_runs()."""
|
|
181
|
+
return self.scheduler.find_waiting_runs(
|
|
182
|
+
wait_reason=wait_reason,
|
|
183
|
+
workflow_id=workflow_id,
|
|
184
|
+
limit=limit,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def stats(self) -> SchedulerStats:
|
|
189
|
+
"""Get scheduler statistics."""
|
|
190
|
+
return self.scheduler.stats
|
|
191
|
+
|
|
192
|
+
def resume_subworkflow_parent(
|
|
193
|
+
self,
|
|
194
|
+
*,
|
|
195
|
+
child_run_id: str,
|
|
196
|
+
child_output: Dict[str, Any],
|
|
197
|
+
) -> Optional[RunState]:
|
|
198
|
+
"""Resume a parent workflow when its child subworkflow completes.
|
|
199
|
+
|
|
200
|
+
Delegates to scheduler.resume_subworkflow_parent().
|
|
201
|
+
"""
|
|
202
|
+
return self.scheduler.resume_subworkflow_parent(
|
|
203
|
+
child_run_id=child_run_id,
|
|
204
|
+
child_output=child_output,
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
def cancel_run(self, run_id: str, *, reason: Optional[str] = None) -> RunState:
|
|
208
|
+
"""Cancel a run. Delegates to runtime.cancel_run()."""
|
|
209
|
+
return self.runtime.cancel_run(run_id, reason=reason)
|
|
210
|
+
|
|
211
|
+
def cancel_with_children(
|
|
212
|
+
self,
|
|
213
|
+
run_id: str,
|
|
214
|
+
*,
|
|
215
|
+
reason: Optional[str] = None,
|
|
216
|
+
) -> List[RunState]:
|
|
217
|
+
"""Cancel a run and all its descendant subworkflows.
|
|
218
|
+
|
|
219
|
+
Delegates to scheduler.cancel_with_children().
|
|
220
|
+
"""
|
|
221
|
+
return self.scheduler.cancel_with_children(run_id, reason=reason)
|
|
222
|
+
|
|
223
|
+
@property
|
|
224
|
+
def is_running(self) -> bool:
|
|
225
|
+
"""Check if scheduler is running."""
|
|
226
|
+
return self.scheduler.is_running
|
|
227
|
+
|
|
228
|
+
def start_scheduler(self) -> None:
|
|
229
|
+
"""Start the scheduler if not already running."""
|
|
230
|
+
if not self.scheduler.is_running:
|
|
231
|
+
self.scheduler.start()
|
|
232
|
+
|
|
233
|
+
def stop(self, timeout: float = 5.0) -> None:
|
|
234
|
+
"""Stop the scheduler."""
|
|
235
|
+
self.scheduler.stop(timeout=timeout)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def create_scheduled_runtime(
|
|
239
|
+
*,
|
|
240
|
+
run_store: Optional[RunStore] = None,
|
|
241
|
+
ledger_store: Optional[LedgerStore] = None,
|
|
242
|
+
artifact_store: Optional[Any] = None,
|
|
243
|
+
effect_policy: Optional[Any] = None,
|
|
244
|
+
workflows: Optional[List[WorkflowSpec]] = None,
|
|
245
|
+
effect_handlers: Optional[Dict] = None,
|
|
246
|
+
poll_interval_s: float = 1.0,
|
|
247
|
+
auto_start: bool = True,
|
|
248
|
+
on_run_resumed: Optional[Callable[[RunState], None]] = None,
|
|
249
|
+
on_run_failed: Optional[Callable[[str, Exception], None]] = None,
|
|
250
|
+
) -> ScheduledRuntime:
|
|
251
|
+
"""Create a Runtime with an integrated Scheduler.
|
|
252
|
+
|
|
253
|
+
This is the recommended way to set up AbstractRuntime. Defaults to
|
|
254
|
+
in-memory storage and auto-starting the scheduler for zero-config operation.
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
run_store: Storage backend for run state. Defaults to InMemoryRunStore.
|
|
258
|
+
ledger_store: Storage backend for the ledger. Defaults to InMemoryLedgerStore.
|
|
259
|
+
artifact_store: Optional artifact store for large payloads.
|
|
260
|
+
workflows: Optional list of workflows to pre-register.
|
|
261
|
+
effect_handlers: Optional custom effect handlers.
|
|
262
|
+
poll_interval_s: Scheduler poll interval in seconds (default: 1.0).
|
|
263
|
+
auto_start: If True (default), start the scheduler immediately.
|
|
264
|
+
on_run_resumed: Optional callback when a run is resumed.
|
|
265
|
+
on_run_failed: Optional callback when a run fails to resume.
|
|
266
|
+
|
|
267
|
+
Returns:
|
|
268
|
+
A ScheduledRuntime instance.
|
|
269
|
+
|
|
270
|
+
Example:
|
|
271
|
+
# Zero-config: just works
|
|
272
|
+
sr = create_scheduled_runtime()
|
|
273
|
+
run_id, state = sr.run(my_workflow)
|
|
274
|
+
sr.stop()
|
|
275
|
+
|
|
276
|
+
# With artifact store
|
|
277
|
+
sr = create_scheduled_runtime(
|
|
278
|
+
artifact_store=InMemoryArtifactStore(),
|
|
279
|
+
workflows=[my_workflow],
|
|
280
|
+
)
|
|
281
|
+
"""
|
|
282
|
+
# Import here to avoid circular imports
|
|
283
|
+
from ..storage.in_memory import InMemoryRunStore, InMemoryLedgerStore
|
|
284
|
+
|
|
285
|
+
# Use defaults if not provided
|
|
286
|
+
if run_store is None:
|
|
287
|
+
run_store = InMemoryRunStore()
|
|
288
|
+
if ledger_store is None:
|
|
289
|
+
ledger_store = InMemoryLedgerStore()
|
|
290
|
+
|
|
291
|
+
# Create registry and register workflows
|
|
292
|
+
registry = WorkflowRegistry()
|
|
293
|
+
if workflows:
|
|
294
|
+
for wf in workflows:
|
|
295
|
+
registry.register(wf)
|
|
296
|
+
|
|
297
|
+
# Create runtime with registry for subworkflow support
|
|
298
|
+
runtime = Runtime(
|
|
299
|
+
run_store=run_store,
|
|
300
|
+
ledger_store=ledger_store,
|
|
301
|
+
effect_handlers=effect_handlers,
|
|
302
|
+
workflow_registry=registry,
|
|
303
|
+
artifact_store=artifact_store,
|
|
304
|
+
effect_policy=effect_policy,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Create scheduler
|
|
308
|
+
scheduler = Scheduler(
|
|
309
|
+
runtime=runtime,
|
|
310
|
+
registry=registry,
|
|
311
|
+
poll_interval_s=poll_interval_s,
|
|
312
|
+
on_run_resumed=on_run_resumed,
|
|
313
|
+
on_run_failed=on_run_failed,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
# Optionally start
|
|
317
|
+
if auto_start:
|
|
318
|
+
scheduler.start()
|
|
319
|
+
|
|
320
|
+
return ScheduledRuntime(
|
|
321
|
+
runtime=runtime,
|
|
322
|
+
scheduler=scheduler,
|
|
323
|
+
registry=registry,
|
|
324
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""abstractruntime.scheduler.registry
|
|
2
|
+
|
|
3
|
+
Workflow registry for scheduler lookups.
|
|
4
|
+
|
|
5
|
+
The scheduler needs to map workflow_id -> WorkflowSpec to call tick().
|
|
6
|
+
This registry provides that mapping.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Dict, Optional
|
|
12
|
+
|
|
13
|
+
from ..core.spec import WorkflowSpec
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class WorkflowRegistry:
|
|
17
|
+
"""Registry mapping workflow_id to WorkflowSpec.
|
|
18
|
+
|
|
19
|
+
Used by the Scheduler to look up workflow specs when resuming runs.
|
|
20
|
+
|
|
21
|
+
Example:
|
|
22
|
+
registry = WorkflowRegistry()
|
|
23
|
+
registry.register(my_workflow)
|
|
24
|
+
registry.register(another_workflow)
|
|
25
|
+
|
|
26
|
+
# Later, scheduler can look up:
|
|
27
|
+
spec = registry.get("my_workflow_id")
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self) -> None:
|
|
31
|
+
self._workflows: Dict[str, WorkflowSpec] = {}
|
|
32
|
+
|
|
33
|
+
def register(self, workflow: WorkflowSpec) -> None:
|
|
34
|
+
"""Register a workflow spec.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
workflow: The workflow spec to register.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ValueError: If a workflow with the same ID is already registered.
|
|
41
|
+
"""
|
|
42
|
+
if workflow.workflow_id in self._workflows:
|
|
43
|
+
raise ValueError(
|
|
44
|
+
f"Workflow '{workflow.workflow_id}' is already registered. "
|
|
45
|
+
"Use unregister() first if you want to replace it."
|
|
46
|
+
)
|
|
47
|
+
self._workflows[workflow.workflow_id] = workflow
|
|
48
|
+
|
|
49
|
+
def unregister(self, workflow_id: str) -> None:
|
|
50
|
+
"""Unregister a workflow spec.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
workflow_id: The workflow ID to unregister.
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
KeyError: If the workflow is not registered.
|
|
57
|
+
"""
|
|
58
|
+
if workflow_id not in self._workflows:
|
|
59
|
+
raise KeyError(f"Workflow '{workflow_id}' is not registered.")
|
|
60
|
+
del self._workflows[workflow_id]
|
|
61
|
+
|
|
62
|
+
def get(self, workflow_id: str) -> Optional[WorkflowSpec]:
|
|
63
|
+
"""Get a workflow spec by ID.
|
|
64
|
+
|
|
65
|
+
Args:
|
|
66
|
+
workflow_id: The workflow ID to look up.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
The WorkflowSpec if found, None otherwise.
|
|
70
|
+
"""
|
|
71
|
+
return self._workflows.get(workflow_id)
|
|
72
|
+
|
|
73
|
+
def get_or_raise(self, workflow_id: str) -> WorkflowSpec:
|
|
74
|
+
"""Get a workflow spec by ID, raising if not found.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
workflow_id: The workflow ID to look up.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The WorkflowSpec.
|
|
81
|
+
|
|
82
|
+
Raises:
|
|
83
|
+
KeyError: If the workflow is not registered.
|
|
84
|
+
"""
|
|
85
|
+
spec = self._workflows.get(workflow_id)
|
|
86
|
+
if spec is None:
|
|
87
|
+
raise KeyError(
|
|
88
|
+
f"Workflow '{workflow_id}' is not registered. "
|
|
89
|
+
"Register it with registry.register(workflow) before starting the scheduler."
|
|
90
|
+
)
|
|
91
|
+
return spec
|
|
92
|
+
|
|
93
|
+
def list_ids(self) -> list[str]:
|
|
94
|
+
"""List all registered workflow IDs."""
|
|
95
|
+
return list(self._workflows.keys())
|
|
96
|
+
|
|
97
|
+
def __len__(self) -> int:
|
|
98
|
+
return len(self._workflows)
|
|
99
|
+
|
|
100
|
+
def __contains__(self, workflow_id: str) -> bool:
|
|
101
|
+
return workflow_id in self._workflows
|