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.
Files changed (61) hide show
  1. cuddlytoddly-0.1.0/LICENSE +21 -0
  2. cuddlytoddly-0.1.0/PKG-INFO +165 -0
  3. cuddlytoddly-0.1.0/README.md +128 -0
  4. cuddlytoddly-0.1.0/cuddlytoddly/__init__.py +1 -0
  5. cuddlytoddly-0.1.0/cuddlytoddly/__main__.py +276 -0
  6. cuddlytoddly-0.1.0/cuddlytoddly/core/__init__.py +1 -0
  7. cuddlytoddly-0.1.0/cuddlytoddly/core/events.py +47 -0
  8. cuddlytoddly-0.1.0/cuddlytoddly/core/id_generator.py +78 -0
  9. cuddlytoddly-0.1.0/cuddlytoddly/core/reducer.py +124 -0
  10. cuddlytoddly-0.1.0/cuddlytoddly/core/task_graph.py +273 -0
  11. cuddlytoddly-0.1.0/cuddlytoddly/engine/__init__.py +1 -0
  12. cuddlytoddly-0.1.0/cuddlytoddly/engine/execution_step_reporter.py +243 -0
  13. cuddlytoddly-0.1.0/cuddlytoddly/engine/llm_orchestrator.py +882 -0
  14. cuddlytoddly-0.1.0/cuddlytoddly/engine/quality_gate.py +274 -0
  15. cuddlytoddly-0.1.0/cuddlytoddly/infra/__init__.py +1 -0
  16. cuddlytoddly-0.1.0/cuddlytoddly/infra/event_log.py +55 -0
  17. cuddlytoddly-0.1.0/cuddlytoddly/infra/event_queue.py +19 -0
  18. cuddlytoddly-0.1.0/cuddlytoddly/infra/logging.py +122 -0
  19. cuddlytoddly-0.1.0/cuddlytoddly/infra/replay.py +19 -0
  20. cuddlytoddly-0.1.0/cuddlytoddly/planning/__init__.py +1 -0
  21. cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_executor.py +420 -0
  22. cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_interface.py +1290 -0
  23. cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_output_validator.py +237 -0
  24. cuddlytoddly-0.1.0/cuddlytoddly/planning/llm_planner.py +344 -0
  25. cuddlytoddly-0.1.0/cuddlytoddly/skills/__init__.py +0 -0
  26. cuddlytoddly-0.1.0/cuddlytoddly/skills/code_execution/__init__.py +0 -0
  27. cuddlytoddly-0.1.0/cuddlytoddly/skills/code_execution/tools.py +65 -0
  28. cuddlytoddly-0.1.0/cuddlytoddly/skills/file_ops/__init__.py +0 -0
  29. cuddlytoddly-0.1.0/cuddlytoddly/skills/file_ops/tools.py +38 -0
  30. cuddlytoddly-0.1.0/cuddlytoddly/skills/skill_loader.py +210 -0
  31. cuddlytoddly-0.1.0/cuddlytoddly/tools/__init__.py +0 -0
  32. cuddlytoddly-0.1.0/cuddlytoddly/tools/mcp_adapter.py +200 -0
  33. cuddlytoddly-0.1.0/cuddlytoddly/tools/registry.py +0 -0
  34. cuddlytoddly-0.1.0/cuddlytoddly/ui/__init__.py +1 -0
  35. cuddlytoddly-0.1.0/cuddlytoddly/ui/curses_startup.py +430 -0
  36. cuddlytoddly-0.1.0/cuddlytoddly/ui/curses_ui.py +1504 -0
  37. cuddlytoddly-0.1.0/cuddlytoddly/ui/git_projection.py +321 -0
  38. cuddlytoddly-0.1.0/cuddlytoddly/ui/startup.py +561 -0
  39. cuddlytoddly-0.1.0/cuddlytoddly/ui/web_server.py +588 -0
  40. cuddlytoddly-0.1.0/cuddlytoddly/ui/web_ui.html +1668 -0
  41. cuddlytoddly-0.1.0/cuddlytoddly/ui/web_ui_startup.html +544 -0
  42. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/PKG-INFO +165 -0
  43. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/SOURCES.txt +59 -0
  44. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/dependency_links.txt +1 -0
  45. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/entry_points.txt +2 -0
  46. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/requires.txt +23 -0
  47. cuddlytoddly-0.1.0/cuddlytoddly.egg-info/top_level.txt +1 -0
  48. cuddlytoddly-0.1.0/pyproject.toml +79 -0
  49. cuddlytoddly-0.1.0/setup.cfg +4 -0
  50. cuddlytoddly-0.1.0/tests/test_events_reducer.py +248 -0
  51. cuddlytoddly-0.1.0/tests/test_id_generator.py +97 -0
  52. cuddlytoddly-0.1.0/tests/test_infra.py +221 -0
  53. cuddlytoddly-0.1.0/tests/test_integration.py +286 -0
  54. cuddlytoddly-0.1.0/tests/test_llm_executor.py +303 -0
  55. cuddlytoddly-0.1.0/tests/test_llm_interface.py +255 -0
  56. cuddlytoddly-0.1.0/tests/test_llm_validator.py +229 -0
  57. cuddlytoddly-0.1.0/tests/test_orchestrator.py +323 -0
  58. cuddlytoddly-0.1.0/tests/test_quality_gate.py +284 -0
  59. cuddlytoddly-0.1.0/tests/test_skill_loader.py +195 -0
  60. cuddlytoddly-0.1.0/tests/test_startup.py +277 -0
  61. 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,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,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"