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.
Files changed (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +335 -0
  3. package/adelie/.python-version +1 -0
  4. package/adelie/README.md +0 -0
  5. package/adelie/__init__.py +1 -0
  6. package/adelie/a2a/__init__.py +16 -0
  7. package/adelie/a2a/persistence.py +83 -0
  8. package/adelie/a2a/server.py +199 -0
  9. package/adelie/a2a/types.py +90 -0
  10. package/adelie/agents/__init__.py +1 -0
  11. package/adelie/agents/analyst_ai.py +247 -0
  12. package/adelie/agents/coder_ai.py +294 -0
  13. package/adelie/agents/coder_manager.py +349 -0
  14. package/adelie/agents/expert_ai.py +470 -0
  15. package/adelie/agents/inform_ai.py +138 -0
  16. package/adelie/agents/monitor_ai.py +241 -0
  17. package/adelie/agents/research_ai.py +224 -0
  18. package/adelie/agents/reviewer_ai.py +165 -0
  19. package/adelie/agents/runner_ai.py +527 -0
  20. package/adelie/agents/scanner_ai.py +380 -0
  21. package/adelie/agents/tester_ai.py +361 -0
  22. package/adelie/agents/writer_ai.py +286 -0
  23. package/adelie/browser_search.py +417 -0
  24. package/adelie/channels/__init__.py +22 -0
  25. package/adelie/channels/base.py +144 -0
  26. package/adelie/channels/discord.py +78 -0
  27. package/adelie/channels/router.py +150 -0
  28. package/adelie/channels/slack.py +74 -0
  29. package/adelie/checkpoint.py +291 -0
  30. package/adelie/cli.py +1673 -0
  31. package/adelie/command_loader.py +137 -0
  32. package/adelie/config.py +88 -0
  33. package/adelie/context_compactor.py +360 -0
  34. package/adelie/context_engine.py +445 -0
  35. package/adelie/env_strategy.py +511 -0
  36. package/adelie/feedback_queue.py +159 -0
  37. package/adelie/gateway.py +276 -0
  38. package/adelie/git_ops.py +170 -0
  39. package/adelie/hooks.py +236 -0
  40. package/adelie/i18n.py +122 -0
  41. package/adelie/integrations/__init__.py +1 -0
  42. package/adelie/integrations/telegram_bot.py +471 -0
  43. package/adelie/interactive.py +467 -0
  44. package/adelie/kb/__init__.py +1 -0
  45. package/adelie/kb/embedding_store.py +249 -0
  46. package/adelie/kb/retriever.py +313 -0
  47. package/adelie/llm_client.py +536 -0
  48. package/adelie/log_rotation.py +67 -0
  49. package/adelie/loop_detector.py +554 -0
  50. package/adelie/loop_manual.md +141 -0
  51. package/adelie/main.py +6 -0
  52. package/adelie/mcp_client.py +374 -0
  53. package/adelie/mcp_manager.py +238 -0
  54. package/adelie/metrics.py +307 -0
  55. package/adelie/orchestrator.py +1486 -0
  56. package/adelie/phases.py +300 -0
  57. package/adelie/plan_mode.py +245 -0
  58. package/adelie/process_supervisor.py +351 -0
  59. package/adelie/project_context.py +196 -0
  60. package/adelie/prompt_loader.py +175 -0
  61. package/adelie/prompts/coder.md +30 -0
  62. package/adelie/prompts/expert.md +104 -0
  63. package/adelie/prompts/reviewer.md +32 -0
  64. package/adelie/pyproject.toml +15 -0
  65. package/adelie/registry.py +88 -0
  66. package/adelie/rules_loader.py +112 -0
  67. package/adelie/sandbox.py +388 -0
  68. package/adelie/scheduler.py +265 -0
  69. package/adelie/skill_manager.py +422 -0
  70. package/adelie/spec_chunker.py +241 -0
  71. package/adelie/spec_loader.py +384 -0
  72. package/adelie/tool_registry.py +369 -0
  73. package/adelie/ui_logger.py +337 -0
  74. package/adelie/utils/__init__.py +0 -0
  75. package/adelie/utils/dep_sync.py +193 -0
  76. package/adelie/utils/import_checker.py +279 -0
  77. package/adelie/web_search.py +239 -0
  78. package/bin/adelie.js +82 -0
  79. package/package.json +47 -0
  80. package/requirements.txt +20 -0
  81. 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
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