Skip to content

Commit e5e1881

Browse files
authored
feat: Python hermes-plugin — DAG workflow engine with 3-tier memory
Squash merge of feat/hermes-plugin branch. - DAG engine (Kahn's algorithm) with parallel scheduling - 3-tier memory: Working (LRU) → Episodic (FTS5) → Semantic (pattern extraction) - SQLite WAL persistence with composite PK, 8-version migrations - Strict FSM state machine with retry + exponential backoff - 81 tests (25 core + 56 edge case checks) - Cursor Bugbot review issues addressed - CI: Python pipeline + paths filter
1 parent 4c04e85 commit e5e1881

24 files changed

Lines changed: 3976 additions & 109 deletions

.github/workflows/ci.yml

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,28 @@ on:
77
branches: [main]
88

99
jobs:
10+
# ── Detect changes ──────────────────────────────────────────
11+
changes:
12+
runs-on: ubuntu-latest
13+
outputs:
14+
openclaw: ${{ steps.filter.outputs.openclaw }}
15+
hermes: ${{ steps.filter.outputs.hermes }}
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: dorny/paths-filter@v3
19+
id: filter
20+
with:
21+
filters: |
22+
openclaw:
23+
- 'openclaw-plugin/**'
24+
hermes:
25+
- 'hermes-plugin/**'
26+
27+
# ── TypeScript (openclaw-plugin) ──────────────────────────────
1028
typecheck:
1129
runs-on: ubuntu-latest
30+
needs: changes
31+
if: needs.changes.outputs.openclaw == 'true' || github.event_name == 'push'
1232
steps:
1333
- uses: actions/checkout@v4
1434

@@ -29,6 +49,7 @@ jobs:
2949
build:
3050
runs-on: ubuntu-latest
3151
needs: typecheck
52+
if: needs.typecheck.result == 'success'
3253
steps:
3354
- uses: actions/checkout@v4
3455

@@ -53,6 +74,7 @@ jobs:
5374
lint:
5475
runs-on: ubuntu-latest
5576
needs: typecheck
77+
if: needs.typecheck.result == 'success'
5678
steps:
5779
- uses: actions/checkout@v4
5880

@@ -69,3 +91,109 @@ jobs:
6991
- name: Lint
7092
working-directory: openclaw-plugin
7193
run: npx tsc --noEmit --pretty 2>&1 | grep "error TS" && exit 1 || echo "No errors"
94+
95+
# ── Python (hermes-plugin) ────────────────────────────────────
96+
python-test:
97+
runs-on: ubuntu-latest
98+
needs: changes
99+
if: needs.changes.outputs.hermes == 'true' || github.event_name == 'push'
100+
defaults:
101+
run:
102+
working-directory: hermes-plugin
103+
steps:
104+
- uses: actions/checkout@v4
105+
106+
- name: Set up Python
107+
uses: actions/setup-python@v5
108+
with:
109+
python-version: "3.11"
110+
111+
- name: Syntax check
112+
run: |
113+
python -m py_compile services/workflow_service.py
114+
python -m py_compile services/scheduler.py
115+
python -m py_compile store/sqlite_store.py
116+
python -m py_compile store/migrations.py
117+
python -m py_compile core/dag.py
118+
python -m py_compile core/fsm.py
119+
python -m py_compile memory/working_memory.py
120+
python -m py_compile memory/episodic_memory.py
121+
python -m py_compile memory/semantic_memory.py
122+
python -m py_compile models.py
123+
python -m py_compile config.py
124+
125+
- name: Import check
126+
run: |
127+
python -c "
128+
from store.sqlite_store import SQLiteStore
129+
from services.workflow_service import WorkflowService
130+
from services.scheduler import Scheduler
131+
from core.dag import build_dag, get_ready_steps
132+
from core.fsm import transition, can_transition
133+
from memory.working_memory import WorkingMemory
134+
from memory.episodic_memory import EpisodicMemory
135+
from memory.semantic_memory import SemanticMemory
136+
from models import WorkflowState, StepState
137+
print('All imports OK')
138+
"
139+
140+
- name: Run integration tests
141+
run: |
142+
cat > /tmp/test_soloflow.py << 'TESTEOF'
143+
import sys, asyncio, tempfile
144+
sys.path.insert(0, ".")
145+
from pathlib import Path
146+
from store.sqlite_store import SQLiteStore
147+
from services.workflow_service import WorkflowService
148+
from services.scheduler import Scheduler
149+
150+
passed = failed = 0
151+
def check(name, cond, detail=""):
152+
global passed, failed
153+
if cond: passed += 1; print(f" ✅ {name}")
154+
else: failed += 1; print(f" ❌ {name} {detail}")
155+
156+
async def main():
157+
tmpdir = tempfile.mkdtemp()
158+
store = SQLiteStore(Path(tmpdir) / "test.db")
159+
store.initialize()
160+
ws = WorkflowService(store)
161+
ws.set_scheduler(Scheduler(store, ws))
162+
163+
steps = [
164+
{"id": "a", "name": "A", "description": "", "discipline": "quick", "prompt": "Do A"},
165+
{"id": "b", "name": "B", "description": "", "discipline": "quick", "prompt": "Do B"},
166+
{"id": "c", "name": "C", "description": "", "discipline": "quick", "prompt": "Do C"},
167+
]
168+
wf = await ws.create_workflow("test", "Test", steps, [("a","b"),("b","c")])
169+
check("created", wf["state"] == "draft")
170+
check("3 steps", len(wf["steps"]) == 3)
171+
172+
await ws.start_workflow(wf["id"])
173+
ready = await ws.get_ready_steps(wf["id"])
174+
check("a ready", "a" in ready)
175+
176+
await ws.advance_step(wf["id"], "a", result="A done")
177+
await ws.advance_step(wf["id"], "b", result="B done")
178+
await ws.advance_step(wf["id"], "c", result="C done")
179+
status = await ws.get_workflow_status(wf["id"])
180+
check("completed", status["state"] == "completed")
181+
check("progress 3/3", status["progress"]["completed"] == 3)
182+
183+
wf2 = await ws.create_workflow("test2", "Test2",
184+
[{"id": "x", "name": "X", "description": "", "discipline": "quick", "prompt": "X"}], [])
185+
await ws.start_workflow(wf2["id"])
186+
await ws.cancel_workflow(wf2["id"])
187+
status2 = await ws.get_workflow_status(wf2["id"])
188+
check("cancelled", status2["state"] == "cancelled")
189+
190+
all_wfs = await ws.list_workflows()
191+
check("2 workflows", len(all_wfs) == 2)
192+
store.close()
193+
194+
print(f"\nResults: {passed} passed, {failed} failed")
195+
return failed
196+
197+
sys.exit(asyncio.run(main()))
198+
TESTEOF
199+
python /tmp/test_soloflow.py

0 commit comments

Comments
 (0)