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.
@@ -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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ orca_runtime_python