cascade-orchestrator 0.1.0__tar.gz
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.
- cascade_orchestrator-0.1.0/LICENSE +21 -0
- cascade_orchestrator-0.1.0/PKG-INFO +290 -0
- cascade_orchestrator-0.1.0/README.md +247 -0
- cascade_orchestrator-0.1.0/cascade/__init__.py +8 -0
- cascade_orchestrator-0.1.0/cascade/config.py +48 -0
- cascade_orchestrator-0.1.0/cascade/database.py +69 -0
- cascade_orchestrator-0.1.0/cascade/engine/__init__.py +1 -0
- cascade_orchestrator-0.1.0/cascade/engine/loop.py +105 -0
- cascade_orchestrator-0.1.0/cascade/engine/pinger.py +39 -0
- cascade_orchestrator-0.1.0/cascade/engine/poller.py +38 -0
- cascade_orchestrator-0.1.0/cascade/engine/progress_tracker.py +63 -0
- cascade_orchestrator-0.1.0/cascade/integrations/__init__.py +33 -0
- cascade_orchestrator-0.1.0/cascade/integrations/hermes_bridge.py +616 -0
- cascade_orchestrator-0.1.0/cascade/integrations/monitor_daemon.py +357 -0
- cascade_orchestrator-0.1.0/cascade/main.py +122 -0
- cascade_orchestrator-0.1.0/cascade/mcp/__init__.py +5 -0
- cascade_orchestrator-0.1.0/cascade/mcp/instructions.py +34 -0
- cascade_orchestrator-0.1.0/cascade/mcp/server.py +74 -0
- cascade_orchestrator-0.1.0/cascade/mcp/tools.py +230 -0
- cascade_orchestrator-0.1.0/cascade/models/__init__.py +26 -0
- cascade_orchestrator-0.1.0/cascade/models/dependency.py +44 -0
- cascade_orchestrator-0.1.0/cascade/models/event.py +58 -0
- cascade_orchestrator-0.1.0/cascade/models/goal.py +46 -0
- cascade_orchestrator-0.1.0/cascade/models/message.py +38 -0
- cascade_orchestrator-0.1.0/cascade/models/milestone.py +43 -0
- cascade_orchestrator-0.1.0/cascade/models/project.py +57 -0
- cascade_orchestrator-0.1.0/cascade/models/task.py +144 -0
- cascade_orchestrator-0.1.0/cascade/models/telemetry.py +34 -0
- cascade_orchestrator-0.1.0/cascade/routers/__init__.py +21 -0
- cascade_orchestrator-0.1.0/cascade/routers/dashboard.py +149 -0
- cascade_orchestrator-0.1.0/cascade/routers/events.py +61 -0
- cascade_orchestrator-0.1.0/cascade/routers/goals.py +70 -0
- cascade_orchestrator-0.1.0/cascade/routers/milestones.py +60 -0
- cascade_orchestrator-0.1.0/cascade/routers/pages.py +154 -0
- cascade_orchestrator-0.1.0/cascade/routers/projects.py +69 -0
- cascade_orchestrator-0.1.0/cascade/routers/tasks.py +204 -0
- cascade_orchestrator-0.1.0/cascade/schemas/__init__.py +51 -0
- cascade_orchestrator-0.1.0/cascade/schemas/event.py +50 -0
- cascade_orchestrator-0.1.0/cascade/schemas/goal.py +57 -0
- cascade_orchestrator-0.1.0/cascade/schemas/message.py +31 -0
- cascade_orchestrator-0.1.0/cascade/schemas/milestone.py +42 -0
- cascade_orchestrator-0.1.0/cascade/schemas/project.py +34 -0
- cascade_orchestrator-0.1.0/cascade/schemas/task.py +92 -0
- cascade_orchestrator-0.1.0/cascade/services/__init__.py +25 -0
- cascade_orchestrator-0.1.0/cascade/services/auto_decision.py +116 -0
- cascade_orchestrator-0.1.0/cascade/services/event_service.py +104 -0
- cascade_orchestrator-0.1.0/cascade/services/goal_service.py +130 -0
- cascade_orchestrator-0.1.0/cascade/services/milestone_service.py +125 -0
- cascade_orchestrator-0.1.0/cascade/services/monitor_service.py +182 -0
- cascade_orchestrator-0.1.0/cascade/services/project_service.py +111 -0
- cascade_orchestrator-0.1.0/cascade/services/scheduler_service.py +124 -0
- cascade_orchestrator-0.1.0/cascade/services/task_service.py +478 -0
- cascade_orchestrator-0.1.0/cascade/utils.py +28 -0
- cascade_orchestrator-0.1.0/cascade/web/static/app.js +50 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/base.html +50 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/board.html +55 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/dashboard.html +98 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/partials/goal_card.html +14 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/partials/progress_bar.html +12 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/partials/project_aggregate.html +41 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/partials/task_card.html +24 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/partials/task_list.html +15 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/project.html +113 -0
- cascade_orchestrator-0.1.0/cascade/web/templates/task.html +157 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/PKG-INFO +290 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/SOURCES.txt +78 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/dependency_links.txt +1 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/entry_points.txt +3 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/requires.txt +19 -0
- cascade_orchestrator-0.1.0/cascade_orchestrator.egg-info/top_level.txt +1 -0
- cascade_orchestrator-0.1.0/pyproject.toml +80 -0
- cascade_orchestrator-0.1.0/setup.cfg +4 -0
- cascade_orchestrator-0.1.0/tests/test_api.py +154 -0
- cascade_orchestrator-0.1.0/tests/test_event_service.py +70 -0
- cascade_orchestrator-0.1.0/tests/test_goal_service.py +103 -0
- cascade_orchestrator-0.1.0/tests/test_mcp_server.py +29 -0
- cascade_orchestrator-0.1.0/tests/test_milestone_service.py +61 -0
- cascade_orchestrator-0.1.0/tests/test_monitor_service.py +111 -0
- cascade_orchestrator-0.1.0/tests/test_project_service.py +51 -0
- cascade_orchestrator-0.1.0/tests/test_task_service.py +182 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cascade contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cascade-orchestrator
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Cascade — agent task orchestration platform combining Leantime's strategic coherence with AgentRQ's agent orchestration
|
|
5
|
+
Author: Cascade contributors
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/nguyenminhduc9988/cascade
|
|
8
|
+
Project-URL: Repository, https://github.com/nguyenminhduc9988/cascade
|
|
9
|
+
Project-URL: Issues, https://github.com/nguyenminhduc9988/cascade/issues
|
|
10
|
+
Keywords: agents,orchestration,task-queue,fastapi,mcp,llm-agents,autonomous-agents,workflow
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Framework :: FastAPI
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Topic :: Software Development :: Libraries :: Application Frameworks
|
|
16
|
+
Classifier: Topic :: Software Development :: Build Tools
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: >=3.11
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: fastapi
|
|
25
|
+
Requires-Dist: uvicorn[standard]
|
|
26
|
+
Requires-Dist: sqlalchemy[asyncio]
|
|
27
|
+
Requires-Dist: aiosqlite
|
|
28
|
+
Requires-Dist: alembic
|
|
29
|
+
Requires-Dist: pydantic
|
|
30
|
+
Requires-Dist: pydantic-settings
|
|
31
|
+
Requires-Dist: python-ulid
|
|
32
|
+
Requires-Dist: jinja2
|
|
33
|
+
Requires-Dist: python-multipart
|
|
34
|
+
Requires-Dist: apscheduler
|
|
35
|
+
Requires-Dist: croniter
|
|
36
|
+
Requires-Dist: sse-starlette
|
|
37
|
+
Requires-Dist: httpx
|
|
38
|
+
Provides-Extra: dev
|
|
39
|
+
Requires-Dist: pytest; extra == "dev"
|
|
40
|
+
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
41
|
+
Requires-Dist: httpx; extra == "dev"
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# 🌀 Cascade
|
|
45
|
+
|
|
46
|
+
**An agent task orchestration platform** that combines Leantime's *strategic
|
|
47
|
+
coherence* (task–goal–milestone links) with AgentRQ's *agent orchestration*
|
|
48
|
+
(dequeue, status state machine, MCP tools, continuous monitoring loop).
|
|
49
|
+
|
|
50
|
+
Cascade is a from-scratch Python reimplementation and significant improvement
|
|
51
|
+
of AgentRQ (Go), built on FastAPI + SQLAlchemy 2.0 with an HTMX/Tailwind UI and
|
|
52
|
+
real-time SSE updates.
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## ✨ Highlights
|
|
57
|
+
|
|
58
|
+
- **Strategic coherence** — every task links explicitly to a goal and milestone;
|
|
59
|
+
goal progress is *computed at read-time* from linked tasks (never denormalised).
|
|
60
|
+
- **Pull-based work queue** — agents dequeue the highest-priority `not_started`
|
|
61
|
+
task whose DAG dependencies are all completed (`idx_tasks_dequeue` composite index).
|
|
62
|
+
- **Status state machine** — `not_started → ongoing → completed|blocked|rejected`,
|
|
63
|
+
validated centrally in `TaskService.update_status`.
|
|
64
|
+
- **Continuous monitoring loop** — a 10-second tick (not hourly) runs the poller,
|
|
65
|
+
pinger and scheduler concurrently; stalls are detected and nudged.
|
|
66
|
+
- **Autonomy-first** — `AutoDecisionService` resolves choices automatically and
|
|
67
|
+
only escalates to a human for genuinely irreversible/destructive operations.
|
|
68
|
+
- **Cross-project choreography** — events + triggers materialise tasks on publish.
|
|
69
|
+
- **MCP agent tools** — `get_task`, `create_task`, `reply`, `update_status`,
|
|
70
|
+
`get_mission`, `get_project_context`, `publish_event`, `get_dependencies`.
|
|
71
|
+
- **Real-time UI** — SSE-powered dashboard with agent liveness dots, goal
|
|
72
|
+
progress bars, drag-and-drop Kanban, and per-task conversation logs.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 🧱 Tech stack
|
|
77
|
+
|
|
78
|
+
| Concern | Choice |
|
|
79
|
+
|----------------|------------------------------------------|
|
|
80
|
+
| Runtime | Python 3.11+, FastAPI, Uvicorn |
|
|
81
|
+
| ORM | SQLAlchemy 2.0 (async, `Mapped[]`) + aiosqlite |
|
|
82
|
+
| Migrations | Alembic |
|
|
83
|
+
| Schemas | Pydantic v2 |
|
|
84
|
+
| IDs | `python-ulid` (time-ordered, sortable) |
|
|
85
|
+
| Real-time | `sse-starlette` + in-memory pub/sub |
|
|
86
|
+
| Scheduling | APScheduler + croniter (cron templates) |
|
|
87
|
+
| UI | HTMX + Tailwind (CDN, no build step) |
|
|
88
|
+
| Templating | Jinja2 |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 🚀 Quick start
|
|
93
|
+
|
|
94
|
+
```bash
|
|
95
|
+
# from /home/minguyen/.hermes
|
|
96
|
+
cd cascade
|
|
97
|
+
|
|
98
|
+
# run with the project venv
|
|
99
|
+
hermes-agent/venv/bin/python -m uvicorn cascade.main:app --reload --port 8100
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Open **http://localhost:8100** for the dashboard. The API docs are at
|
|
103
|
+
`/docs`, and `/api/health` reports service health.
|
|
104
|
+
|
|
105
|
+
On first boot the database is created automatically (`init_db`). For managed
|
|
106
|
+
schema changes, use Alembic:
|
|
107
|
+
|
|
108
|
+
```bash
|
|
109
|
+
hermes-agent/venv/bin/python -m alembic upgrade head
|
|
110
|
+
hermes-agent/venv/bin/python -m alembic revision --autogenerate -m "describe change"
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## 🗂️ Project structure
|
|
116
|
+
|
|
117
|
+
```
|
|
118
|
+
cascade/
|
|
119
|
+
├── pyproject.toml # dependencies + pytest config
|
|
120
|
+
├── alembic.ini # migration config (async)
|
|
121
|
+
├── alembic/ # env.py + versions/
|
|
122
|
+
├── cascade/
|
|
123
|
+
│ ├── main.py # FastAPI app factory + lifespan (monitoring loop)
|
|
124
|
+
│ ├── config.py # Pydantic Settings (CASCADE_ env prefix)
|
|
125
|
+
│ ├── database.py # async engine, session factory, Base
|
|
126
|
+
│ ├── utils.py # ULID + JSON helpers
|
|
127
|
+
│ ├── models/ # SQLAlchemy 2.0 typed models
|
|
128
|
+
│ ├── schemas/ # Pydantic v2 request/response
|
|
129
|
+
│ ├── services/ # business logic (thin controllers → services)
|
|
130
|
+
│ ├── routers/ # FastAPI route handlers (REST + SSE + HTMX pages)
|
|
131
|
+
│ ├── mcp/ # MCP server factory + tools + agent instructions
|
|
132
|
+
│ ├── engine/ # monitoring loop, poller, pinger, progress tracker
|
|
133
|
+
│ └── web/ # Jinja2 templates + static app.js
|
|
134
|
+
└── tests/ # pytest-asyncio (23 tests)
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## 🧠 Data model (the core)
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
Project ─┬─< Goal ────< Task
|
|
143
|
+
├─< Milestone─< Task
|
|
144
|
+
└─< Task >─ TaskDependency (DAG edges)
|
|
145
|
+
> Message (append-only conversation)
|
|
146
|
+
> Telemetry (audit trail)
|
|
147
|
+
Event / EventTrigger ── publish ──> auto-create Task
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
`Task` is the **unified work item** (polymorphic: epic/story/task/subtask) with
|
|
151
|
+
a status state machine, bidirectional human/agent delegation, self-referential
|
|
152
|
+
hierarchy, strategic goal/milestone links, cron-template spawning and event
|
|
153
|
+
choreography — a single model doing what Leantime spreads across many.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## ⚙️ Key behaviours
|
|
158
|
+
|
|
159
|
+
### Dequeue (agent pull queue)
|
|
160
|
+
`GET /api/tasks/dequeue?project_id=…&assignee=agent` returns the highest-priority
|
|
161
|
+
`not_started` task whose every `depends_on` is `completed`. Backed by the
|
|
162
|
+
`idx_tasks_dequeue (project_id, assignee, status)` composite index.
|
|
163
|
+
|
|
164
|
+
### Status state machine
|
|
165
|
+
All transitions go through `TaskService.update_status`, which validates against
|
|
166
|
+
`VALID_TRANSITIONS`, sets `started_at`/`completed_at`, records telemetry, posts
|
|
167
|
+
a system message and broadcasts an SSE `status_change` event.
|
|
168
|
+
|
|
169
|
+
### Goal progress (read-time)
|
|
170
|
+
`GoalService.get_progress` counts linked tasks completed/total when
|
|
171
|
+
`auto_aggregate` is True — progress is **never stored/denormalised**.
|
|
172
|
+
|
|
173
|
+
### Continuous monitoring loop
|
|
174
|
+
`engine/loop.monitoring_loop` runs every **10 seconds**, concurrently executing
|
|
175
|
+
the poller (stall nudging), pinger (dead-session eviction) and scheduler (cron
|
|
176
|
+
template spawning). Stall detection runs on its own slower cadence.
|
|
177
|
+
|
|
178
|
+
### Autonomy
|
|
179
|
+
`AutoDecisionService.should_ask_human` returns `True` only for destructive
|
|
180
|
+
operations (`delete`, `drop`, `production-deploy`, …); everything else is
|
|
181
|
+
auto-resolved via `auto_resolve_choice` (prefers low-risk, low-effort,
|
|
182
|
+
reversible options).
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## 🤖 MCP tools
|
|
187
|
+
|
|
188
|
+
| Tool | Purpose |
|
|
189
|
+
|---------------------|--------------------------------------------------|
|
|
190
|
+
| `get_task` | Dequeue next task (no ID) or fetch a specific one|
|
|
191
|
+
| `create_task` | Decompose / delegate (parent_id + depends_on) |
|
|
192
|
+
| `reply` | Post progress/reply/permission messages |
|
|
193
|
+
| `update_status` | Transition task status |
|
|
194
|
+
| `get_mission` | Big-picture mission + active goals |
|
|
195
|
+
| `get_project_context` | Full project state for coherence |
|
|
196
|
+
| `publish_event` | Emit a cross-project choreography event |
|
|
197
|
+
| `get_dependencies` | Dependency tree status |
|
|
198
|
+
| `auto_decide` | Auto-resolve a choice |
|
|
199
|
+
|
|
200
|
+
See [`cascade/mcp/instructions.py`](cascade/mcp/instructions.py:1) for the
|
|
201
|
+
agent operating contract served as the MCP server instructions.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 🧪 Tests
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cd cascade
|
|
209
|
+
hermes-agent/venv/bin/python -m pytest -q
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
23 tests cover the task state machine + dequeue + DAG resolution, goal progress
|
|
213
|
+
aggregation, agent liveness/stall detection, auto-decision, and the REST API +
|
|
214
|
+
HTMX page rendering (isolated in-memory SQLite per test).
|
|
215
|
+
|
|
216
|
+
---
|
|
217
|
+
|
|
218
|
+
## 🔧 Configuration
|
|
219
|
+
|
|
220
|
+
All settings are overridable via `CASCADE_`-prefixed env vars or a `.env` file
|
|
221
|
+
(see [`cascade/config.py`](cascade/config.py:1)):
|
|
222
|
+
|
|
223
|
+
| Setting | Default |
|
|
224
|
+
|---------------------------------|--------------------------|
|
|
225
|
+
| `CASCADE_DATABASE_URL` | `sqlite+aiosqlite:///./cascade.db` |
|
|
226
|
+
| `CASCADE_PORT` | `8100` |
|
|
227
|
+
| `CASCADE_LOOP_TICK_SECONDS` | `10` |
|
|
228
|
+
| `CASCADE_STALL_THRESHOLD_MINUTES` | `30` |
|
|
229
|
+
| `CASCADE_SESSION_TIMEOUT_SECONDS` | `60` |
|
|
230
|
+
| `CASCADE_ENABLE_MONITORING_LOOP` | `true` |
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## 🛠️ Development
|
|
235
|
+
|
|
236
|
+
### Install dev dependencies
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
cd cascade
|
|
240
|
+
hermes-agent/venv/bin/pip install -e ".[dev]"
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Run the test suite
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
hermes-agent/venv/bin/python -m pytest -q
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Tests use `pytest-asyncio` with an isolated in-memory SQLite database per test,
|
|
250
|
+
so they are fast and side-effect-free. Coverage spans the task state machine and
|
|
251
|
+
dequeue/DAG resolution, goal progress aggregation, agent liveness and stall
|
|
252
|
+
detection, the auto-decision engine, and the REST API + HTMX page rendering.
|
|
253
|
+
|
|
254
|
+
### Architecture notes
|
|
255
|
+
|
|
256
|
+
- **Thin controllers → services.** Routers only parse + serialise; all business
|
|
257
|
+
logic lives in the `services/` package, keeping endpoints trivial to test.
|
|
258
|
+
- **Read-time aggregation.** Goal progress is *computed* on read (never stored),
|
|
259
|
+
so there is no denormalisation drift to repair.
|
|
260
|
+
- **Centralised state transitions.** Every status change funnels through
|
|
261
|
+
`TaskService.update_status`, which enforces `VALID_TRANSITIONS` and records
|
|
262
|
+
telemetry + SSE broadcasts in one place.
|
|
263
|
+
|
|
264
|
+
### Database migrations
|
|
265
|
+
|
|
266
|
+
Schema changes are managed with Alembic. Create a new revision, review the
|
|
267
|
+
autogenerated diff, then upgrade:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
hermes-agent/venv/bin/python -m alembic revision --autogenerate -m "add new table"
|
|
271
|
+
hermes-agent/venv/bin/python -m alembic upgrade head
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Contributing
|
|
275
|
+
|
|
276
|
+
1. Fork the repo and create a feature branch.
|
|
277
|
+
2. Add or update tests for any behaviour change.
|
|
278
|
+
3. Ensure `pytest -q` passes and no lint regressions.
|
|
279
|
+
4. Open a pull request describing the change and its rationale.
|
|
280
|
+
|
|
281
|
+
---
|
|
282
|
+
|
|
283
|
+
## 📄 License
|
|
284
|
+
|
|
285
|
+
Released under the **MIT License** — see [`LICENSE`](LICENSE:1).
|
|
286
|
+
|
|
287
|
+
Cascade reinterprets ideas from [Leantime](https://github.com/Leantime/leantime)
|
|
288
|
+
(strategic task–goal–milestone coherence) and AgentRQ (agent dequeue + status
|
|
289
|
+
state machine + monitoring loop), reimplemented from scratch in Python. It is an
|
|
290
|
+
independent work and is not affiliated with or endorsed by either project.
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# 🌀 Cascade
|
|
2
|
+
|
|
3
|
+
**An agent task orchestration platform** that combines Leantime's *strategic
|
|
4
|
+
coherence* (task–goal–milestone links) with AgentRQ's *agent orchestration*
|
|
5
|
+
(dequeue, status state machine, MCP tools, continuous monitoring loop).
|
|
6
|
+
|
|
7
|
+
Cascade is a from-scratch Python reimplementation and significant improvement
|
|
8
|
+
of AgentRQ (Go), built on FastAPI + SQLAlchemy 2.0 with an HTMX/Tailwind UI and
|
|
9
|
+
real-time SSE updates.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## ✨ Highlights
|
|
14
|
+
|
|
15
|
+
- **Strategic coherence** — every task links explicitly to a goal and milestone;
|
|
16
|
+
goal progress is *computed at read-time* from linked tasks (never denormalised).
|
|
17
|
+
- **Pull-based work queue** — agents dequeue the highest-priority `not_started`
|
|
18
|
+
task whose DAG dependencies are all completed (`idx_tasks_dequeue` composite index).
|
|
19
|
+
- **Status state machine** — `not_started → ongoing → completed|blocked|rejected`,
|
|
20
|
+
validated centrally in `TaskService.update_status`.
|
|
21
|
+
- **Continuous monitoring loop** — a 10-second tick (not hourly) runs the poller,
|
|
22
|
+
pinger and scheduler concurrently; stalls are detected and nudged.
|
|
23
|
+
- **Autonomy-first** — `AutoDecisionService` resolves choices automatically and
|
|
24
|
+
only escalates to a human for genuinely irreversible/destructive operations.
|
|
25
|
+
- **Cross-project choreography** — events + triggers materialise tasks on publish.
|
|
26
|
+
- **MCP agent tools** — `get_task`, `create_task`, `reply`, `update_status`,
|
|
27
|
+
`get_mission`, `get_project_context`, `publish_event`, `get_dependencies`.
|
|
28
|
+
- **Real-time UI** — SSE-powered dashboard with agent liveness dots, goal
|
|
29
|
+
progress bars, drag-and-drop Kanban, and per-task conversation logs.
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 🧱 Tech stack
|
|
34
|
+
|
|
35
|
+
| Concern | Choice |
|
|
36
|
+
|----------------|------------------------------------------|
|
|
37
|
+
| Runtime | Python 3.11+, FastAPI, Uvicorn |
|
|
38
|
+
| ORM | SQLAlchemy 2.0 (async, `Mapped[]`) + aiosqlite |
|
|
39
|
+
| Migrations | Alembic |
|
|
40
|
+
| Schemas | Pydantic v2 |
|
|
41
|
+
| IDs | `python-ulid` (time-ordered, sortable) |
|
|
42
|
+
| Real-time | `sse-starlette` + in-memory pub/sub |
|
|
43
|
+
| Scheduling | APScheduler + croniter (cron templates) |
|
|
44
|
+
| UI | HTMX + Tailwind (CDN, no build step) |
|
|
45
|
+
| Templating | Jinja2 |
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## 🚀 Quick start
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# from /home/minguyen/.hermes
|
|
53
|
+
cd cascade
|
|
54
|
+
|
|
55
|
+
# run with the project venv
|
|
56
|
+
hermes-agent/venv/bin/python -m uvicorn cascade.main:app --reload --port 8100
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Open **http://localhost:8100** for the dashboard. The API docs are at
|
|
60
|
+
`/docs`, and `/api/health` reports service health.
|
|
61
|
+
|
|
62
|
+
On first boot the database is created automatically (`init_db`). For managed
|
|
63
|
+
schema changes, use Alembic:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
hermes-agent/venv/bin/python -m alembic upgrade head
|
|
67
|
+
hermes-agent/venv/bin/python -m alembic revision --autogenerate -m "describe change"
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 🗂️ Project structure
|
|
73
|
+
|
|
74
|
+
```
|
|
75
|
+
cascade/
|
|
76
|
+
├── pyproject.toml # dependencies + pytest config
|
|
77
|
+
├── alembic.ini # migration config (async)
|
|
78
|
+
├── alembic/ # env.py + versions/
|
|
79
|
+
├── cascade/
|
|
80
|
+
│ ├── main.py # FastAPI app factory + lifespan (monitoring loop)
|
|
81
|
+
│ ├── config.py # Pydantic Settings (CASCADE_ env prefix)
|
|
82
|
+
│ ├── database.py # async engine, session factory, Base
|
|
83
|
+
│ ├── utils.py # ULID + JSON helpers
|
|
84
|
+
│ ├── models/ # SQLAlchemy 2.0 typed models
|
|
85
|
+
│ ├── schemas/ # Pydantic v2 request/response
|
|
86
|
+
│ ├── services/ # business logic (thin controllers → services)
|
|
87
|
+
│ ├── routers/ # FastAPI route handlers (REST + SSE + HTMX pages)
|
|
88
|
+
│ ├── mcp/ # MCP server factory + tools + agent instructions
|
|
89
|
+
│ ├── engine/ # monitoring loop, poller, pinger, progress tracker
|
|
90
|
+
│ └── web/ # Jinja2 templates + static app.js
|
|
91
|
+
└── tests/ # pytest-asyncio (23 tests)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 🧠 Data model (the core)
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
Project ─┬─< Goal ────< Task
|
|
100
|
+
├─< Milestone─< Task
|
|
101
|
+
└─< Task >─ TaskDependency (DAG edges)
|
|
102
|
+
> Message (append-only conversation)
|
|
103
|
+
> Telemetry (audit trail)
|
|
104
|
+
Event / EventTrigger ── publish ──> auto-create Task
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`Task` is the **unified work item** (polymorphic: epic/story/task/subtask) with
|
|
108
|
+
a status state machine, bidirectional human/agent delegation, self-referential
|
|
109
|
+
hierarchy, strategic goal/milestone links, cron-template spawning and event
|
|
110
|
+
choreography — a single model doing what Leantime spreads across many.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## ⚙️ Key behaviours
|
|
115
|
+
|
|
116
|
+
### Dequeue (agent pull queue)
|
|
117
|
+
`GET /api/tasks/dequeue?project_id=…&assignee=agent` returns the highest-priority
|
|
118
|
+
`not_started` task whose every `depends_on` is `completed`. Backed by the
|
|
119
|
+
`idx_tasks_dequeue (project_id, assignee, status)` composite index.
|
|
120
|
+
|
|
121
|
+
### Status state machine
|
|
122
|
+
All transitions go through `TaskService.update_status`, which validates against
|
|
123
|
+
`VALID_TRANSITIONS`, sets `started_at`/`completed_at`, records telemetry, posts
|
|
124
|
+
a system message and broadcasts an SSE `status_change` event.
|
|
125
|
+
|
|
126
|
+
### Goal progress (read-time)
|
|
127
|
+
`GoalService.get_progress` counts linked tasks completed/total when
|
|
128
|
+
`auto_aggregate` is True — progress is **never stored/denormalised**.
|
|
129
|
+
|
|
130
|
+
### Continuous monitoring loop
|
|
131
|
+
`engine/loop.monitoring_loop` runs every **10 seconds**, concurrently executing
|
|
132
|
+
the poller (stall nudging), pinger (dead-session eviction) and scheduler (cron
|
|
133
|
+
template spawning). Stall detection runs on its own slower cadence.
|
|
134
|
+
|
|
135
|
+
### Autonomy
|
|
136
|
+
`AutoDecisionService.should_ask_human` returns `True` only for destructive
|
|
137
|
+
operations (`delete`, `drop`, `production-deploy`, …); everything else is
|
|
138
|
+
auto-resolved via `auto_resolve_choice` (prefers low-risk, low-effort,
|
|
139
|
+
reversible options).
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## 🤖 MCP tools
|
|
144
|
+
|
|
145
|
+
| Tool | Purpose |
|
|
146
|
+
|---------------------|--------------------------------------------------|
|
|
147
|
+
| `get_task` | Dequeue next task (no ID) or fetch a specific one|
|
|
148
|
+
| `create_task` | Decompose / delegate (parent_id + depends_on) |
|
|
149
|
+
| `reply` | Post progress/reply/permission messages |
|
|
150
|
+
| `update_status` | Transition task status |
|
|
151
|
+
| `get_mission` | Big-picture mission + active goals |
|
|
152
|
+
| `get_project_context` | Full project state for coherence |
|
|
153
|
+
| `publish_event` | Emit a cross-project choreography event |
|
|
154
|
+
| `get_dependencies` | Dependency tree status |
|
|
155
|
+
| `auto_decide` | Auto-resolve a choice |
|
|
156
|
+
|
|
157
|
+
See [`cascade/mcp/instructions.py`](cascade/mcp/instructions.py:1) for the
|
|
158
|
+
agent operating contract served as the MCP server instructions.
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## 🧪 Tests
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
cd cascade
|
|
166
|
+
hermes-agent/venv/bin/python -m pytest -q
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
23 tests cover the task state machine + dequeue + DAG resolution, goal progress
|
|
170
|
+
aggregation, agent liveness/stall detection, auto-decision, and the REST API +
|
|
171
|
+
HTMX page rendering (isolated in-memory SQLite per test).
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## 🔧 Configuration
|
|
176
|
+
|
|
177
|
+
All settings are overridable via `CASCADE_`-prefixed env vars or a `.env` file
|
|
178
|
+
(see [`cascade/config.py`](cascade/config.py:1)):
|
|
179
|
+
|
|
180
|
+
| Setting | Default |
|
|
181
|
+
|---------------------------------|--------------------------|
|
|
182
|
+
| `CASCADE_DATABASE_URL` | `sqlite+aiosqlite:///./cascade.db` |
|
|
183
|
+
| `CASCADE_PORT` | `8100` |
|
|
184
|
+
| `CASCADE_LOOP_TICK_SECONDS` | `10` |
|
|
185
|
+
| `CASCADE_STALL_THRESHOLD_MINUTES` | `30` |
|
|
186
|
+
| `CASCADE_SESSION_TIMEOUT_SECONDS` | `60` |
|
|
187
|
+
| `CASCADE_ENABLE_MONITORING_LOOP` | `true` |
|
|
188
|
+
|
|
189
|
+
---
|
|
190
|
+
|
|
191
|
+
## 🛠️ Development
|
|
192
|
+
|
|
193
|
+
### Install dev dependencies
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
cd cascade
|
|
197
|
+
hermes-agent/venv/bin/pip install -e ".[dev]"
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### Run the test suite
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
hermes-agent/venv/bin/python -m pytest -q
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
Tests use `pytest-asyncio` with an isolated in-memory SQLite database per test,
|
|
207
|
+
so they are fast and side-effect-free. Coverage spans the task state machine and
|
|
208
|
+
dequeue/DAG resolution, goal progress aggregation, agent liveness and stall
|
|
209
|
+
detection, the auto-decision engine, and the REST API + HTMX page rendering.
|
|
210
|
+
|
|
211
|
+
### Architecture notes
|
|
212
|
+
|
|
213
|
+
- **Thin controllers → services.** Routers only parse + serialise; all business
|
|
214
|
+
logic lives in the `services/` package, keeping endpoints trivial to test.
|
|
215
|
+
- **Read-time aggregation.** Goal progress is *computed* on read (never stored),
|
|
216
|
+
so there is no denormalisation drift to repair.
|
|
217
|
+
- **Centralised state transitions.** Every status change funnels through
|
|
218
|
+
`TaskService.update_status`, which enforces `VALID_TRANSITIONS` and records
|
|
219
|
+
telemetry + SSE broadcasts in one place.
|
|
220
|
+
|
|
221
|
+
### Database migrations
|
|
222
|
+
|
|
223
|
+
Schema changes are managed with Alembic. Create a new revision, review the
|
|
224
|
+
autogenerated diff, then upgrade:
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
hermes-agent/venv/bin/python -m alembic revision --autogenerate -m "add new table"
|
|
228
|
+
hermes-agent/venv/bin/python -m alembic upgrade head
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Contributing
|
|
232
|
+
|
|
233
|
+
1. Fork the repo and create a feature branch.
|
|
234
|
+
2. Add or update tests for any behaviour change.
|
|
235
|
+
3. Ensure `pytest -q` passes and no lint regressions.
|
|
236
|
+
4. Open a pull request describing the change and its rationale.
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## 📄 License
|
|
241
|
+
|
|
242
|
+
Released under the **MIT License** — see [`LICENSE`](LICENSE:1).
|
|
243
|
+
|
|
244
|
+
Cascade reinterprets ideas from [Leantime](https://github.com/Leantime/leantime)
|
|
245
|
+
(strategic task–goal–milestone coherence) and AgentRQ (agent dequeue + status
|
|
246
|
+
state machine + monitoring loop), reimplemented from scratch in Python. It is an
|
|
247
|
+
independent work and is not affiliated with or endorsed by either project.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""Application configuration via Pydantic Settings.
|
|
2
|
+
|
|
3
|
+
All settings can be overridden with environment variables prefixed ``CASCADE_``
|
|
4
|
+
(e.g. ``CASCADE_PORT=8100``) or via a ``.env`` file in the working directory.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Settings(BaseSettings):
|
|
13
|
+
"""Central configuration for the Cascade platform."""
|
|
14
|
+
|
|
15
|
+
model_config = SettingsConfigDict(
|
|
16
|
+
env_prefix="CASCADE_",
|
|
17
|
+
env_file=".env",
|
|
18
|
+
env_file_encoding="utf-8",
|
|
19
|
+
extra="ignore",
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
# --- Database ---
|
|
23
|
+
database_url: str = "sqlite+aiosqlite:///./cascade.db"
|
|
24
|
+
|
|
25
|
+
# --- Server ---
|
|
26
|
+
host: str = "0.0.0.0"
|
|
27
|
+
port: int = 8100
|
|
28
|
+
reload: bool = True
|
|
29
|
+
|
|
30
|
+
# --- Engine ---
|
|
31
|
+
# Seconds between monitoring-loop ticks. Continuous monitoring, not hourly.
|
|
32
|
+
loop_tick_seconds: int = 10
|
|
33
|
+
# Seconds between stall-detection sweeps.
|
|
34
|
+
stall_check_interval_seconds: int = 300
|
|
35
|
+
# Minutes of silence before an ongoing task is considered stalled.
|
|
36
|
+
stall_threshold_minutes: int = 30
|
|
37
|
+
# Seconds before an agent session with no heartbeat is considered dead.
|
|
38
|
+
session_timeout_seconds: int = 60
|
|
39
|
+
|
|
40
|
+
# --- Scheduling ---
|
|
41
|
+
scheduler_interval_seconds: int = 60
|
|
42
|
+
|
|
43
|
+
# --- Feature flags ---
|
|
44
|
+
enable_monitoring_loop: bool = True
|
|
45
|
+
enable_scheduler: bool = True
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
settings = Settings()
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Async database engine, session factory and declarative Base.
|
|
2
|
+
|
|
3
|
+
Provides the single :data:`engine`, the :data:`async_session_factory` and the
|
|
4
|
+
dependency :func:`get_db` used by FastAPI route handlers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from collections.abc import AsyncGenerator
|
|
10
|
+
|
|
11
|
+
from sqlalchemy import event
|
|
12
|
+
from sqlalchemy.ext.asyncio import (
|
|
13
|
+
AsyncSession,
|
|
14
|
+
async_sessionmaker,
|
|
15
|
+
create_async_engine,
|
|
16
|
+
)
|
|
17
|
+
from sqlalchemy.orm import DeclarativeBase
|
|
18
|
+
|
|
19
|
+
from cascade.config import settings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class Base(DeclarativeBase):
|
|
23
|
+
"""Declarative base for all SQLAlchemy 2.0 typed models."""
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
engine = create_async_engine(
|
|
27
|
+
settings.database_url,
|
|
28
|
+
echo=False,
|
|
29
|
+
future=True,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
if engine.url.get_backend_name() == "sqlite":
|
|
33
|
+
# The monitoring loop and API requests hit SQLite from several concurrent
|
|
34
|
+
# connections. Without WAL + a busy timeout, SQLite's default rollback
|
|
35
|
+
# journal serialises writers and raises "database is locked" almost
|
|
36
|
+
# immediately under any real concurrency.
|
|
37
|
+
@event.listens_for(engine.sync_engine, "connect")
|
|
38
|
+
def _set_sqlite_pragmas(dbapi_connection, connection_record) -> None: # noqa: ANN001
|
|
39
|
+
cursor = dbapi_connection.cursor()
|
|
40
|
+
cursor.execute("PRAGMA journal_mode=WAL")
|
|
41
|
+
cursor.execute("PRAGMA busy_timeout=30000")
|
|
42
|
+
cursor.execute("PRAGMA foreign_keys=ON")
|
|
43
|
+
cursor.close()
|
|
44
|
+
|
|
45
|
+
async_session_factory: async_sessionmaker[AsyncSession] = async_sessionmaker(
|
|
46
|
+
bind=engine,
|
|
47
|
+
class_=AsyncSession,
|
|
48
|
+
expire_on_commit=False,
|
|
49
|
+
autoflush=False,
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
async def get_db() -> AsyncGenerator[AsyncSession, None]:
|
|
54
|
+
"""FastAPI dependency that yields an :class:`AsyncSession`."""
|
|
55
|
+
async with async_session_factory() as session:
|
|
56
|
+
try:
|
|
57
|
+
yield session
|
|
58
|
+
except Exception:
|
|
59
|
+
await session.rollback()
|
|
60
|
+
raise
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def init_db() -> None:
|
|
64
|
+
"""Create all tables. Intended for dev/bootstrap; Alembic is used in prod."""
|
|
65
|
+
# Importing here ensures every model is registered on ``Base.metadata``.
|
|
66
|
+
from cascade import models # noqa: F401 (register models)
|
|
67
|
+
|
|
68
|
+
async with engine.begin() as conn:
|
|
69
|
+
await conn.run_sync(Base.metadata.create_all)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Engine: continuous monitoring loop, polling, pinger and progress tracking."""
|