Skip to content

Commit 7dedc6a

Browse files
JohnRichard4096Copilot
andcommitted
Update: README and tests.
Co-authored-by: Copilot <copilot@github.com>
1 parent 10c003b commit 7dedc6a

2 files changed

Lines changed: 295 additions & 6 deletions

File tree

README.md

Lines changed: 47 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,51 @@
2727
2828
</center>
2929

30-
## TODO
30+
AmritaSense is a **general-purpose workflow orchestration engine** that replaces traditional graph-based models with an **instruction set architecture**—treating workflows not as nodes-and-edges diagrams, but as programmable execution streams driven by a lightweight virtual machine.
3131

32-
- [x] Main project
33-
- [x] [Documentation](https://sense.amritabot.com)
34-
- [ ] Workflow Debugger (Planned, in progress)
35-
- [x] Tests (In progress)
36-
- [ ] DEMOS (In progress)
32+
## Why AmritaSense?
33+
34+
Most workflow engines force you into a graph mindset: define nodes, connect edges, manage state objects. AmritaSense takes a different path. You compose nodes and control flow just like writing ordinary code—the engine compiles them into a linear instruction sequence, then executes them step by step. The result: **zero scheduling overhead, native interrupt support, and the expressive power of assembly-level control flow.**
35+
36+
## Core Features
37+
38+
- **Complete Instruction Set**`IF/ELIF/ELSE`, `WHILE/DO-WHILE`, `GOTO`/`CALL`, `TRY/CATCH/THEN/FIN`, `NOP`, `INTERRUPT`. All control flow is first-class, not simulated through graph routing.
39+
- **VM-Style Execution**—A program counter (`PointerVector`) and call stack drive execution. Jumps are integer operations, not graph traversals.
40+
- **Async-Native Suspend/Resume**—Two `Future` callbacks enable full workflow interruption at any node boundary. Built for debuggers and human-in-the-loop systems.
41+
- **Declarative Dependency Injection**—Nodes declare dependencies via function signatures. The engine resolves them at runtime with type matching and concurrent resolution.
42+
- **Ultra Lightweight**—Core interpreter is ~300 lines. Compiles 100,000 nodes in ~200ms. Runs anywhere from Raspberry Pi to cloud.
43+
- **Self-Compile Instructions**—Extend the instruction set with `SelfCompileInstruction`. Compile-time expansion, zero runtime overhead.
44+
45+
## Installation
46+
47+
```bash
48+
pip install amrita-sense
49+
```
50+
51+
## Quick Look
52+
53+
```python
54+
from amrita_sense import Node, WorkflowInterpreter as WorkflowPC, IF, NOP
55+
56+
@Node()
57+
def condition() -> bool: return True
58+
59+
@Node()
60+
def action(): print("Done")
61+
62+
flow = IF(condition, action) >> NOP
63+
pc = WorkflowPC(flow.render())
64+
pc.run_sync()
65+
```
66+
67+
## Documentation
68+
69+
Full guides, concept explanations, and API reference at **[sense.amritabot.com](https://sense.amritabot.com)**.
70+
71+
## Contributing
72+
73+
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) and our [Code of Conduct](CODE_OF_CONDUCT.md).
74+
75+
## License
76+
77+
LGPL V2. See [LICENSE](LICENSE).

tests/test_runtime.py

Lines changed: 248 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
1+
from typing import Any
2+
13
import pytest
4+
from amrita_core.hook.matcher import DependsFactory, MatcherFactory
25

6+
from amrita_sense.exceptions import (
7+
DependsInjectFailed,
8+
DependsResolveFailed,
9+
InterruptNotice,
10+
NullPointerException,
11+
)
312
from amrita_sense.node.core import NodeCompose
413
from amrita_sense.node.wrapper import Node as NodeDecorator
514
from amrita_sense.runtime.workflow import WorkflowInterpreter
15+
from amrita_sense.types import PointerVector
616

717

818
class TestWorkflowInterpreter:
@@ -62,3 +72,241 @@ def test_node():
6272

6373
graph = interpreter.get_graph()
6474
assert graph is rendered
75+
76+
@pytest.mark.asyncio
77+
async def test_run_step_by_runtime_args_unresolved_raises(self):
78+
@NodeDecorator()
79+
def simple_node():
80+
return "hello"
81+
82+
rendered = NodeCompose(simple_node).render()
83+
interpreter = WorkflowInterpreter(rendered)
84+
interpreter._ava_args = (interpreter, DependsFactory(lambda: None))
85+
86+
original = MatcherFactory._do_runtime_resolve
87+
88+
async def _fake_runtime_resolve(*args: Any, **kwargs: Any) -> bool:
89+
return False
90+
91+
MatcherFactory._do_runtime_resolve = classmethod(_fake_runtime_resolve) # pyright: ignore[reportAttributeAccessIssue]
92+
try:
93+
with pytest.raises(
94+
RuntimeError, match="Runtime arguments cannot be resolved"
95+
):
96+
await interpreter.run_step_by().__anext__()
97+
finally:
98+
MatcherFactory._do_runtime_resolve = original
99+
100+
@pytest.mark.asyncio
101+
async def test_run_step_by_interrupt_notice_clears_state(self):
102+
@NodeDecorator()
103+
def interrupt_node():
104+
raise InterruptNotice("stop")
105+
106+
rendered = NodeCompose(interrupt_node).render()
107+
interpreter = WorkflowInterpreter(rendered)
108+
109+
with pytest.raises(StopAsyncIteration):
110+
await interpreter.run_step_by().__anext__()
111+
112+
assert not interpreter._pointer
113+
assert not interpreter._ret_addr_stack
114+
assert not interpreter._jump_marked
115+
116+
@pytest.mark.asyncio
117+
async def test_call_raises_depends_resolve_failed(self):
118+
@NodeDecorator()
119+
def simple_node():
120+
return "hello"
121+
122+
rendered = NodeCompose(simple_node).render()
123+
interpreter = WorkflowInterpreter(rendered)
124+
125+
original = MatcherFactory._resolve_dependencies
126+
MatcherFactory._resolve_dependencies = classmethod(
127+
lambda *args, **kwargs: (False, [], {}, {}) # pyright: ignore[reportAttributeAccessIssue]
128+
)
129+
try:
130+
with pytest.raises(DependsResolveFailed):
131+
await interpreter._call()
132+
finally:
133+
MatcherFactory._resolve_dependencies = original
134+
135+
@pytest.mark.asyncio
136+
async def test_call_raises_depends_inject_failed(self):
137+
@NodeDecorator()
138+
def simple_node():
139+
return "hello"
140+
141+
rendered = NodeCompose(simple_node).render()
142+
interpreter = WorkflowInterpreter(rendered)
143+
144+
original_resolve = MatcherFactory._resolve_dependencies
145+
original_runtime = MatcherFactory._do_runtime_resolve
146+
MatcherFactory._resolve_dependencies = classmethod(
147+
lambda *args, **kwargs: (True, [], {}, {"x": DependsFactory(lambda: None)}) # pyright: ignore[reportAttributeAccessIssue]
148+
)
149+
150+
async def _fake_runtime_resolve(*args: Any, **kwargs: Any) -> bool:
151+
return False
152+
153+
MatcherFactory._do_runtime_resolve = classmethod(_fake_runtime_resolve) # pyright: ignore[reportAttributeAccessIssue]
154+
try:
155+
with pytest.raises(DependsInjectFailed):
156+
await interpreter._call()
157+
finally:
158+
MatcherFactory._resolve_dependencies = original_resolve
159+
MatcherFactory._do_runtime_resolve = original_runtime
160+
161+
@pytest.mark.asyncio
162+
async def test_call_uses_async_and_sync_no_wrap(self):
163+
@NodeDecorator(wrap_to_async=False)
164+
async def async_node():
165+
return "async"
166+
167+
@NodeDecorator(wrap_to_async=False)
168+
def sync_node():
169+
return "sync"
170+
171+
rendered_async = NodeCompose(async_node).render()
172+
interpreter_async = WorkflowInterpreter(rendered_async)
173+
interpreter_async._pointer = PointerVector([0])
174+
assert await interpreter_async._call() == "async"
175+
176+
rendered_sync = NodeCompose(sync_node).render()
177+
interpreter_sync = WorkflowInterpreter(rendered_sync)
178+
interpreter_sync._pointer = PointerVector([0])
179+
assert await interpreter_sync._call() == "sync"
180+
181+
def test_find_addr_or_none_returns_none_for_invalid_path(self):
182+
@NodeDecorator()
183+
def simple_node():
184+
return "hello"
185+
186+
rendered = NodeCompose(simple_node).render()
187+
interpreter = WorkflowInterpreter(rendered)
188+
189+
assert interpreter._find_addr_or_none([0, 0, 0]) is None
190+
191+
def test_advance_pointer_container_sibling_and_nested(self):
192+
@NodeDecorator()
193+
def simple_node():
194+
return "hello"
195+
196+
rendered = NodeCompose(simple_node, NodeCompose(simple_node)).render()
197+
interpreter = WorkflowInterpreter(rendered)
198+
interpreter._pointer = PointerVector([0])
199+
200+
assert interpreter._advance_pointer()
201+
assert interpreter._pointer == PointerVector([1, 0])
202+
203+
def test_advance_pointer_backtrack_to_nested_sibling(self):
204+
@NodeDecorator()
205+
def simple_node():
206+
return "hello"
207+
208+
rendered = NodeCompose(
209+
NodeCompose(simple_node), NodeCompose(simple_node)
210+
).render()
211+
interpreter = WorkflowInterpreter(rendered)
212+
interpreter._pointer = PointerVector([0, 0])
213+
214+
assert interpreter._advance_pointer()
215+
assert interpreter._pointer == PointerVector([1, 0])
216+
217+
@pytest.mark.asyncio
218+
async def test_call_offset_and_call_near_preserve_pointer(self):
219+
@NodeDecorator()
220+
def target_node():
221+
return "target"
222+
223+
workflow = NodeCompose(target_node, target_node)
224+
rendered = workflow.render()
225+
interpreter = WorkflowInterpreter(rendered)
226+
227+
interpreter._pointer = PointerVector([0])
228+
result = await interpreter.call_offset(1)
229+
assert result == "target"
230+
assert interpreter._pointer == PointerVector([0])
231+
232+
interpreter._pointer = PointerVector([0])
233+
result = await interpreter.call_near(1)
234+
assert result == "target"
235+
assert interpreter._pointer == PointerVector([0])
236+
237+
def test_jump_methods_modify_pointer_and_raise(self):
238+
@NodeDecorator()
239+
def simple_node():
240+
return "hello"
241+
242+
interpreter = WorkflowInterpreter(NodeCompose(simple_node).render())
243+
interpreter._pointer = PointerVector([1, 2])
244+
245+
interpreter.jump_to_top(0)
246+
assert interpreter._pointer == PointerVector([0])
247+
248+
interpreter = WorkflowInterpreter(NodeCompose(simple_node).render())
249+
interpreter._pointer = PointerVector([1, 2])
250+
interpreter.jump_offset_top(1)
251+
assert interpreter._pointer == PointerVector([2])
252+
253+
interpreter = WorkflowInterpreter(NodeCompose(simple_node).render())
254+
interpreter._pointer = PointerVector([1, 2])
255+
interpreter.jump_far_ptr([0, 1])
256+
assert interpreter._pointer == PointerVector([0, 1])
257+
258+
interpreter = WorkflowInterpreter(NodeCompose(simple_node).render())
259+
with pytest.raises(NullPointerException):
260+
interpreter.jump_to([99])
261+
262+
@pytest.mark.asyncio
263+
async def test_call_raises_on_nodecompose(self):
264+
@NodeDecorator()
265+
def simple_node():
266+
return "hello"
267+
268+
rendered = NodeCompose(NodeCompose(simple_node)).render()
269+
interpreter = WorkflowInterpreter(rendered)
270+
interpreter._pointer = PointerVector([0])
271+
272+
with pytest.raises(RuntimeError):
273+
await interpreter._call()
274+
275+
def test_advance_pointer_empty_and_invalid(self):
276+
@NodeDecorator()
277+
def simple_node():
278+
return "hello"
279+
280+
interpreter = WorkflowInterpreter(NodeCompose(simple_node).render())
281+
interpreter._pointer = PointerVector()
282+
assert not interpreter._advance_pointer()
283+
284+
interpreter._pointer = PointerVector([0, 0])
285+
assert not interpreter._advance_pointer()
286+
287+
def test_advance_pointer_nested_and_backtrack(self):
288+
@NodeDecorator()
289+
def simple_node():
290+
return "hello"
291+
292+
rendered = NodeCompose(NodeCompose(simple_node), simple_node).render()
293+
interpreter = WorkflowInterpreter(rendered)
294+
295+
interpreter._pointer = PointerVector([0])
296+
assert interpreter._advance_pointer()
297+
assert interpreter._pointer == PointerVector([0, 0])
298+
299+
interpreter._pointer = PointerVector([0, 0])
300+
assert interpreter._advance_pointer()
301+
assert interpreter._pointer == PointerVector([1])
302+
303+
def test_advance_pointer_backtrack_from_nested_end(self):
304+
@NodeDecorator()
305+
def simple_node():
306+
return "hello"
307+
308+
rendered = NodeCompose(NodeCompose(simple_node)).render()
309+
interpreter = WorkflowInterpreter(rendered)
310+
interpreter._pointer = PointerVector([0, 0])
311+
312+
assert not interpreter._advance_pointer()

0 commit comments

Comments
 (0)