cuddlytoddly 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.
- cuddlytoddly-0.1.0/LICENSE +21 -0
- cuddlytoddly-0.1.0/PKG-INFO +165 -0
- cuddlytoddly-0.1.0/README.md +128 -0
- cuddlytoddly-0.1.0/cuddlytoddly/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/__main__.py +276 -0
- cuddlytoddly-0.1.0/cuddlytoddly/core/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/core/events.py +47 -0
- cuddlytoddly-0.1.0/cuddlytoddly/core/id_generator.py +78 -0
- cuddlytoddly-0.1.0/cuddlytoddly/core/reducer.py +124 -0
- cuddlytoddly-0.1.0/cuddlytoddly/core/task_graph.py +273 -0
- cuddlytoddly-0.1.0/cuddlytoddly/engine/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/engine/execution_step_reporter.py +243 -0
- cuddlytoddly-0.1.0/cuddlytoddly/engine/llm_orchestrator.py +882 -0
- cuddlytoddly-0.1.0/cuddlytoddly/engine/quality_gate.py +274 -0
- cuddlytoddly-0.1.0/cuddlytoddly/infra/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/infra/event_log.py +55 -0
- cuddlytoddly-0.1.0/cuddlytoddly/infra/event_queue.py +19 -0
- cuddlytoddly-0.1.0/cuddlytoddly/infra/logging.py +122 -0
- cuddlytoddly-0.1.0/cuddlytoddly/infra/replay.py +19 -0
- cuddlytoddly-0.1.0/cuddlytoddly/planning/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_executor.py +420 -0
- cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_interface.py +1290 -0
- cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_output_validator.py +237 -0
- cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_planner.py +344 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/__init__.py +0 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/code_execution/__init__.py +0 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/code_execution/tools.py +65 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/file_ops/__init__.py +0 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/file_ops/tools.py +38 -0
- cuddlytoddly-0.1.0/cuddlytoddly/skills/skill_loader.py +210 -0
- cuddlytoddly-0.1.0/cuddlytoddly/tools/__init__.py +0 -0
- cuddlytoddly-0.1.0/cuddlytoddly/tools/mcp_adapter.py +200 -0
- cuddlytoddly-0.1.0/cuddlytoddly/tools/registry.py +0 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/__init__.py +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/curses_startup.py +430 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/curses_ui.py +1504 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/git_projection.py +321 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/startup.py +561 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/web_server.py +588 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/web_ui.html +1668 -0
- cuddlytoddly-0.1.0/cuddlytoddly/ui/web_ui_startup.html +544 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/PKG-INFO +165 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/SOURCES.txt +59 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/dependency_links.txt +1 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/entry_points.txt +2 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/requires.txt +23 -0
- cuddlytoddly-0.1.0/cuddlytoddly.egg-info/top_level.txt +1 -0
- cuddlytoddly-0.1.0/pyproject.toml +79 -0
- cuddlytoddly-0.1.0/setup.cfg +4 -0
- cuddlytoddly-0.1.0/tests/test_events_reducer.py +248 -0
- cuddlytoddly-0.1.0/tests/test_id_generator.py +97 -0
- cuddlytoddly-0.1.0/tests/test_infra.py +221 -0
- cuddlytoddly-0.1.0/tests/test_integration.py +286 -0
- cuddlytoddly-0.1.0/tests/test_llm_executor.py +303 -0
- cuddlytoddly-0.1.0/tests/test_llm_interface.py +255 -0
- cuddlytoddly-0.1.0/tests/test_llm_validator.py +229 -0
- cuddlytoddly-0.1.0/tests/test_orchestrator.py +323 -0
- cuddlytoddly-0.1.0/tests/test_quality_gate.py +284 -0
- cuddlytoddly-0.1.0/tests/test_skill_loader.py +195 -0
- cuddlytoddly-0.1.0/tests/test_startup.py +277 -0
- cuddlytoddly-0.1.0/tests/test_task_graph.py +311 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 3IVIS
|
|
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,165 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: cuddlytoddly
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: LLM-driven autonomous DAG planning and execution system
|
|
5
|
+
Author: 3IVIS GmbH
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/3IVIS/cuddlytoddly
|
|
8
|
+
Project-URL: Documentation, https://github.com/3IVIS/cuddlytoddly/tree/main/docs
|
|
9
|
+
Project-URL: Issues, https://github.com/3IVIS/cuddlytoddly/issues
|
|
10
|
+
Keywords: llm,dag,planning,autonomous,agent
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: gitpython>=3.1
|
|
22
|
+
Requires-Dist: windows-curses>=2.3; sys_platform == "win32"
|
|
23
|
+
Provides-Extra: openai
|
|
24
|
+
Requires-Dist: openai>=1.0; extra == "openai"
|
|
25
|
+
Provides-Extra: claude
|
|
26
|
+
Requires-Dist: anthropic>=0.25; extra == "claude"
|
|
27
|
+
Provides-Extra: local
|
|
28
|
+
Requires-Dist: llama-cpp-python>=0.2; extra == "local"
|
|
29
|
+
Requires-Dist: outlines>=0.0.46; extra == "local"
|
|
30
|
+
Provides-Extra: all
|
|
31
|
+
Requires-Dist: cuddlytoddly[claude,local,openai]; extra == "all"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-timeout; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff; extra == "dev"
|
|
36
|
+
Requires-Dist: mypy; extra == "dev"
|
|
37
|
+
|
|
38
|
+
# cuddlytoddly
|
|
39
|
+
|
|
40
|
+
An LLM-driven autonomous planning and execution system built around a DAG (directed acyclic graph) of tasks. Give it a goal; it breaks the goal into tasks, executes them with tools, verifies results, and fills in gaps — continuously, with live terminal and web UIs.
|
|
41
|
+
|
|
42
|
+
Why "cuddlytoddly"?
|
|
43
|
+
|
|
44
|
+
Large language models are powerful at generating text and solving well-scoped problems, but they are widely recognized as weak at long-horizon, complex planning. Left alone, they often miss dependencies, overlook edge cases, or wander off track.
|
|
45
|
+
|
|
46
|
+
cuddlytoddly takes a different approach: instead of expecting the LLM to plan everything autonomously, we provide a structured framework where humans and the system collaborate. The LLM handles decomposition, execution, and verification, while the framework ensures tasks are organized, dependencies are respected, and gaps are bridged.
|
|
47
|
+
|
|
48
|
+
Think of it as holding the model’s hand while it learns to walk through complex goals — hence the name cuddlytoddly. It’s not about blind autonomy; it’s about guided, reliable progress.
|
|
49
|
+
|
|
50
|
+
## How it works
|
|
51
|
+
|
|
52
|
+
1. A plain-English **goal** is seeded into the graph.
|
|
53
|
+
2. The **LLMPlanner** decomposes it into a DAG of tasks with explicit dependencies.
|
|
54
|
+
3. The **SimpleOrchestrator** picks up ready nodes and hands them to the **LLMExecutor**.
|
|
55
|
+
4. The executor runs a multi-turn LLM loop, calling tools (code execution, file I/O, custom skills) until the task is done.
|
|
56
|
+
5. The **QualityGate** checks the result against declared outputs; if something is missing it injects a bridging task automatically.
|
|
57
|
+
6. Every mutation is written to an **event log** — crash and resume with no lost work.
|
|
58
|
+
|
|
59
|
+
```
|
|
60
|
+
goal → LLMPlanner → TaskGraph (DAG)
|
|
61
|
+
│
|
|
62
|
+
SimpleOrchestrator
|
|
63
|
+
├── LLMExecutor + tools
|
|
64
|
+
└── QualityGate (verify / bridge)
|
|
65
|
+
│
|
|
66
|
+
EventLog (JSONL) → replay on restart
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install cuddlytoddly
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
**Requirements:** Python 3.11+, `git` on your PATH (for the DAG visualiser).
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
81
|
+
cuddlytoddly "Write a market analysis for electric scooters"
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Or pass no argument to use the startup screen with multiple options.
|
|
85
|
+
The UI opens automatically. The run data is stored locally and can be resumed later — the event log preserves all state.
|
|
86
|
+
|
|
87
|
+
## LLM backends
|
|
88
|
+
|
|
89
|
+
| Backend | Install | `create_llm_client` call |
|
|
90
|
+
|---|---|---|
|
|
91
|
+
| Anthropic Claude | included | `create_llm_client("claude", model="claude-3-5-sonnet-20241022")` |
|
|
92
|
+
| OpenAI / compatible | `[openai]` | `create_llm_client("openai", model="gpt-4o")` |
|
|
93
|
+
| Local llama.cpp | `[local]` | `create_llm_client("llamacpp", model_path="/path/to/model.gguf")` |
|
|
94
|
+
|
|
95
|
+
## Adding skills
|
|
96
|
+
|
|
97
|
+
Drop a folder with a `SKILL.md` (and optional `tools.py`) into `cuddlytoddly/skills/`. The `SkillLoader` discovers it automatically. See [docs/skills.md](docs/skills.md) for the full format.
|
|
98
|
+
|
|
99
|
+
## Documentation
|
|
100
|
+
|
|
101
|
+
- [Architecture](docs/architecture.md) — how the components fit together
|
|
102
|
+
- [Configuration](docs/configuration.md) — LLM backends, run directory, environment variables
|
|
103
|
+
- [Skills](docs/skills.md) — built-in skills and how to add custom ones
|
|
104
|
+
- [API Reference](docs/api.md) — public Python API
|
|
105
|
+
|
|
106
|
+
## Project structure
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
cuddlytoddly/
|
|
110
|
+
├── core/ # TaskGraph, events, reducer, ID generator
|
|
111
|
+
├── engine/ # SimpleOrchestrator, QualityGate, ExecutionStepReporter
|
|
112
|
+
├── infra/ # Logging, EventQueue, EventLog, replay
|
|
113
|
+
├── planning/ # LLM interface, LLMPlanner, LLMExecutor, output validator
|
|
114
|
+
├── skills/ # SkillLoader + built-in skill packs
|
|
115
|
+
│ ├── code_execution/
|
|
116
|
+
│ └── file_ops/
|
|
117
|
+
└── ui/ # Curses terminal UI, Git DAG projection
|
|
118
|
+
docs/
|
|
119
|
+
pyproject.toml
|
|
120
|
+
LICENSE
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
## Python API
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from cuddlytoddly.core.task_graph import TaskGraph
|
|
127
|
+
from cuddlytoddly.core.events import Event, ADD_NODE
|
|
128
|
+
from cuddlytoddly.core.reducer import apply_event
|
|
129
|
+
from cuddlytoddly.infra.event_queue import EventQueue
|
|
130
|
+
from cuddlytoddly.infra.event_log import EventLog
|
|
131
|
+
from cuddlytoddly.planning.llm_interface import create_llm_client
|
|
132
|
+
from cuddlytoddly.planning.llm_planner import LLMPlanner
|
|
133
|
+
from cuddlytoddly.planning.llm_executor import LLMExecutor
|
|
134
|
+
from cuddlytoddly.engine.quality_gate import QualityGate
|
|
135
|
+
from cuddlytoddly.engine.llm_orchestrator import SimpleOrchestrator
|
|
136
|
+
from cuddlytoddly.skills.skill_loader import SkillLoader
|
|
137
|
+
|
|
138
|
+
# LLM client — swap "claude" for "openai" or "llamacpp"
|
|
139
|
+
llm = create_llm_client("claude", model="claude-3-5-sonnet-20241022")
|
|
140
|
+
|
|
141
|
+
graph = TaskGraph()
|
|
142
|
+
skills = SkillLoader()
|
|
143
|
+
planner = LLMPlanner(llm_client=llm, graph=graph, skills_summary=skills.prompt_summary)
|
|
144
|
+
executor = LLMExecutor(llm_client=llm, tool_registry=skills.registry)
|
|
145
|
+
gate = QualityGate(llm_client=llm, tool_registry=skills.registry)
|
|
146
|
+
|
|
147
|
+
orchestrator = SimpleOrchestrator(
|
|
148
|
+
graph=graph, planner=planner, executor=executor,
|
|
149
|
+
quality_gate=gate, event_queue=EventQueue(),
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Seed a goal
|
|
153
|
+
apply_event(graph, Event(ADD_NODE, {
|
|
154
|
+
"node_id": "my_goal",
|
|
155
|
+
"node_type": "goal",
|
|
156
|
+
"metadata": {"description": "Summarise the key risks of AGI", "expanded": False},
|
|
157
|
+
}))
|
|
158
|
+
|
|
159
|
+
orchestrator.start()
|
|
160
|
+
# orchestrator runs in the background — block however suits your use case
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# cuddlytoddly
|
|
2
|
+
|
|
3
|
+
An LLM-driven autonomous planning and execution system built around a DAG (directed acyclic graph) of tasks. Give it a goal; it breaks the goal into tasks, executes them with tools, verifies results, and fills in gaps — continuously, with live terminal and web UIs.
|
|
4
|
+
|
|
5
|
+
Why "cuddlytoddly"?
|
|
6
|
+
|
|
7
|
+
Large language models are powerful at generating text and solving well-scoped problems, but they are widely recognized as weak at long-horizon, complex planning. Left alone, they often miss dependencies, overlook edge cases, or wander off track.
|
|
8
|
+
|
|
9
|
+
cuddlytoddly takes a different approach: instead of expecting the LLM to plan everything autonomously, we provide a structured framework where humans and the system collaborate. The LLM handles decomposition, execution, and verification, while the framework ensures tasks are organized, dependencies are respected, and gaps are bridged.
|
|
10
|
+
|
|
11
|
+
Think of it as holding the model’s hand while it learns to walk through complex goals — hence the name cuddlytoddly. It’s not about blind autonomy; it’s about guided, reliable progress.
|
|
12
|
+
|
|
13
|
+
## How it works
|
|
14
|
+
|
|
15
|
+
1. A plain-English **goal** is seeded into the graph.
|
|
16
|
+
2. The **LLMPlanner** decomposes it into a DAG of tasks with explicit dependencies.
|
|
17
|
+
3. The **SimpleOrchestrator** picks up ready nodes and hands them to the **LLMExecutor**.
|
|
18
|
+
4. The executor runs a multi-turn LLM loop, calling tools (code execution, file I/O, custom skills) until the task is done.
|
|
19
|
+
5. The **QualityGate** checks the result against declared outputs; if something is missing it injects a bridging task automatically.
|
|
20
|
+
6. Every mutation is written to an **event log** — crash and resume with no lost work.
|
|
21
|
+
|
|
22
|
+
```
|
|
23
|
+
goal → LLMPlanner → TaskGraph (DAG)
|
|
24
|
+
│
|
|
25
|
+
SimpleOrchestrator
|
|
26
|
+
├── LLMExecutor + tools
|
|
27
|
+
└── QualityGate (verify / bridge)
|
|
28
|
+
│
|
|
29
|
+
EventLog (JSONL) → replay on restart
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Installation
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install cuddlytoddly
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
**Requirements:** Python 3.11+, `git` on your PATH (for the DAG visualiser).
|
|
39
|
+
|
|
40
|
+
## Quick start
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
export ANTHROPIC_API_KEY=sk-ant-...
|
|
44
|
+
cuddlytoddly "Write a market analysis for electric scooters"
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Or pass no argument to use the startup screen with multiple options.
|
|
48
|
+
The UI opens automatically. The run data is stored locally and can be resumed later — the event log preserves all state.
|
|
49
|
+
|
|
50
|
+
## LLM backends
|
|
51
|
+
|
|
52
|
+
| Backend | Install | `create_llm_client` call |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| Anthropic Claude | included | `create_llm_client("claude", model="claude-3-5-sonnet-20241022")` |
|
|
55
|
+
| OpenAI / compatible | `[openai]` | `create_llm_client("openai", model="gpt-4o")` |
|
|
56
|
+
| Local llama.cpp | `[local]` | `create_llm_client("llamacpp", model_path="/path/to/model.gguf")` |
|
|
57
|
+
|
|
58
|
+
## Adding skills
|
|
59
|
+
|
|
60
|
+
Drop a folder with a `SKILL.md` (and optional `tools.py`) into `cuddlytoddly/skills/`. The `SkillLoader` discovers it automatically. See [docs/skills.md](docs/skills.md) for the full format.
|
|
61
|
+
|
|
62
|
+
## Documentation
|
|
63
|
+
|
|
64
|
+
- [Architecture](docs/architecture.md) — how the components fit together
|
|
65
|
+
- [Configuration](docs/configuration.md) — LLM backends, run directory, environment variables
|
|
66
|
+
- [Skills](docs/skills.md) — built-in skills and how to add custom ones
|
|
67
|
+
- [API Reference](docs/api.md) — public Python API
|
|
68
|
+
|
|
69
|
+
## Project structure
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
cuddlytoddly/
|
|
73
|
+
├── core/ # TaskGraph, events, reducer, ID generator
|
|
74
|
+
├── engine/ # SimpleOrchestrator, QualityGate, ExecutionStepReporter
|
|
75
|
+
├── infra/ # Logging, EventQueue, EventLog, replay
|
|
76
|
+
├── planning/ # LLM interface, LLMPlanner, LLMExecutor, output validator
|
|
77
|
+
├── skills/ # SkillLoader + built-in skill packs
|
|
78
|
+
│ ├── code_execution/
|
|
79
|
+
│ └── file_ops/
|
|
80
|
+
└── ui/ # Curses terminal UI, Git DAG projection
|
|
81
|
+
docs/
|
|
82
|
+
pyproject.toml
|
|
83
|
+
LICENSE
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Python API
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from cuddlytoddly.core.task_graph import TaskGraph
|
|
90
|
+
from cuddlytoddly.core.events import Event, ADD_NODE
|
|
91
|
+
from cuddlytoddly.core.reducer import apply_event
|
|
92
|
+
from cuddlytoddly.infra.event_queue import EventQueue
|
|
93
|
+
from cuddlytoddly.infra.event_log import EventLog
|
|
94
|
+
from cuddlytoddly.planning.llm_interface import create_llm_client
|
|
95
|
+
from cuddlytoddly.planning.llm_planner import LLMPlanner
|
|
96
|
+
from cuddlytoddly.planning.llm_executor import LLMExecutor
|
|
97
|
+
from cuddlytoddly.engine.quality_gate import QualityGate
|
|
98
|
+
from cuddlytoddly.engine.llm_orchestrator import SimpleOrchestrator
|
|
99
|
+
from cuddlytoddly.skills.skill_loader import SkillLoader
|
|
100
|
+
|
|
101
|
+
# LLM client — swap "claude" for "openai" or "llamacpp"
|
|
102
|
+
llm = create_llm_client("claude", model="claude-3-5-sonnet-20241022")
|
|
103
|
+
|
|
104
|
+
graph = TaskGraph()
|
|
105
|
+
skills = SkillLoader()
|
|
106
|
+
planner = LLMPlanner(llm_client=llm, graph=graph, skills_summary=skills.prompt_summary)
|
|
107
|
+
executor = LLMExecutor(llm_client=llm, tool_registry=skills.registry)
|
|
108
|
+
gate = QualityGate(llm_client=llm, tool_registry=skills.registry)
|
|
109
|
+
|
|
110
|
+
orchestrator = SimpleOrchestrator(
|
|
111
|
+
graph=graph, planner=planner, executor=executor,
|
|
112
|
+
quality_gate=gate, event_queue=EventQueue(),
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Seed a goal
|
|
116
|
+
apply_event(graph, Event(ADD_NODE, {
|
|
117
|
+
"node_id": "my_goal",
|
|
118
|
+
"node_type": "goal",
|
|
119
|
+
"metadata": {"description": "Summarise the key risks of AGI", "expanded": False},
|
|
120
|
+
}))
|
|
121
|
+
|
|
122
|
+
orchestrator.start()
|
|
123
|
+
# orchestrator runs in the background — block however suits your use case
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## License
|
|
127
|
+
|
|
128
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
# __main__.py — updated startup section
|
|
2
|
+
# Replace the existing main() function with this version.
|
|
3
|
+
# Everything from the LLM client setup downward is unchanged.
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import os
|
|
7
|
+
import argparse
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from cuddlytoddly.core.task_graph import TaskGraph
|
|
13
|
+
from cuddlytoddly.core.events import Event, ADD_NODE
|
|
14
|
+
from cuddlytoddly.core.reducer import apply_event
|
|
15
|
+
from cuddlytoddly.infra.event_queue import EventQueue
|
|
16
|
+
from cuddlytoddly.infra.event_log import EventLog
|
|
17
|
+
from cuddlytoddly.infra.replay import rebuild_graph_from_log
|
|
18
|
+
from cuddlytoddly.infra.logging import setup_logging, get_logger
|
|
19
|
+
from cuddlytoddly.planning.llm_interface import create_llm_client
|
|
20
|
+
from cuddlytoddly.planning.llm_planner import LLMPlanner
|
|
21
|
+
from cuddlytoddly.planning.llm_executor import LLMExecutor
|
|
22
|
+
from cuddlytoddly.engine.quality_gate import QualityGate
|
|
23
|
+
from cuddlytoddly.engine.llm_orchestrator import SimpleOrchestrator
|
|
24
|
+
from cuddlytoddly.skills.skill_loader import SkillLoader
|
|
25
|
+
from cuddlytoddly.ui.curses_ui import run_ui
|
|
26
|
+
from cuddlytoddly.ui.startup import StartupChoice
|
|
27
|
+
from cuddlytoddly.ui.startup import run_startup_curses
|
|
28
|
+
from cuddlytoddly.ui.web_server import run_web_ui
|
|
29
|
+
from cuddlytoddly.core.id_generator import StableIDGenerator
|
|
30
|
+
import cuddlytoddly.planning.llm_interface as llm_iface
|
|
31
|
+
|
|
32
|
+
REPO_ROOT = Path(__file__).resolve().parent
|
|
33
|
+
|
|
34
|
+
setup_logging()
|
|
35
|
+
logger = get_logger(__name__)
|
|
36
|
+
|
|
37
|
+
# ── Configuration ──────────────────────────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
MODEL_PATH = str(REPO_ROOT / "models/Llama-3.3-70B-Instruct-Q4_K_M.gguf")
|
|
40
|
+
N_GPU_LAYERS = -1
|
|
41
|
+
TEMPERATURE = 0.1
|
|
42
|
+
N_CTX = int(131072 / 8)
|
|
43
|
+
MAX_TOKENS = int(65536 / 8)
|
|
44
|
+
MAX_WORKERS = 1
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def make_run_dir(goal_text: str) -> Path:
|
|
48
|
+
safe = goal_text.lower().replace(" ", "_")
|
|
49
|
+
safe = "".join(c for c in safe if c.isalnum() or c == "_")[:60]
|
|
50
|
+
run_dir = REPO_ROOT / "runs" / safe
|
|
51
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
52
|
+
(run_dir / "outputs").mkdir(exist_ok=True)
|
|
53
|
+
return run_dir
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main():
|
|
57
|
+
|
|
58
|
+
# ── CLI ───────────────────────────────────────────────────────────────────
|
|
59
|
+
parser = argparse.ArgumentParser(
|
|
60
|
+
prog="cuddlytoddly",
|
|
61
|
+
description="LLM-powered DAG planning and execution system.",
|
|
62
|
+
)
|
|
63
|
+
parser.add_argument(
|
|
64
|
+
"--terminal",
|
|
65
|
+
action="store_true",
|
|
66
|
+
default=False,
|
|
67
|
+
help="Launch the terminal UI instead of the web UI.",
|
|
68
|
+
)
|
|
69
|
+
parser.add_argument(
|
|
70
|
+
"--host",
|
|
71
|
+
default="127.0.0.1",
|
|
72
|
+
help="Host for the web UI server (default: 127.0.0.1).",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--port",
|
|
76
|
+
type=int,
|
|
77
|
+
default=8765,
|
|
78
|
+
help="Port for the web UI server (default: 8765).",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"goal",
|
|
82
|
+
nargs="*",
|
|
83
|
+
help=(
|
|
84
|
+
"Goal text to start immediately, skipping the startup screen. "
|
|
85
|
+
"If omitted the startup screen is shown."
|
|
86
|
+
),
|
|
87
|
+
)
|
|
88
|
+
args = parser.parse_args()
|
|
89
|
+
use_web = not args.terminal
|
|
90
|
+
|
|
91
|
+
# ── Startup screen ────────────────────────────────────────────────────────
|
|
92
|
+
# If a goal was passed on the CLI, skip the startup screen and go straight
|
|
93
|
+
# to a new-goal run. Otherwise show the appropriate startup UI.
|
|
94
|
+
inline_goal = " ".join(args.goal).strip()
|
|
95
|
+
|
|
96
|
+
if inline_goal:
|
|
97
|
+
# Bypass startup screen — behaves like the old CLI usage
|
|
98
|
+
choice = StartupChoice(
|
|
99
|
+
mode="new_goal",
|
|
100
|
+
run_dir=make_run_dir(inline_goal).resolve(),
|
|
101
|
+
goal_text=inline_goal,
|
|
102
|
+
is_fresh=True,
|
|
103
|
+
)
|
|
104
|
+
elif use_web:
|
|
105
|
+
# Web startup screen is shown inside the browser — no blocking call here.
|
|
106
|
+
# We use a deferred choice that will be filled in via /api/startup.
|
|
107
|
+
choice = None
|
|
108
|
+
else:
|
|
109
|
+
# Curses startup screen — blocks until the user makes a choice.
|
|
110
|
+
try:
|
|
111
|
+
choice = run_startup_curses(REPO_ROOT)
|
|
112
|
+
except SystemExit:
|
|
113
|
+
return # user pressed q
|
|
114
|
+
|
|
115
|
+
# ── For web UI with no inline goal, defer all init to init_fn ────────────
|
|
116
|
+
if use_web and choice is None:
|
|
117
|
+
|
|
118
|
+
def init_fn(ch: StartupChoice):
|
|
119
|
+
return _init_system(ch, use_web)
|
|
120
|
+
|
|
121
|
+
run_web_ui(
|
|
122
|
+
repo_root=REPO_ROOT,
|
|
123
|
+
init_fn=init_fn,
|
|
124
|
+
host=args.host,
|
|
125
|
+
port=args.port,
|
|
126
|
+
)
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# ── Init system ───────────────────────────────────────────────────────────
|
|
130
|
+
orchestrator, run_dir = _init_system(choice, use_web)
|
|
131
|
+
|
|
132
|
+
# ── Launch UI ─────────────────────────────────────────────────────────────
|
|
133
|
+
if use_web:
|
|
134
|
+
run_web_ui(
|
|
135
|
+
orchestrator=orchestrator,
|
|
136
|
+
run_dir=run_dir,
|
|
137
|
+
host=args.host,
|
|
138
|
+
port=args.port,
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
run_ui(
|
|
142
|
+
orchestrator,
|
|
143
|
+
run_dir=run_dir,
|
|
144
|
+
repo_root=REPO_ROOT,
|
|
145
|
+
restart_fn=_init_system,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# ── Final log ─────────────────────────────────────────────────────────────
|
|
149
|
+
snap = orchestrator.graph.get_snapshot()
|
|
150
|
+
logger.info("=== Final graph state (%d nodes) ===", len(snap))
|
|
151
|
+
for nid, n in snap.items():
|
|
152
|
+
logger.info(" [%s] %s", n.status, nid)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _init_system(choice: "StartupChoice", use_web: bool):
|
|
156
|
+
"""
|
|
157
|
+
Build the full orchestrator from a StartupChoice.
|
|
158
|
+
Extracted so both the curses and web startup paths share the same logic.
|
|
159
|
+
Returns (orchestrator, run_dir).
|
|
160
|
+
"""
|
|
161
|
+
goal_text = choice.goal_text
|
|
162
|
+
goal_id = goal_text.replace(" ", "_")[:60]
|
|
163
|
+
run_dir = choice.run_dir.resolve()
|
|
164
|
+
|
|
165
|
+
# ── Logging ───────────────────────────────────────────────────────────────
|
|
166
|
+
setup_logging(log_dir=run_dir / "logs")
|
|
167
|
+
_logger = get_logger(__name__)
|
|
168
|
+
_logger.info("=== cuddlytoddly starting mode=%s ui=%s ===",
|
|
169
|
+
choice.mode, "web" if use_web else "curses")
|
|
170
|
+
_logger.info("Run directory: %s", run_dir)
|
|
171
|
+
|
|
172
|
+
# ── Event log ─────────────────────────────────────────────────────────────
|
|
173
|
+
event_log_path = run_dir / "events.jsonl"
|
|
174
|
+
event_log = EventLog(str(event_log_path))
|
|
175
|
+
log_path = Path(event_log_path)
|
|
176
|
+
|
|
177
|
+
# ── LLM cache ─────────────────────────────────────────────────────────────
|
|
178
|
+
cache_path = str(run_dir / "llamacpp_cache.json")
|
|
179
|
+
|
|
180
|
+
# ── Git repo — per run ────────────────────────────────────────────────────
|
|
181
|
+
import cuddlytoddly.ui.git_projection as git_proj
|
|
182
|
+
git_proj.REPO_PATH = str(run_dir / "dag_repo")
|
|
183
|
+
|
|
184
|
+
# ── Working directory — sandbox file tools ────────────────────────────────
|
|
185
|
+
os.chdir(run_dir / "outputs")
|
|
186
|
+
_logger.info("Working directory: %s", Path.cwd())
|
|
187
|
+
|
|
188
|
+
llm_iface.id_gen = StableIDGenerator(
|
|
189
|
+
mapping_file=run_dir / "task_id_map.json",
|
|
190
|
+
id_length=6,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
# ── Graph init ────────────────────────────────────────────────────────────
|
|
194
|
+
if not choice.is_fresh and log_path.exists() and log_path.stat().st_size > 0:
|
|
195
|
+
_logger.info("[STARTUP] Replaying event log")
|
|
196
|
+
graph = rebuild_graph_from_log(event_log)
|
|
197
|
+
fresh_start = False
|
|
198
|
+
_logger.info("[STARTUP] Restored %d nodes", len(graph.nodes))
|
|
199
|
+
|
|
200
|
+
for step_id in [n.id for n in graph.nodes.values()
|
|
201
|
+
if n.node_type == "execution_step"]:
|
|
202
|
+
if step_id in graph.nodes:
|
|
203
|
+
graph.detach_node(step_id)
|
|
204
|
+
|
|
205
|
+
for node_id in {n.id for n in graph.nodes.values()
|
|
206
|
+
if n.status in ("running", "failed")
|
|
207
|
+
and n.node_type != "execution_step"}:
|
|
208
|
+
n = graph.nodes.get(node_id)
|
|
209
|
+
if n:
|
|
210
|
+
n.status = "pending"
|
|
211
|
+
n.result = None
|
|
212
|
+
n.metadata.pop("retry_count", None)
|
|
213
|
+
n.metadata.pop("verification_failure", None)
|
|
214
|
+
n.metadata.pop("verified", None)
|
|
215
|
+
|
|
216
|
+
graph.recompute_readiness()
|
|
217
|
+
else:
|
|
218
|
+
graph = TaskGraph()
|
|
219
|
+
fresh_start = True
|
|
220
|
+
|
|
221
|
+
# ── LLM / components ──────────────────────────────────────────────────────
|
|
222
|
+
shared_llm = create_llm_client(
|
|
223
|
+
"llamacpp",
|
|
224
|
+
model_path=MODEL_PATH,
|
|
225
|
+
n_gpu_layers=N_GPU_LAYERS,
|
|
226
|
+
temperature=TEMPERATURE,
|
|
227
|
+
n_ctx=N_CTX,
|
|
228
|
+
max_tokens=MAX_TOKENS,
|
|
229
|
+
cache_path=cache_path,
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
skills = SkillLoader()
|
|
233
|
+
registry = skills.registry
|
|
234
|
+
planner = LLMPlanner(llm_client=shared_llm, graph=graph,
|
|
235
|
+
skills_summary=skills.prompt_summary)
|
|
236
|
+
executor = LLMExecutor(llm_client=shared_llm, tool_registry=registry, max_turns=5)
|
|
237
|
+
quality_gate = QualityGate(llm_client=shared_llm, tool_registry=registry)
|
|
238
|
+
|
|
239
|
+
queue = EventQueue()
|
|
240
|
+
orchestrator = SimpleOrchestrator(
|
|
241
|
+
graph=graph, planner=planner, executor=executor,
|
|
242
|
+
quality_gate=quality_gate, event_log=event_log,
|
|
243
|
+
event_queue=queue, max_workers=MAX_WORKERS,
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
# ── Seed graph ────────────────────────────────────────────────────────────
|
|
247
|
+
if fresh_start:
|
|
248
|
+
if choice.mode == "manual_plan" and choice.plan_events:
|
|
249
|
+
_logger.info("[STARTUP] Seeding manual plan (%d events)",
|
|
250
|
+
len(choice.plan_events))
|
|
251
|
+
for evt_dict in choice.plan_events:
|
|
252
|
+
apply_event(graph, Event(evt_dict["type"], evt_dict["payload"]),
|
|
253
|
+
event_log=event_log)
|
|
254
|
+
else:
|
|
255
|
+
_logger.info("[STARTUP] Seeding new goal: %s", goal_text)
|
|
256
|
+
apply_event(graph, Event(ADD_NODE, {
|
|
257
|
+
"node_id": goal_id,
|
|
258
|
+
"node_type": "goal",
|
|
259
|
+
"dependencies": [],
|
|
260
|
+
"origin": "user",
|
|
261
|
+
"metadata": {"description": goal_text, "expanded": False},
|
|
262
|
+
}), event_log=event_log)
|
|
263
|
+
|
|
264
|
+
orchestrator.start()
|
|
265
|
+
|
|
266
|
+
if not fresh_start:
|
|
267
|
+
def _bg_verify():
|
|
268
|
+
_logger.info("[STARTUP] Background verification pass starting...")
|
|
269
|
+
orchestrator.verify_restored_nodes()
|
|
270
|
+
_logger.info("[STARTUP] Background verification complete")
|
|
271
|
+
threading.Thread(target=_bg_verify, daemon=True, name="startup-verify").start()
|
|
272
|
+
|
|
273
|
+
return orchestrator, run_dir
|
|
274
|
+
|
|
275
|
+
if __name__ == "__main__":
|
|
276
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event Definitions
|
|
3
|
+
|
|
4
|
+
All mutations must go through reducer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
# core/events.py
|
|
8
|
+
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Event:
|
|
13
|
+
def __init__(self, type, payload, timestamp=None):
|
|
14
|
+
self.type = type
|
|
15
|
+
self.payload = payload
|
|
16
|
+
self.timestamp = timestamp or datetime.utcnow().isoformat()
|
|
17
|
+
|
|
18
|
+
def to_dict(self):
|
|
19
|
+
return {
|
|
20
|
+
"type": self.type,
|
|
21
|
+
"payload": self.payload,
|
|
22
|
+
"timestamp": self.timestamp,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def from_dict(cls, data):
|
|
27
|
+
return cls(
|
|
28
|
+
type=data["type"],
|
|
29
|
+
payload=data["payload"],
|
|
30
|
+
timestamp=data.get("timestamp"),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
# Event types
|
|
34
|
+
ADD_NODE = "ADD_NODE"
|
|
35
|
+
REMOVE_NODE = "REMOVE_NODE"
|
|
36
|
+
ADD_DEPENDENCY = "ADD_DEPENDENCY"
|
|
37
|
+
REMOVE_DEPENDENCY = "REMOVE_DEPENDENCY"
|
|
38
|
+
MARK_RUNNING = "MARK_RUNNING"
|
|
39
|
+
MARK_DONE = "MARK_DONE"
|
|
40
|
+
MARK_FAILED = "MARK_FAILED"
|
|
41
|
+
RESET_NODE = "RESET_NODE"
|
|
42
|
+
UPDATE_METADATA = "UPDATE_METADATA"
|
|
43
|
+
DETACH_NODE = "DETACH_NODE"
|
|
44
|
+
UPDATE_STATUS = "UPDATE_STATUS"
|
|
45
|
+
SET_RESULT = "SET_RESULT"
|
|
46
|
+
SET_NODE_TYPE = "SET_NODE_TYPE"
|
|
47
|
+
RESET_SUBTREE = "RESET_SUBTREE"
|