orca-runtime-python 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.
- orca_runtime_python/__init__.py +69 -0
- orca_runtime_python/bus.py +227 -0
- orca_runtime_python/effects.py +216 -0
- orca_runtime_python/logging.py +161 -0
- orca_runtime_python/machine.py +875 -0
- orca_runtime_python/parser.py +894 -0
- orca_runtime_python/persistence.py +83 -0
- orca_runtime_python/types.py +279 -0
- orca_runtime_python-0.1.0.dist-info/METADATA +246 -0
- orca_runtime_python-0.1.0.dist-info/RECORD +12 -0
- orca_runtime_python-0.1.0.dist-info/WHEEL +5 -0
- orca_runtime_python-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Pluggable persistence adapters for Orca machine state.
|
|
3
|
+
|
|
4
|
+
PersistenceAdapter is a Protocol — any object implementing save/load/exists
|
|
5
|
+
can be used. FilePersistence is the bundled default (zero extra dependencies).
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from orca_runtime_python import FilePersistence
|
|
9
|
+
|
|
10
|
+
fp = FilePersistence("./runs")
|
|
11
|
+
await run_pipeline(machines, ctx, persistence=fp, run_id="exp-001")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
from typing import Any, Protocol, runtime_checkable
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@runtime_checkable
|
|
23
|
+
class PersistenceAdapter(Protocol):
|
|
24
|
+
"""
|
|
25
|
+
Protocol for Orca machine state persistence.
|
|
26
|
+
|
|
27
|
+
Implementations must support three operations keyed by run_id:
|
|
28
|
+
save, load (returns None if not found), and exists.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def save(self, run_id: str, snapshot: dict[str, Any]) -> None:
|
|
32
|
+
"""Persist snapshot under run_id. Overwrites any prior snapshot."""
|
|
33
|
+
...
|
|
34
|
+
|
|
35
|
+
def load(self, run_id: str) -> dict[str, Any] | None:
|
|
36
|
+
"""Return the snapshot for run_id, or None if it doesn't exist."""
|
|
37
|
+
...
|
|
38
|
+
|
|
39
|
+
def exists(self, run_id: str) -> bool:
|
|
40
|
+
"""Return True if a snapshot exists for run_id."""
|
|
41
|
+
...
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class FilePersistence:
|
|
45
|
+
"""
|
|
46
|
+
File-based persistence adapter.
|
|
47
|
+
|
|
48
|
+
Snapshots are stored as JSON files under base_dir:
|
|
49
|
+
{base_dir}/{run_id}.json
|
|
50
|
+
|
|
51
|
+
Writes are atomic: the file is written to a .tmp sibling and then
|
|
52
|
+
renamed, so a crash mid-write leaves the previous snapshot intact.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
fp = FilePersistence("./runs")
|
|
56
|
+
fp.save("exp-001", machine.snapshot())
|
|
57
|
+
snap = fp.load("exp-001") # dict or None
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, base_dir: str | Path):
|
|
61
|
+
self._base = Path(base_dir)
|
|
62
|
+
|
|
63
|
+
def _path(self, run_id: str) -> Path:
|
|
64
|
+
return self._base / f"{run_id}.json"
|
|
65
|
+
|
|
66
|
+
def save(self, run_id: str, snapshot: dict[str, Any]) -> None:
|
|
67
|
+
"""Write snapshot atomically. Creates base_dir if needed."""
|
|
68
|
+
self._base.mkdir(parents=True, exist_ok=True)
|
|
69
|
+
target = self._path(run_id)
|
|
70
|
+
tmp = target.with_suffix(".tmp")
|
|
71
|
+
tmp.write_text(json.dumps(snapshot, indent=2, default=str))
|
|
72
|
+
os.replace(tmp, target)
|
|
73
|
+
|
|
74
|
+
def load(self, run_id: str) -> dict[str, Any] | None:
|
|
75
|
+
"""Return snapshot dict, or None if run_id has no saved state."""
|
|
76
|
+
path = self._path(run_id)
|
|
77
|
+
if not path.exists():
|
|
78
|
+
return None
|
|
79
|
+
return json.loads(path.read_text())
|
|
80
|
+
|
|
81
|
+
def exists(self, run_id: str) -> bool:
|
|
82
|
+
"""True if a snapshot exists for run_id."""
|
|
83
|
+
return self._path(run_id).exists()
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Core type definitions for Orca runtime.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass, field
|
|
8
|
+
from typing import Any, TypedDict
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# Context type - represents the machine's state
|
|
12
|
+
Context = dict[str, Any]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass
|
|
16
|
+
class RegionDef:
|
|
17
|
+
"""Region definition inside a parallel state."""
|
|
18
|
+
name: str
|
|
19
|
+
states: list[StateDef] = field(default_factory=list)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class ParallelDef:
|
|
24
|
+
"""Parallel regions definition."""
|
|
25
|
+
regions: list[RegionDef] = field(default_factory=list)
|
|
26
|
+
sync: str | None = None # all-final, any-final, custom
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass
|
|
30
|
+
class InvokeDef:
|
|
31
|
+
"""Machine invocation definition."""
|
|
32
|
+
machine: str
|
|
33
|
+
input: dict[str, str] | None = None
|
|
34
|
+
on_done: str | None = None
|
|
35
|
+
on_error: str | None = None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class StateDef:
|
|
40
|
+
"""State definition in an Orca machine."""
|
|
41
|
+
name: str
|
|
42
|
+
is_initial: bool = False
|
|
43
|
+
is_final: bool = False
|
|
44
|
+
on_entry: str | None = None
|
|
45
|
+
on_exit: str | None = None
|
|
46
|
+
on_done: str | None = None
|
|
47
|
+
description: str | None = None
|
|
48
|
+
contains: list[StateDef] = field(default_factory=list)
|
|
49
|
+
parallel: ParallelDef | None = None
|
|
50
|
+
parent: str | None = None
|
|
51
|
+
timeout: dict[str, str] | None = None # {duration, target}
|
|
52
|
+
ignored_events: list[str] = field(default_factory=list)
|
|
53
|
+
invoke: InvokeDef | None = None
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class Transition:
|
|
58
|
+
"""Transition between states."""
|
|
59
|
+
source: str
|
|
60
|
+
event: str
|
|
61
|
+
target: str
|
|
62
|
+
guard: str | None = None
|
|
63
|
+
action: str | None = None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class GuardDef:
|
|
68
|
+
"""Guard condition definition."""
|
|
69
|
+
name: str
|
|
70
|
+
expression: GuardExpression
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@dataclass
|
|
74
|
+
class ActionSignature:
|
|
75
|
+
"""Action function signature."""
|
|
76
|
+
name: str
|
|
77
|
+
parameters: list[str]
|
|
78
|
+
return_type: str
|
|
79
|
+
has_effect: bool = False
|
|
80
|
+
effect_type: str | None = None
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class EffectDef:
|
|
85
|
+
"""Effect type declaration from the ## effects section."""
|
|
86
|
+
name: str
|
|
87
|
+
input: str # type description (e.g. '{ cmd: string, cwd: string }')
|
|
88
|
+
output: str # type description (e.g. '{ exit_code: int, stdout: string }')
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass
|
|
92
|
+
class MachineDef:
|
|
93
|
+
"""Complete Orca machine definition."""
|
|
94
|
+
name: str
|
|
95
|
+
context: dict[str, Any]
|
|
96
|
+
events: list[str]
|
|
97
|
+
states: list[StateDef]
|
|
98
|
+
transitions: list[Transition]
|
|
99
|
+
guards: dict[str, GuardExpression] = field(default_factory=dict)
|
|
100
|
+
actions: list[ActionSignature] = field(default_factory=list)
|
|
101
|
+
effects: list[EffectDef] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class GuardExpression:
|
|
105
|
+
"""Union type for guard expressions."""
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass
|
|
110
|
+
class GuardTrue(GuardExpression):
|
|
111
|
+
"""True literal guard."""
|
|
112
|
+
pass
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@dataclass
|
|
116
|
+
class GuardFalse(GuardExpression):
|
|
117
|
+
"""False literal guard."""
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@dataclass
|
|
122
|
+
class GuardCompare(GuardExpression):
|
|
123
|
+
"""Comparison guard: left op right"""
|
|
124
|
+
op: str # eq, ne, lt, gt, le, ge
|
|
125
|
+
left: VariableRef
|
|
126
|
+
right: ValueRef
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@dataclass
|
|
130
|
+
class GuardAnd(GuardExpression):
|
|
131
|
+
"""And guard: left and right"""
|
|
132
|
+
left: GuardExpression
|
|
133
|
+
right: GuardExpression
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass
|
|
137
|
+
class GuardOr(GuardExpression):
|
|
138
|
+
"""Or guard: left or right"""
|
|
139
|
+
left: GuardExpression
|
|
140
|
+
right: GuardExpression
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
@dataclass
|
|
144
|
+
class GuardNot(GuardExpression):
|
|
145
|
+
"""Not guard: not expr"""
|
|
146
|
+
expr: GuardExpression
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
@dataclass
|
|
150
|
+
class GuardNullcheck(GuardExpression):
|
|
151
|
+
"""Null check guard: expr is (not) null"""
|
|
152
|
+
expr: VariableRef
|
|
153
|
+
is_null: bool
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@dataclass
|
|
157
|
+
class VariableRef:
|
|
158
|
+
"""Variable path reference."""
|
|
159
|
+
path: list[str]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class ValueRef:
|
|
164
|
+
"""Value literal reference."""
|
|
165
|
+
type: str # string, number, boolean, null
|
|
166
|
+
value: str | int | float | bool | None
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@dataclass
|
|
170
|
+
class Effect:
|
|
171
|
+
"""Represents an effect (async operation) to be executed."""
|
|
172
|
+
type: str
|
|
173
|
+
payload: dict[str, Any] = field(default_factory=dict)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@dataclass
|
|
177
|
+
class EffectResult:
|
|
178
|
+
"""Result of an effect execution."""
|
|
179
|
+
status: EffectStatus
|
|
180
|
+
data: Any = None
|
|
181
|
+
error: str | None = None
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
class EffectStatus:
|
|
185
|
+
"""Effect execution status."""
|
|
186
|
+
SUCCESS = "success"
|
|
187
|
+
FAILURE = "failure"
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class StateValue:
|
|
191
|
+
"""
|
|
192
|
+
Represents the current state of a machine.
|
|
193
|
+
Supports both simple (string) and compound (nested) states.
|
|
194
|
+
"""
|
|
195
|
+
|
|
196
|
+
def __init__(self, value: str | dict[str, Any]):
|
|
197
|
+
self.value = value
|
|
198
|
+
|
|
199
|
+
def __str__(self) -> str:
|
|
200
|
+
if isinstance(self.value, str):
|
|
201
|
+
return self.value
|
|
202
|
+
return self._format_compound()
|
|
203
|
+
|
|
204
|
+
def __repr__(self) -> str:
|
|
205
|
+
return f"StateValue({self.value!r})"
|
|
206
|
+
|
|
207
|
+
def __eq__(self, other: object) -> bool:
|
|
208
|
+
if isinstance(other, StateValue):
|
|
209
|
+
return self.value == other.value
|
|
210
|
+
if isinstance(other, str):
|
|
211
|
+
return self.value == other
|
|
212
|
+
if isinstance(other, dict):
|
|
213
|
+
return self.value == other
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
def _format_compound(self) -> str:
|
|
217
|
+
"""Format compound state as dot-notation string."""
|
|
218
|
+
if isinstance(self.value, str):
|
|
219
|
+
return self.value
|
|
220
|
+
|
|
221
|
+
def format_recursive(d: dict[str, Any], prefix: str = "") -> str:
|
|
222
|
+
parts = []
|
|
223
|
+
for key, val in d.items():
|
|
224
|
+
if isinstance(val, dict) and val:
|
|
225
|
+
# Non-empty nested dict, recurse deeper
|
|
226
|
+
parts.append(format_recursive(val, prefix + key + "."))
|
|
227
|
+
else:
|
|
228
|
+
# Empty dict or non-dict - key is a leaf state
|
|
229
|
+
parts.append(prefix + key)
|
|
230
|
+
return ", ".join(parts) if parts else str(d)
|
|
231
|
+
|
|
232
|
+
return format_recursive(self.value)
|
|
233
|
+
|
|
234
|
+
def is_compound(self) -> bool:
|
|
235
|
+
"""Check if this is a compound (nested) state."""
|
|
236
|
+
return isinstance(self.value, dict)
|
|
237
|
+
|
|
238
|
+
def leaf(self) -> str:
|
|
239
|
+
"""Get the leaf state name from compound state."""
|
|
240
|
+
if isinstance(self.value, str):
|
|
241
|
+
return self.value
|
|
242
|
+
if isinstance(self.value, dict):
|
|
243
|
+
# Recursively find the deepest state
|
|
244
|
+
for key, val in self.value.items():
|
|
245
|
+
if isinstance(val, dict) and val:
|
|
246
|
+
# Non-empty nested dict, recurse
|
|
247
|
+
result = StateValue(val).leaf()
|
|
248
|
+
if result:
|
|
249
|
+
return result
|
|
250
|
+
else:
|
|
251
|
+
# Empty dict or non-dict value - 'key' is the leaf
|
|
252
|
+
return key
|
|
253
|
+
return str(self.value)
|
|
254
|
+
|
|
255
|
+
def leaves(self) -> list[str]:
|
|
256
|
+
"""Get all leaf state names (one per active region in parallel states)."""
|
|
257
|
+
if isinstance(self.value, str):
|
|
258
|
+
return [self.value]
|
|
259
|
+
|
|
260
|
+
result: list[str] = []
|
|
261
|
+
|
|
262
|
+
def collect_leaves(obj: dict[str, Any]) -> None:
|
|
263
|
+
for key, val in obj.items():
|
|
264
|
+
if isinstance(val, dict) and val:
|
|
265
|
+
collect_leaves(val)
|
|
266
|
+
else:
|
|
267
|
+
result.append(key)
|
|
268
|
+
|
|
269
|
+
if isinstance(self.value, dict):
|
|
270
|
+
collect_leaves(self.value)
|
|
271
|
+
return result if result else [str(self.value)]
|
|
272
|
+
|
|
273
|
+
def parent_names(self) -> list[str]:
|
|
274
|
+
"""Get all parent state names (for nested states)."""
|
|
275
|
+
if isinstance(self.value, str):
|
|
276
|
+
return []
|
|
277
|
+
if isinstance(self.value, dict):
|
|
278
|
+
return list(self.value.keys())
|
|
279
|
+
return []
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: orca-runtime-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python async runtime for Orca state machines
|
|
5
|
+
License: Apache-2.0
|
|
6
|
+
Project-URL: Homepage, https://github.com/orca-lang/orca-lang
|
|
7
|
+
Project-URL: Repository, https://github.com/orca-lang/orca-lang-python
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pytest-asyncio>=0.21; extra == "dev"
|
|
21
|
+
Requires-Dist: black>=23.0; extra == "dev"
|
|
22
|
+
Requires-Dist: mypy>=1.0; extra == "dev"
|
|
23
|
+
Requires-Dist: ruff>=0.1; extra == "dev"
|
|
24
|
+
|
|
25
|
+
# Orca Runtime Python
|
|
26
|
+
|
|
27
|
+
A first-class Python async runtime for [Orca](https://github.com/orca-lang/orca-lang) state machines.
|
|
28
|
+
|
|
29
|
+
## Overview
|
|
30
|
+
|
|
31
|
+
Orca is a state machine language designed for LLM code generation. This package provides a Python runtime that executes Orca machine definitions with:
|
|
32
|
+
|
|
33
|
+
- **Async-first** - Native `async/await` throughout
|
|
34
|
+
- **Event bus integration** - Decoupled pub/sub for agentic systems
|
|
35
|
+
- **Effect system** - Typed async operations with handler registration
|
|
36
|
+
- **Hierarchical states** - Nested/compound state support
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install orca-runtime-python
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quick Start
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from orca_runtime_python import (
|
|
49
|
+
parse_orca,
|
|
50
|
+
OrcaMachine,
|
|
51
|
+
get_event_bus,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Define an Orca machine
|
|
55
|
+
orca_source = """
|
|
56
|
+
machine OrderProcessor
|
|
57
|
+
|
|
58
|
+
context {
|
|
59
|
+
order_id: ""
|
|
60
|
+
status: "pending"
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
state pending [initial] "Order received"
|
|
64
|
+
state fulfilled [final] "Order complete"
|
|
65
|
+
|
|
66
|
+
transitions {
|
|
67
|
+
pending + ORDER_PLACED -> pending
|
|
68
|
+
pending + ORDER_FULFILLED -> fulfilled
|
|
69
|
+
}
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
async def main():
|
|
73
|
+
# Parse and create machine
|
|
74
|
+
machine_def = parse_orca(orca_source)
|
|
75
|
+
machine = OrcaMachine(machine_def)
|
|
76
|
+
|
|
77
|
+
# Register effect handlers
|
|
78
|
+
bus = get_event_bus()
|
|
79
|
+
bus.register_effect_handler("Effect", lambda e: EffectResult(
|
|
80
|
+
status="success", data=e.payload
|
|
81
|
+
))
|
|
82
|
+
|
|
83
|
+
# Start and run
|
|
84
|
+
await machine.start()
|
|
85
|
+
print(f"Initial state: {machine.state}")
|
|
86
|
+
|
|
87
|
+
await machine.send("ORDER_FULFILLED")
|
|
88
|
+
print(f"After event: {machine.state}")
|
|
89
|
+
|
|
90
|
+
await machine.stop()
|
|
91
|
+
|
|
92
|
+
asyncio.run(main())
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Architecture
|
|
96
|
+
|
|
97
|
+
```
|
|
98
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
99
|
+
│ OrcaMachine │
|
|
100
|
+
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────┐ │
|
|
101
|
+
│ │ State Store │ │ Transition │ │ Effect Executor │ │
|
|
102
|
+
│ │ (current) │ │ Evaluator │ │ (async handlers) │ │
|
|
103
|
+
│ └──────────────┘ └──────────────┘ └──────────────────────┘ │
|
|
104
|
+
│ │ │ │ │
|
|
105
|
+
│ └────────────────┼──────────────────────┘ │
|
|
106
|
+
│ ▼ │
|
|
107
|
+
│ ┌──────────────────┐ │
|
|
108
|
+
│ │ EventBus │ (pub/sub) │
|
|
109
|
+
│ └──────────────────┘ │
|
|
110
|
+
└─────────────────────────────────────────────────────────────────┘
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
## Core Components
|
|
114
|
+
|
|
115
|
+
### OrcaMachine
|
|
116
|
+
|
|
117
|
+
The main runtime class that executes state machines:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
machine = OrcaMachine(
|
|
121
|
+
definition=machine_def,
|
|
122
|
+
event_bus=get_event_bus(),
|
|
123
|
+
context={"order_id": "123"},
|
|
124
|
+
on_transition=lambda old, new: print(f"{old} -> {new}"),
|
|
125
|
+
)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### EventBus
|
|
129
|
+
|
|
130
|
+
Async event bus with pub/sub and request/response:
|
|
131
|
+
|
|
132
|
+
```python
|
|
133
|
+
bus = get_event_bus()
|
|
134
|
+
|
|
135
|
+
# Subscribe to events
|
|
136
|
+
bus.subscribe(EventType.STATE_CHANGED, handler)
|
|
137
|
+
|
|
138
|
+
# Publish events
|
|
139
|
+
await bus.publish(Event(
|
|
140
|
+
type=EventType.STATE_CHANGED,
|
|
141
|
+
source="OrderProcessor",
|
|
142
|
+
payload={"from": "pending", "to": "fulfilled"},
|
|
143
|
+
))
|
|
144
|
+
|
|
145
|
+
# Request/response pattern
|
|
146
|
+
response = await bus.request_response(
|
|
147
|
+
request_type=EventType.SCHEDULING_QUERY,
|
|
148
|
+
request_payload={"type": "availability"},
|
|
149
|
+
response_type=EventType.SCHEDULING_QUERY_RESPONSE,
|
|
150
|
+
)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Effect Handlers
|
|
154
|
+
|
|
155
|
+
Register async handlers for effect types:
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
async def handle_narrative(effect: Effect) -> EffectResult:
|
|
159
|
+
narrative = await generate_narrative(effect.payload)
|
|
160
|
+
return EffectResult(status="success", data={"narrative": narrative})
|
|
161
|
+
|
|
162
|
+
bus.register_effect_handler("NarrativeRequest", handle_narrative)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Orca DSL Syntax
|
|
166
|
+
|
|
167
|
+
```orca
|
|
168
|
+
machine GameEngine
|
|
169
|
+
|
|
170
|
+
context {
|
|
171
|
+
health: int = 100
|
|
172
|
+
inventory: string[]
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
events {
|
|
176
|
+
start_game
|
|
177
|
+
attack
|
|
178
|
+
heal
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
state idle [initial] {
|
|
182
|
+
description: "Waiting for player input"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
state combat {
|
|
186
|
+
description: "In combat"
|
|
187
|
+
|
|
188
|
+
state fighting [initial] {
|
|
189
|
+
description: "Actively fighting"
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
state defending {
|
|
193
|
+
description: "Blocking attacks"
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
state game_over [final] {
|
|
198
|
+
description: "Game ended"
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
guards {
|
|
202
|
+
can_heal: ctx.health < 100
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
transitions {
|
|
206
|
+
idle + start_game -> combat : start_combat
|
|
207
|
+
combat + attack -> combat : resolve_attack
|
|
208
|
+
combat + heal [can_heal] -> combat : apply_heal
|
|
209
|
+
combat + attack [health <= 0] -> game_over : end_game
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
actions {
|
|
213
|
+
start_combat: (ctx: Context) -> Context
|
|
214
|
+
resolve_attack: (ctx: Context) -> Context + Effect<DamageRequest>
|
|
215
|
+
apply_heal: (ctx: Context) -> Context
|
|
216
|
+
end_game: (ctx: Context) -> Context
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Hierarchy
|
|
221
|
+
|
|
222
|
+
```
|
|
223
|
+
orca-runtime-python/
|
|
224
|
+
├── orca_runtime_python/ # Main package
|
|
225
|
+
│ ├── __init__.py
|
|
226
|
+
│ ├── types.py # Core types
|
|
227
|
+
│ ├── parser.py # DSL parser
|
|
228
|
+
│ ├── machine.py # OrcaMachine runtime
|
|
229
|
+
│ ├── bus.py # EventBus
|
|
230
|
+
│ └── effects.py # Effect system
|
|
231
|
+
├── pyproject.toml
|
|
232
|
+
└── README.md
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Relationship to Other Implementations
|
|
236
|
+
|
|
237
|
+
| Package | Language | Purpose |
|
|
238
|
+
|---------|----------|---------|
|
|
239
|
+
| `orca` (npm) | TypeScript/JS | Core implementation (parser, verifier, compiler) |
|
|
240
|
+
| `orca-runtime-python` | Python | Python async runtime |
|
|
241
|
+
|
|
242
|
+
The Orca language is defined once and implemented across platforms. The Python runtime executes machines compiled from Orca DSL.
|
|
243
|
+
|
|
244
|
+
## License
|
|
245
|
+
|
|
246
|
+
Apache 2.0
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
orca_runtime_python/__init__.py,sha256=RgEnSjy5mJO_M6_3exaoWmmEK_5aXYPRMS82naWcebk,1139
|
|
2
|
+
orca_runtime_python/bus.py,sha256=kCdMwm3O97wlX5hr3_aK-YkhABO7Z0M-eaYPYJAIa9g,7171
|
|
3
|
+
orca_runtime_python/effects.py,sha256=eAkm2JdHSH5wUm8ZQjBf7ncgMI1iw7xq9bKi5X1AJEM,5649
|
|
4
|
+
orca_runtime_python/logging.py,sha256=Bnw_PCGlMQaDbti3UNMudhj0RcOckF_7YYt_5yWatGY,4360
|
|
5
|
+
orca_runtime_python/machine.py,sha256=6gccjFkD96Wpzo1VWXGTbS7gkcIzdu3w88ZTu1ey_C8,33126
|
|
6
|
+
orca_runtime_python/parser.py,sha256=wu9Dlv7O7xOurRr3--lO5X-v57LkVvkbtAfcc3ez8cg,30714
|
|
7
|
+
orca_runtime_python/persistence.py,sha256=lsQlvGXPelZ5u2Ote-OegtmE6Rk10fa-qj9Dkg-zl9E,2590
|
|
8
|
+
orca_runtime_python/types.py,sha256=YlYcyvGHI9myHq5XDinGTjPIKadyG4kuGQvMPtU0eCo,7232
|
|
9
|
+
orca_runtime_python-0.1.0.dist-info/METADATA,sha256=HUc6tUpkq4KfD0Qs1AjP6MWpHzW72gljNrABKBj4sCs,7164
|
|
10
|
+
orca_runtime_python-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
11
|
+
orca_runtime_python-0.1.0.dist-info/top_level.txt,sha256=r7KIKsP_6Io6KiMqlBoNEc4IwzaE7O3VTG-PSEu0wVY,20
|
|
12
|
+
orca_runtime_python-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
orca_runtime_python
|