adelie-ai 0.1.0
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.
- package/LICENSE +21 -0
- package/README.md +335 -0
- package/adelie/.python-version +1 -0
- package/adelie/README.md +0 -0
- package/adelie/__init__.py +1 -0
- package/adelie/a2a/__init__.py +16 -0
- package/adelie/a2a/persistence.py +83 -0
- package/adelie/a2a/server.py +199 -0
- package/adelie/a2a/types.py +90 -0
- package/adelie/agents/__init__.py +1 -0
- package/adelie/agents/analyst_ai.py +247 -0
- package/adelie/agents/coder_ai.py +294 -0
- package/adelie/agents/coder_manager.py +349 -0
- package/adelie/agents/expert_ai.py +470 -0
- package/adelie/agents/inform_ai.py +138 -0
- package/adelie/agents/monitor_ai.py +241 -0
- package/adelie/agents/research_ai.py +224 -0
- package/adelie/agents/reviewer_ai.py +165 -0
- package/adelie/agents/runner_ai.py +527 -0
- package/adelie/agents/scanner_ai.py +380 -0
- package/adelie/agents/tester_ai.py +361 -0
- package/adelie/agents/writer_ai.py +286 -0
- package/adelie/browser_search.py +417 -0
- package/adelie/channels/__init__.py +22 -0
- package/adelie/channels/base.py +144 -0
- package/adelie/channels/discord.py +78 -0
- package/adelie/channels/router.py +150 -0
- package/adelie/channels/slack.py +74 -0
- package/adelie/checkpoint.py +291 -0
- package/adelie/cli.py +1673 -0
- package/adelie/command_loader.py +137 -0
- package/adelie/config.py +88 -0
- package/adelie/context_compactor.py +360 -0
- package/adelie/context_engine.py +445 -0
- package/adelie/env_strategy.py +511 -0
- package/adelie/feedback_queue.py +159 -0
- package/adelie/gateway.py +276 -0
- package/adelie/git_ops.py +170 -0
- package/adelie/hooks.py +236 -0
- package/adelie/i18n.py +122 -0
- package/adelie/integrations/__init__.py +1 -0
- package/adelie/integrations/telegram_bot.py +471 -0
- package/adelie/interactive.py +467 -0
- package/adelie/kb/__init__.py +1 -0
- package/adelie/kb/embedding_store.py +249 -0
- package/adelie/kb/retriever.py +313 -0
- package/adelie/llm_client.py +536 -0
- package/adelie/log_rotation.py +67 -0
- package/adelie/loop_detector.py +554 -0
- package/adelie/loop_manual.md +141 -0
- package/adelie/main.py +6 -0
- package/adelie/mcp_client.py +374 -0
- package/adelie/mcp_manager.py +238 -0
- package/adelie/metrics.py +307 -0
- package/adelie/orchestrator.py +1486 -0
- package/adelie/phases.py +300 -0
- package/adelie/plan_mode.py +245 -0
- package/adelie/process_supervisor.py +351 -0
- package/adelie/project_context.py +196 -0
- package/adelie/prompt_loader.py +175 -0
- package/adelie/prompts/coder.md +30 -0
- package/adelie/prompts/expert.md +104 -0
- package/adelie/prompts/reviewer.md +32 -0
- package/adelie/pyproject.toml +15 -0
- package/adelie/registry.py +88 -0
- package/adelie/rules_loader.py +112 -0
- package/adelie/sandbox.py +388 -0
- package/adelie/scheduler.py +265 -0
- package/adelie/skill_manager.py +422 -0
- package/adelie/spec_chunker.py +241 -0
- package/adelie/spec_loader.py +384 -0
- package/adelie/tool_registry.py +369 -0
- package/adelie/ui_logger.py +337 -0
- package/adelie/utils/__init__.py +0 -0
- package/adelie/utils/dep_sync.py +193 -0
- package/adelie/utils/import_checker.py +279 -0
- package/adelie/web_search.py +239 -0
- package/bin/adelie.js +82 -0
- package/package.json +47 -0
- package/requirements.txt +20 -0
- package/scripts/postinstall.js +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 kimhyunbin
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="docs/adelie_logo.jpeg" alt="Adelie Logo" width="200" />
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
<h1 align="center">Adelie</h1>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<strong>Self-Communicating Autonomous AI Loop System</strong><br/>
|
|
9
|
+
An AI orchestrator that plans, codes, reviews, tests, deploys, and evolves — autonomously.
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="https://img.shields.io/badge/python-3.10+-blue?logo=python" alt="Python 3.10+" />
|
|
14
|
+
<img src="https://img.shields.io/badge/LLM-Gemini%20%7C%20Ollama-orange" alt="LLM Support" />
|
|
15
|
+
<img src="https://img.shields.io/badge/license-MIT-green" alt="MIT License" />
|
|
16
|
+
<img src="https://img.shields.io/badge/tests-374%20passed-brightgreen" alt="Tests" />
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## What is Adelie?
|
|
22
|
+
|
|
23
|
+
Adelie is an **autonomous AI loop system** that orchestrates multiple specialized AI agents to build, maintain, and evolve software projects — with minimal human intervention.
|
|
24
|
+
|
|
25
|
+
Think of it as an AI team that continuously works on your project:
|
|
26
|
+
|
|
27
|
+
| Agent | Role |
|
|
28
|
+
|-------|------|
|
|
29
|
+
| **Expert AI** | Makes strategic decisions, dispatches tasks, manages state |
|
|
30
|
+
| **Writer AI** | Creates and maintains the Knowledge Base (documentation) |
|
|
31
|
+
| **Coder AI** | Writes actual source code in a layered architecture |
|
|
32
|
+
| **Reviewer AI** | Reviews code quality, feeds back to coders |
|
|
33
|
+
| **Tester AI** | Runs tests, reports failures back for fixes |
|
|
34
|
+
| **Runner AI** | Builds, deploys, and runs the project |
|
|
35
|
+
| **Monitor AI** | Checks system health, triggers restarts |
|
|
36
|
+
| **Analyst AI** | Provides project-level insights and analysis |
|
|
37
|
+
| **Research AI** | Searches the web for external information |
|
|
38
|
+
| **Scanner AI** | Scans existing codebases on first run |
|
|
39
|
+
|
|
40
|
+
All agents communicate through a **file-based Knowledge Base** and are coordinated by the **Orchestrator** — an endless loop with a built-in state machine.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Architecture
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
┌─────────────────────────────────────────────────────┐
|
|
48
|
+
│ ORCHESTRATOR │
|
|
49
|
+
│ │
|
|
50
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────┐ │
|
|
51
|
+
│ │ Writer AI│───>│Expert AI │───>│ Coder Manager│ │
|
|
52
|
+
│ └──────────┘ └──────────┘ └──────┬───────┘ │
|
|
53
|
+
│ │ │ │ │
|
|
54
|
+
│ v │ ┌──────┴──────┐ │
|
|
55
|
+
│ ┌──────────┐ │ │ Layer 0-2 │ │
|
|
56
|
+
│ │Knowledge │ │ │ Coders │ │
|
|
57
|
+
│ │ Base │<────────┘ └──────┬──────┘ │
|
|
58
|
+
│ └──────────┘ │ │
|
|
59
|
+
│ v │
|
|
60
|
+
│ ┌──────────┐ ┌──────────┐ ┌──────────────────┐│
|
|
61
|
+
│ │ Reviewer │ │ Tester │ │ Runner / Monitor ││
|
|
62
|
+
│ │ AI │ │ AI │ │ AI ││
|
|
63
|
+
│ └──────────┘ └──────────┘ └──────────────────┘│
|
|
64
|
+
│ │
|
|
65
|
+
│ ┌──────────────────────────────────────────────┐ │
|
|
66
|
+
│ │ Loop Detector │ Scheduler │ Process Supv. │ │
|
|
67
|
+
│ └──────────────────────────────────────────────┘ │
|
|
68
|
+
└─────────────────────────────────────────────────────┘
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Project Lifecycle (Phases)
|
|
74
|
+
|
|
75
|
+
Adelie evolves your project through 6 phases:
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
INITIAL ──> MID ──> MID_1 ──> MID_2 ──> LATE ──> EVOLVE
|
|
79
|
+
Planning Coding Testing Optimizing Maintaining Autonomous
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
| Phase | Focus | Coder Layers |
|
|
83
|
+
|-------|-------|-------------|
|
|
84
|
+
| Initial | Documentation, architecture, roadmap | None |
|
|
85
|
+
| Mid | Implementation, feature coding | Layer 0 (features) |
|
|
86
|
+
| Mid-1 | Integration, testing, roadmap check | Layer 0-1 (+ connectors) |
|
|
87
|
+
| Mid-2 | Stabilization, optimization, deployment | Layer 0-2 (+ infra) |
|
|
88
|
+
| Late | Maintenance, new features | All layers |
|
|
89
|
+
| Evolve | Autonomous evolution, self-improvement | All layers |
|
|
90
|
+
|
|
91
|
+
Phase transitions are **gated** by quality metrics (KB file count, test pass rate, review scores) and confirmed by the Expert AI.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Safety Harnesses
|
|
96
|
+
|
|
97
|
+
Adelie includes multiple built-in safety mechanisms:
|
|
98
|
+
|
|
99
|
+
| Harness | Purpose |
|
|
100
|
+
|---------|---------|
|
|
101
|
+
| **Loop Detector** | Detects 5 types of repetitive patterns with escalating interventions |
|
|
102
|
+
| **Phase Gates** | Prevents premature transitions with quality thresholds |
|
|
103
|
+
| **Context Budget** | Per-agent token budgets prevent unbounded prompt growth |
|
|
104
|
+
| **Process Supervisor** | Timeout enforcement, orphan cleanup, concurrent limits |
|
|
105
|
+
| **Reviewer Loop** | Code review → feedback → retry cycle (max 2 retries) |
|
|
106
|
+
| **Tester Loop** | Test failure → coder fix → re-test cycle (max 2 retries) |
|
|
107
|
+
| **Expert Fallback** | JSON retry + regex extraction + safe fallback decision |
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Quick Start
|
|
112
|
+
|
|
113
|
+
### Prerequisites
|
|
114
|
+
|
|
115
|
+
- **Python 3.10+**
|
|
116
|
+
- **Node.js 16+** (for the CLI wrapper)
|
|
117
|
+
- **Gemini API key** or **Ollama** running locally
|
|
118
|
+
|
|
119
|
+
### Installation
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
# Install via npm (recommended)
|
|
123
|
+
npm install -g adelie
|
|
124
|
+
|
|
125
|
+
# Or install from source
|
|
126
|
+
git clone https://github.com/kimhyunbin/Adelie.git
|
|
127
|
+
cd Adelie
|
|
128
|
+
npm install -g .
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Setup
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# Initialize a workspace in your project directory
|
|
135
|
+
cd /path/to/your/project
|
|
136
|
+
adelie init
|
|
137
|
+
|
|
138
|
+
# Configure LLM provider
|
|
139
|
+
adelie config --provider gemini --api-key YOUR_GEMINI_API_KEY
|
|
140
|
+
|
|
141
|
+
# Or use Ollama (local, free)
|
|
142
|
+
adelie config --provider ollama --model gemma3:12b
|
|
143
|
+
|
|
144
|
+
# Set display language (ko or en)
|
|
145
|
+
adelie config --lang en
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Run
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
# Start the autonomous AI loop
|
|
152
|
+
adelie run --goal "Build a REST API for task management"
|
|
153
|
+
|
|
154
|
+
# Or run a single cycle
|
|
155
|
+
adelie run once --goal "Analyze and document the codebase"
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## CLI Reference
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
Workspace
|
|
164
|
+
adelie init [dir] Initialize workspace (default: current dir)
|
|
165
|
+
adelie ws List all workspaces
|
|
166
|
+
adelie ws remove <N> Remove workspace #N
|
|
167
|
+
|
|
168
|
+
Run
|
|
169
|
+
adelie run --goal "..." Start AI loop
|
|
170
|
+
adelie run ws <N> Resume loop in workspace #N
|
|
171
|
+
adelie run once --goal "..." Run exactly one cycle
|
|
172
|
+
|
|
173
|
+
Configuration
|
|
174
|
+
adelie config Show current config
|
|
175
|
+
adelie config --provider ... Switch LLM provider (gemini/ollama)
|
|
176
|
+
adelie config --model ... Set model name
|
|
177
|
+
adelie config --interval N Set loop interval (seconds)
|
|
178
|
+
adelie config --api-key KEY Set Gemini API key
|
|
179
|
+
adelie config --lang ko|en Set display language
|
|
180
|
+
|
|
181
|
+
Monitoring
|
|
182
|
+
adelie status System health & provider status
|
|
183
|
+
adelie inform Generate project status report
|
|
184
|
+
adelie phase Show current project phase
|
|
185
|
+
adelie phase set <phase> Set phase manually
|
|
186
|
+
|
|
187
|
+
Knowledge Base
|
|
188
|
+
adelie kb Show KB file counts per category
|
|
189
|
+
adelie kb --clear-errors Clear error files
|
|
190
|
+
adelie kb --reset Reset entire KB (destructive)
|
|
191
|
+
|
|
192
|
+
Project Management
|
|
193
|
+
adelie goal Show current project goal
|
|
194
|
+
adelie goal set "..." Set project goal
|
|
195
|
+
adelie feedback "message" Send feedback to the AI loop
|
|
196
|
+
adelie research "topic" Search the web and save to KB
|
|
197
|
+
adelie git Show git status & recent commits
|
|
198
|
+
adelie metrics Show recent cycle metrics
|
|
199
|
+
|
|
200
|
+
Ollama
|
|
201
|
+
adelie ollama list List installed models
|
|
202
|
+
adelie ollama pull <model> Download a model
|
|
203
|
+
adelie ollama run [model] Interactive chat
|
|
204
|
+
|
|
205
|
+
Telegram
|
|
206
|
+
adelie telegram setup Setup bot token
|
|
207
|
+
adelie telegram start Start Telegram bot
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Environment Variables
|
|
213
|
+
|
|
214
|
+
All settings are stored in `.adelie/.env` (created by `adelie init`):
|
|
215
|
+
|
|
216
|
+
| Variable | Default | Description |
|
|
217
|
+
|----------|---------|-------------|
|
|
218
|
+
| `LLM_PROVIDER` | `gemini` | `gemini` or `ollama` |
|
|
219
|
+
| `GEMINI_API_KEY` | — | Required for Gemini provider |
|
|
220
|
+
| `GEMINI_MODEL` | `gemini-2.0-flash` | Gemini model name |
|
|
221
|
+
| `OLLAMA_BASE_URL` | `http://localhost:11434` | Ollama server URL |
|
|
222
|
+
| `OLLAMA_MODEL` | `llama3.2` | Ollama model name |
|
|
223
|
+
| `FALLBACK_MODELS` | — | Comma-separated fallback chain (e.g. `gemini:gemini-2.5-flash,ollama:llama3.2`) |
|
|
224
|
+
| `LOOP_INTERVAL_SECONDS` | `30` | Seconds between loop cycles |
|
|
225
|
+
| `ADELIE_LANGUAGE` | `ko` | Display language (`ko` or `en`) |
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## Knowledge Base Structure
|
|
230
|
+
|
|
231
|
+
The KB uses 6 categories:
|
|
232
|
+
|
|
233
|
+
```
|
|
234
|
+
.adelie/workspace/
|
|
235
|
+
├── skills/ # How-to guides, procedures, capabilities
|
|
236
|
+
├── dependencies/ # External APIs, libraries, services
|
|
237
|
+
├── errors/ # Known errors, root causes, recovery
|
|
238
|
+
├── logic/ # Decision patterns, planning docs
|
|
239
|
+
├── exports/ # Reports, roadmaps, outputs
|
|
240
|
+
└── maintenance/ # System health, status updates
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
All KB files are Markdown with tag-based and semantic (embedding) retrieval.
|
|
244
|
+
|
|
245
|
+
---
|
|
246
|
+
|
|
247
|
+
## Testing
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
# Run all tests
|
|
251
|
+
python -m pytest tests/ -v
|
|
252
|
+
|
|
253
|
+
# Run specific test file
|
|
254
|
+
python -m pytest tests/test_orchestrator.py -v
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Currently **374 tests** covering all agents, context engine, loop detection, scheduling, and more.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## Project Structure
|
|
262
|
+
|
|
263
|
+
```
|
|
264
|
+
Adelie/
|
|
265
|
+
├── adelie/ # Core package
|
|
266
|
+
│ ├── orchestrator.py # Main loop controller (state machine)
|
|
267
|
+
│ ├── cli.py # CLI commands
|
|
268
|
+
│ ├── config.py # Configuration & env loading
|
|
269
|
+
│ ├── i18n.py # Internationalization (ko/en)
|
|
270
|
+
│ ├── llm_client.py # LLM abstraction (Gemini + Ollama)
|
|
271
|
+
│ ├── scheduler.py # Per-agent scheduling
|
|
272
|
+
│ ├── phases.py # Project lifecycle phases
|
|
273
|
+
│ ├── hooks.py # Event-driven plugin system
|
|
274
|
+
│ ├── loop_detector.py # Stuck-loop detection
|
|
275
|
+
│ ├── context_engine.py # Per-agent context assembly
|
|
276
|
+
│ ├── context_compactor.py # Token budget enforcement
|
|
277
|
+
│ ├── process_supervisor.py# Subprocess management
|
|
278
|
+
│ ├── feedback_queue.py # User feedback injection
|
|
279
|
+
│ ├── git_ops.py # Git auto-commit
|
|
280
|
+
│ ├── web_search.py # Web search for Research AI
|
|
281
|
+
│ ├── kb/ # Knowledge Base
|
|
282
|
+
│ │ ├── retriever.py # Tag + semantic KB retrieval
|
|
283
|
+
│ │ └── embedding_store.py
|
|
284
|
+
│ ├── agents/ # AI agents
|
|
285
|
+
│ │ ├── writer_ai.py # KB file generation
|
|
286
|
+
│ │ ├── expert_ai.py # Decision-making
|
|
287
|
+
│ │ ├── coder_ai.py # Code generation
|
|
288
|
+
│ │ ├── coder_manager.py # Multi-layer coder orchestration
|
|
289
|
+
│ │ ├── reviewer_ai.py # Code review
|
|
290
|
+
│ │ ├── tester_ai.py # Test execution
|
|
291
|
+
│ │ ├── runner_ai.py # Build & deploy
|
|
292
|
+
│ │ ├── monitor_ai.py # Health checks
|
|
293
|
+
│ │ ├── analyst_ai.py # Project analysis
|
|
294
|
+
│ │ ├── research_ai.py # Web research
|
|
295
|
+
│ │ ├── scanner_ai.py # Codebase scanning
|
|
296
|
+
│ │ └── inform_ai.py # Status reports
|
|
297
|
+
│ └── integrations/
|
|
298
|
+
│ └── telegram_bot.py # Telegram integration
|
|
299
|
+
├── tests/ # 374 tests
|
|
300
|
+
├── bin/ # Node.js CLI wrapper
|
|
301
|
+
├── scripts/ # Install scripts
|
|
302
|
+
├── requirements.txt # Python dependencies
|
|
303
|
+
└── package.json # npm package config
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
---
|
|
307
|
+
|
|
308
|
+
## How It Works
|
|
309
|
+
|
|
310
|
+
Each orchestrator cycle runs these steps:
|
|
311
|
+
|
|
312
|
+
1. **Writer AI** creates/updates Knowledge Base files
|
|
313
|
+
2. **Expert AI** reads the KB and makes a structured decision (JSON)
|
|
314
|
+
3. **Research AI** searches the web if the Expert requested external info
|
|
315
|
+
4. **Coder Manager** dispatches code generation tasks by layer
|
|
316
|
+
5. **Reviewer AI** reviews the generated code; retries on failure
|
|
317
|
+
6. **Staging → Project** promotes approved code to the project
|
|
318
|
+
7. **Tester AI** runs tests; retries on failure
|
|
319
|
+
8. **Runner AI** builds and deploys
|
|
320
|
+
9. **Monitor AI** checks health; restarts if needed
|
|
321
|
+
10. **Phase Gates** check if the project is ready for the next phase
|
|
322
|
+
|
|
323
|
+
The loop runs continuously until shutdown, with the **Scheduler** controlling how often each agent runs and the **Loop Detector** intervening when the system gets stuck.
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## License
|
|
328
|
+
|
|
329
|
+
MIT — see [LICENSE](./LICENSE) for details.
|
|
330
|
+
|
|
331
|
+
---
|
|
332
|
+
|
|
333
|
+
<p align="center">
|
|
334
|
+
Made with Adelie
|
|
335
|
+
</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.10
|
package/adelie/README.md
ADDED
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Adelie — Self-communicating autonomous AI loop system."""
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adelie/a2a/__init__.py
|
|
3
|
+
|
|
4
|
+
Agent-to-Agent (A2A) protocol for Adelie.
|
|
5
|
+
Allows external agents to create tasks, query status,
|
|
6
|
+
and receive real-time events.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from adelie.a2a.types import (
|
|
10
|
+
TaskState,
|
|
11
|
+
A2ATask,
|
|
12
|
+
A2AEvent,
|
|
13
|
+
EventType,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
__all__ = ["TaskState", "A2ATask", "A2AEvent", "EventType"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adelie/a2a/persistence.py
|
|
3
|
+
|
|
4
|
+
Task persistence — stores A2A tasks to disk for durability.
|
|
5
|
+
Inspired by Gemini CLI's a2a-server persistence module.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Dict, Optional
|
|
13
|
+
|
|
14
|
+
from adelie.a2a.types import A2ATask, TaskState, A2AEvent, EventType
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class TaskStore:
|
|
18
|
+
"""
|
|
19
|
+
Persists A2A tasks to JSON files.
|
|
20
|
+
|
|
21
|
+
Storage: {store_dir}/{task_id}.json
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self, store_dir: Optional[Path] = None):
|
|
25
|
+
if store_dir is None:
|
|
26
|
+
from adelie.config import ADELIE_ROOT
|
|
27
|
+
store_dir = ADELIE_ROOT / "a2a_tasks"
|
|
28
|
+
self._dir = store_dir
|
|
29
|
+
self._dir.mkdir(parents=True, exist_ok=True)
|
|
30
|
+
self._cache: Dict[str, A2ATask] = {}
|
|
31
|
+
|
|
32
|
+
def save(self, task: A2ATask) -> None:
|
|
33
|
+
"""Save a task to disk and cache."""
|
|
34
|
+
self._cache[task.task_id] = task
|
|
35
|
+
path = self._dir / f"{task.task_id}.json"
|
|
36
|
+
path.write_text(
|
|
37
|
+
json.dumps(task.to_dict(), indent=2, ensure_ascii=False),
|
|
38
|
+
encoding="utf-8",
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
def load(self, task_id: str) -> Optional[A2ATask]:
|
|
42
|
+
"""Load a task from cache or disk."""
|
|
43
|
+
if task_id in self._cache:
|
|
44
|
+
return self._cache[task_id]
|
|
45
|
+
|
|
46
|
+
path = self._dir / f"{task_id}.json"
|
|
47
|
+
if not path.exists():
|
|
48
|
+
return None
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
52
|
+
task = A2ATask(
|
|
53
|
+
task_id=data["task_id"],
|
|
54
|
+
prompt=data.get("prompt", ""),
|
|
55
|
+
state=TaskState(data.get("state", "submitted")),
|
|
56
|
+
created_at=data.get("created_at", ""),
|
|
57
|
+
updated_at=data.get("updated_at", ""),
|
|
58
|
+
result=data.get("result", ""),
|
|
59
|
+
error=data.get("error", ""),
|
|
60
|
+
metadata=data.get("metadata", {}),
|
|
61
|
+
)
|
|
62
|
+
self._cache[task_id] = task
|
|
63
|
+
return task
|
|
64
|
+
except Exception:
|
|
65
|
+
return None
|
|
66
|
+
|
|
67
|
+
def delete(self, task_id: str) -> bool:
|
|
68
|
+
"""Delete a task."""
|
|
69
|
+
self._cache.pop(task_id, None)
|
|
70
|
+
path = self._dir / f"{task_id}.json"
|
|
71
|
+
if path.exists():
|
|
72
|
+
path.unlink()
|
|
73
|
+
return True
|
|
74
|
+
return False
|
|
75
|
+
|
|
76
|
+
def list_tasks(self) -> list[A2ATask]:
|
|
77
|
+
"""List all persisted tasks."""
|
|
78
|
+
tasks = []
|
|
79
|
+
for f in sorted(self._dir.glob("*.json")):
|
|
80
|
+
task = self.load(f.stem)
|
|
81
|
+
if task:
|
|
82
|
+
tasks.append(task)
|
|
83
|
+
return tasks
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""
|
|
2
|
+
adelie/a2a/server.py
|
|
3
|
+
|
|
4
|
+
A2A HTTP server — extends the Gateway with agent-to-agent endpoints.
|
|
5
|
+
|
|
6
|
+
Endpoints:
|
|
7
|
+
POST /a2a/tasks — Create a new task
|
|
8
|
+
GET /a2a/tasks — List all tasks
|
|
9
|
+
GET /a2a/tasks/<id> — Get task status
|
|
10
|
+
POST /a2a/tasks/<id>/cancel — Cancel a task
|
|
11
|
+
|
|
12
|
+
Inspired by Gemini CLI's a2a-server HTTP layer.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import logging
|
|
19
|
+
import re
|
|
20
|
+
import threading
|
|
21
|
+
from http.server import HTTPServer, BaseHTTPRequestHandler
|
|
22
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
23
|
+
from urllib.parse import urlparse
|
|
24
|
+
|
|
25
|
+
from adelie.a2a.types import A2ATask, TaskState, EventType
|
|
26
|
+
from adelie.a2a.persistence import TaskStore
|
|
27
|
+
|
|
28
|
+
logger = logging.getLogger("adelie.a2a")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class A2AServer:
|
|
32
|
+
"""
|
|
33
|
+
Agent-to-Agent protocol server.
|
|
34
|
+
|
|
35
|
+
Can run standalone or alongside the Gateway on a different port.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
port: int = 8090,
|
|
41
|
+
host: str = "127.0.0.1",
|
|
42
|
+
token: str = "",
|
|
43
|
+
store: Optional[TaskStore] = None,
|
|
44
|
+
):
|
|
45
|
+
self._port = port
|
|
46
|
+
self._host = host
|
|
47
|
+
self._token = token
|
|
48
|
+
self._store = store or TaskStore()
|
|
49
|
+
self._server: Optional[HTTPServer] = None
|
|
50
|
+
self._thread: Optional[threading.Thread] = None
|
|
51
|
+
self._task_handler: Optional[Callable[[A2ATask], None]] = None
|
|
52
|
+
|
|
53
|
+
def start(self) -> bool:
|
|
54
|
+
"""Start the A2A server in background."""
|
|
55
|
+
if self._server:
|
|
56
|
+
return False
|
|
57
|
+
try:
|
|
58
|
+
handler = _make_a2a_handler(self)
|
|
59
|
+
self._server = HTTPServer((self._host, self._port), handler)
|
|
60
|
+
self._thread = threading.Thread(
|
|
61
|
+
target=self._server.serve_forever,
|
|
62
|
+
daemon=True,
|
|
63
|
+
name="adelie-a2a",
|
|
64
|
+
)
|
|
65
|
+
self._thread.start()
|
|
66
|
+
logger.info(f"A2A server started on http://{self._host}:{self._port}")
|
|
67
|
+
return True
|
|
68
|
+
except Exception as e:
|
|
69
|
+
logger.error(f"A2A server start failed: {e}")
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
def stop(self) -> None:
|
|
73
|
+
if self._server:
|
|
74
|
+
self._server.shutdown()
|
|
75
|
+
self._server = None
|
|
76
|
+
self._thread = None
|
|
77
|
+
|
|
78
|
+
@property
|
|
79
|
+
def is_running(self) -> bool:
|
|
80
|
+
return self._server is not None
|
|
81
|
+
|
|
82
|
+
def on_task(self, handler: Callable[[A2ATask], None]) -> None:
|
|
83
|
+
"""Register handler for new tasks (called when task is submitted)."""
|
|
84
|
+
self._task_handler = handler
|
|
85
|
+
|
|
86
|
+
# ── Task Operations ──────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
def create_task(self, prompt: str, metadata: Dict = None) -> A2ATask:
|
|
89
|
+
"""Create a new task."""
|
|
90
|
+
task = A2ATask(prompt=prompt, metadata=metadata or {})
|
|
91
|
+
self._store.save(task)
|
|
92
|
+
if self._task_handler:
|
|
93
|
+
self._task_handler(task)
|
|
94
|
+
return task
|
|
95
|
+
|
|
96
|
+
def get_task(self, task_id: str) -> Optional[A2ATask]:
|
|
97
|
+
return self._store.load(task_id)
|
|
98
|
+
|
|
99
|
+
def list_tasks(self) -> List[A2ATask]:
|
|
100
|
+
return self._store.list_tasks()
|
|
101
|
+
|
|
102
|
+
def cancel_task(self, task_id: str) -> bool:
|
|
103
|
+
task = self._store.load(task_id)
|
|
104
|
+
if not task or task.is_terminal:
|
|
105
|
+
return False
|
|
106
|
+
task.transition(TaskState.CANCELLED)
|
|
107
|
+
self._store.save(task)
|
|
108
|
+
return True
|
|
109
|
+
|
|
110
|
+
def check_auth(self, headers: dict) -> bool:
|
|
111
|
+
if not self._token:
|
|
112
|
+
return True
|
|
113
|
+
auth = headers.get("Authorization", "")
|
|
114
|
+
return auth == f"Bearer {self._token}"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
# ── HTTP Handler ─────────────────────────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _make_a2a_handler(server: A2AServer):
|
|
121
|
+
|
|
122
|
+
class A2AHandler(BaseHTTPRequestHandler):
|
|
123
|
+
|
|
124
|
+
def log_message(self, format, *args):
|
|
125
|
+
logger.debug(f"A2A: {format % args}")
|
|
126
|
+
|
|
127
|
+
def _send_json(self, data: dict, status: int = 200):
|
|
128
|
+
self.send_response(status)
|
|
129
|
+
self.send_header("Content-Type", "application/json")
|
|
130
|
+
self.send_header("Access-Control-Allow-Origin", "*")
|
|
131
|
+
self.end_headers()
|
|
132
|
+
self.wfile.write(json.dumps(data, ensure_ascii=False, default=str).encode("utf-8"))
|
|
133
|
+
|
|
134
|
+
def _read_body(self) -> dict:
|
|
135
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
136
|
+
if length == 0:
|
|
137
|
+
return {}
|
|
138
|
+
body = self.rfile.read(length)
|
|
139
|
+
try:
|
|
140
|
+
return json.loads(body.decode("utf-8"))
|
|
141
|
+
except Exception:
|
|
142
|
+
return {}
|
|
143
|
+
|
|
144
|
+
def do_GET(self):
|
|
145
|
+
if not server.check_auth(dict(self.headers)):
|
|
146
|
+
return self._send_json({"error": "unauthorized"}, 401)
|
|
147
|
+
|
|
148
|
+
path = urlparse(self.path).path
|
|
149
|
+
|
|
150
|
+
# GET /a2a/tasks
|
|
151
|
+
if path == "/a2a/tasks":
|
|
152
|
+
tasks = server.list_tasks()
|
|
153
|
+
self._send_json({
|
|
154
|
+
"tasks": [t.to_dict() for t in tasks],
|
|
155
|
+
"count": len(tasks),
|
|
156
|
+
})
|
|
157
|
+
return
|
|
158
|
+
|
|
159
|
+
# GET /a2a/tasks/<id>
|
|
160
|
+
m = re.match(r"^/a2a/tasks/([a-f0-9]+)$", path)
|
|
161
|
+
if m:
|
|
162
|
+
task = server.get_task(m.group(1))
|
|
163
|
+
if task:
|
|
164
|
+
self._send_json(task.to_dict())
|
|
165
|
+
else:
|
|
166
|
+
self._send_json({"error": "task not found"}, 404)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
self._send_json({"error": "not found"}, 404)
|
|
170
|
+
|
|
171
|
+
def do_POST(self):
|
|
172
|
+
if not server.check_auth(dict(self.headers)):
|
|
173
|
+
return self._send_json({"error": "unauthorized"}, 401)
|
|
174
|
+
|
|
175
|
+
path = urlparse(self.path).path
|
|
176
|
+
body = self._read_body()
|
|
177
|
+
|
|
178
|
+
# POST /a2a/tasks
|
|
179
|
+
if path == "/a2a/tasks":
|
|
180
|
+
prompt = body.get("prompt", "")
|
|
181
|
+
if not prompt:
|
|
182
|
+
return self._send_json({"error": "prompt is required"}, 400)
|
|
183
|
+
task = server.create_task(prompt, metadata=body.get("metadata", {}))
|
|
184
|
+
self._send_json(task.to_dict(), 201)
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
# POST /a2a/tasks/<id>/cancel
|
|
188
|
+
m = re.match(r"^/a2a/tasks/([a-f0-9]+)/cancel$", path)
|
|
189
|
+
if m:
|
|
190
|
+
success = server.cancel_task(m.group(1))
|
|
191
|
+
if success:
|
|
192
|
+
self._send_json({"ok": True, "action": "cancelled"})
|
|
193
|
+
else:
|
|
194
|
+
self._send_json({"error": "task not found or already terminal"}, 404)
|
|
195
|
+
return
|
|
196
|
+
|
|
197
|
+
self._send_json({"error": "not found"}, 404)
|
|
198
|
+
|
|
199
|
+
return A2AHandler
|