devpilot-agentic-cli 1.0.0__py3-none-any.whl
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.
- agent/__init__.py +1 -0
- agent/a2a_client.py +94 -0
- agent/a2a_server.py +148 -0
- agent/cli.py +233 -0
- agent/config.py +232 -0
- agent/context.py +182 -0
- agent/history.py +172 -0
- agent/loop.py +102 -0
- agent/mcp_client.py +104 -0
- agent/providers/__init__.py +4 -0
- agent/providers/anthropic_provider.py +169 -0
- agent/providers/base.py +148 -0
- agent/providers/factory.py +35 -0
- agent/providers/openai_provider.py +194 -0
- agent/providers/system_prompt.py +132 -0
- agent/setup_wizard.py +309 -0
- agent/tools/__init__.py +15 -0
- agent/tools/a2a.py +56 -0
- agent/tools/base.py +52 -0
- agent/tools/diagram.py +131 -0
- agent/tools/doc_gen.py +163 -0
- agent/tools/fs.py +411 -0
- agent/tools/git_ops.py +145 -0
- agent/tools/registry.py +219 -0
- agent/tools/search_code.py +120 -0
- agent/tools/shell.py +118 -0
- agent/tools/web_search.py +105 -0
- agent/tui/__init__.py +3 -0
- agent/tui/app.py +557 -0
- agent/ui.py +263 -0
- devpilot_agentic_cli-1.0.0.dist-info/METADATA +288 -0
- devpilot_agentic_cli-1.0.0.dist-info/RECORD +35 -0
- devpilot_agentic_cli-1.0.0.dist-info/WHEEL +5 -0
- devpilot_agentic_cli-1.0.0.dist-info/entry_points.txt +2 -0
- devpilot_agentic_cli-1.0.0.dist-info/top_level.txt +1 -0
agent/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# DevPilot Agent Package
|
agent/a2a_client.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/a2a_client.py
|
|
3
|
+
───────────────────
|
|
4
|
+
A2A client implementation. Discovers peer agents via their AgentCard
|
|
5
|
+
and delegates tasks to them using HTTP SSE.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import json
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
|
|
13
|
+
from agent.tools import ToolResult
|
|
14
|
+
from agent.ui import UI
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
async def _fetch_agent_card(peer_url: str, token: str | None) -> dict[str, Any]:
|
|
18
|
+
headers = {}
|
|
19
|
+
if token:
|
|
20
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
21
|
+
|
|
22
|
+
card_url = f"{peer_url.rstrip('/')}/.well-known/agent.json"
|
|
23
|
+
async with httpx.AsyncClient() as client:
|
|
24
|
+
response = await client.get(card_url, headers=headers)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
return response.json()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def delegate_task_to_peer(peer_url: str, prompt: str, token: str | None = None) -> ToolResult:
|
|
30
|
+
"""
|
|
31
|
+
Delegate a task to an A2A peer agent.
|
|
32
|
+
1. Fetch AgentCard to find the /tasks/send endpoint.
|
|
33
|
+
2. POST the task.
|
|
34
|
+
3. Stream SSE events to get the artifact.
|
|
35
|
+
"""
|
|
36
|
+
try:
|
|
37
|
+
card = await _fetch_agent_card(peer_url, token)
|
|
38
|
+
except Exception as e:
|
|
39
|
+
return ToolResult(f"Failed to fetch AgentCard from {peer_url}: {e}", is_error=True)
|
|
40
|
+
|
|
41
|
+
endpoints = card.get("endpoints", {})
|
|
42
|
+
send_url = endpoints.get("tasks")
|
|
43
|
+
if not send_url:
|
|
44
|
+
return ToolResult(f"Peer AgentCard missing 'tasks' endpoint. Card: {card}", is_error=True)
|
|
45
|
+
|
|
46
|
+
headers = {}
|
|
47
|
+
if token:
|
|
48
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
async with httpx.AsyncClient() as client:
|
|
52
|
+
# Send task
|
|
53
|
+
response = await client.post(send_url, json={"prompt": prompt}, headers=headers)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
data = response.json()
|
|
56
|
+
task_id = data.get("task_id")
|
|
57
|
+
|
|
58
|
+
if not task_id:
|
|
59
|
+
return ToolResult("Peer did not return a task_id.", is_error=True)
|
|
60
|
+
|
|
61
|
+
base = send_url[: send_url.rfind("/send")]
|
|
62
|
+
stream_url = f"{base}/{task_id}/stream"
|
|
63
|
+
|
|
64
|
+
# Read SSE
|
|
65
|
+
UI.print_info(f"Delegated task {task_id} to {peer_url}. Waiting for results...")
|
|
66
|
+
|
|
67
|
+
final_artifact = ""
|
|
68
|
+
status = "working"
|
|
69
|
+
|
|
70
|
+
async with client.stream("GET", stream_url, headers=headers) as stream_resp:
|
|
71
|
+
stream_resp.raise_for_status()
|
|
72
|
+
async for line in stream_resp.aiter_lines():
|
|
73
|
+
line = line.strip()
|
|
74
|
+
if not line:
|
|
75
|
+
continue
|
|
76
|
+
if line.startswith("event: "):
|
|
77
|
+
current_event = line[7:]
|
|
78
|
+
elif line.startswith("data: "):
|
|
79
|
+
data_payload = line[6:]
|
|
80
|
+
if "current_event" in locals():
|
|
81
|
+
if current_event == "artifact":
|
|
82
|
+
final_artifact += data_payload + "\n"
|
|
83
|
+
elif current_event == "status":
|
|
84
|
+
status = data_payload
|
|
85
|
+
|
|
86
|
+
if status == "failed":
|
|
87
|
+
return ToolResult(f"Task failed on peer: {final_artifact}", is_error=True)
|
|
88
|
+
|
|
89
|
+
return ToolResult(f"Task completed successfully by peer. Artifact:\n{final_artifact}", is_error=False)
|
|
90
|
+
|
|
91
|
+
except Exception as e:
|
|
92
|
+
return ToolResult(f"A2A connection error: {e}", is_error=True)
|
|
93
|
+
|
|
94
|
+
|
agent/a2a_server.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/a2a_server.py
|
|
3
|
+
───────────────────
|
|
4
|
+
FastAPI server implementing the Agent-to-Agent (A2A) protocol.
|
|
5
|
+
Handles incoming task delegation requests and streams results via SSE.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import asyncio
|
|
9
|
+
import uuid
|
|
10
|
+
from typing import Any, AsyncGenerator
|
|
11
|
+
|
|
12
|
+
from fastapi import Depends, FastAPI, HTTPException, Request
|
|
13
|
+
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
from sse_starlette.sse import EventSourceResponse
|
|
16
|
+
|
|
17
|
+
from agent.config import Config
|
|
18
|
+
from agent.history import HistoryManager
|
|
19
|
+
from agent.loop import run_agent_loop
|
|
20
|
+
from agent.providers.factory import create_provider
|
|
21
|
+
from agent.tools import ToolRegistry
|
|
22
|
+
|
|
23
|
+
app = FastAPI(title="DevPilot A2A Server")
|
|
24
|
+
security = HTTPBearer(auto_error=False)
|
|
25
|
+
|
|
26
|
+
# Global task queues for SSE streaming
|
|
27
|
+
# task_id -> asyncio.Queue
|
|
28
|
+
_task_queues: dict[str, asyncio.Queue] = {}
|
|
29
|
+
|
|
30
|
+
# We'll store config, registry, etc in app.state during startup
|
|
31
|
+
# app.state.config
|
|
32
|
+
# app.state.registry
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TaskRequest(BaseModel):
|
|
36
|
+
prompt: str
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def verify_token(credentials: HTTPAuthorizationCredentials | None = Depends(security)) -> None:
|
|
40
|
+
config: Config = app.state.config
|
|
41
|
+
if config.a2a_token:
|
|
42
|
+
if not credentials or credentials.credentials != config.a2a_token:
|
|
43
|
+
raise HTTPException(status_code=401, detail="Invalid or missing bearer token")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.get("/.well-known/agent.json")
|
|
47
|
+
async def get_agent_card() -> dict[str, Any]:
|
|
48
|
+
"""Return the A2A AgentCard describing this agent."""
|
|
49
|
+
port = app.state.config.a2a_port
|
|
50
|
+
return {
|
|
51
|
+
"name": "DevPilot",
|
|
52
|
+
"description": "AI-Powered Terminal Coding Agent",
|
|
53
|
+
"protocols": ["A2A v0.2"],
|
|
54
|
+
"endpoints": {
|
|
55
|
+
"tasks": f"http://localhost:{port}/tasks/send"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
async def background_task_runner(task_id: str, prompt: str) -> None:
|
|
61
|
+
"""Executes the agentic loop in the background and pushes SSE events."""
|
|
62
|
+
queue = _task_queues[task_id]
|
|
63
|
+
await queue.put({"event": "status", "data": "working"})
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
config: Config = app.state.config
|
|
67
|
+
registry: ToolRegistry = app.state.registry
|
|
68
|
+
|
|
69
|
+
# Instantiate a fresh provider and history for this task
|
|
70
|
+
provider = create_provider(config)
|
|
71
|
+
history = HistoryManager()
|
|
72
|
+
|
|
73
|
+
# Add the prompt
|
|
74
|
+
history.append(provider.make_user_message(prompt))
|
|
75
|
+
|
|
76
|
+
# Run loop. We force no_confirm=True for background tasks to avoid hanging.
|
|
77
|
+
# Use dataclasses.replace() so we don't duplicate every field.
|
|
78
|
+
import dataclasses
|
|
79
|
+
task_config = dataclasses.replace(config, no_confirm=True)
|
|
80
|
+
|
|
81
|
+
await run_agent_loop(
|
|
82
|
+
provider=provider,
|
|
83
|
+
registry=registry,
|
|
84
|
+
history=history,
|
|
85
|
+
config=task_config,
|
|
86
|
+
max_iterations=task_config.max_iterations,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# After loop finishes, the last message should be the assistant's final output
|
|
90
|
+
messages = history.get_messages()
|
|
91
|
+
if messages and messages[-1].get("role") == "assistant":
|
|
92
|
+
content = messages[-1].get("content", "")
|
|
93
|
+
if isinstance(content, list):
|
|
94
|
+
# Canonical format: list of typed blocks — extract text blocks only
|
|
95
|
+
final_text = " ".join(
|
|
96
|
+
b.get("text", "")
|
|
97
|
+
for b in content
|
|
98
|
+
if isinstance(b, dict) and b.get("type") == "text"
|
|
99
|
+
).strip() or "Task completed."
|
|
100
|
+
else:
|
|
101
|
+
final_text = str(content)
|
|
102
|
+
else:
|
|
103
|
+
final_text = "Task completed, but no final response was generated."
|
|
104
|
+
|
|
105
|
+
await queue.put({"event": "artifact", "data": final_text})
|
|
106
|
+
await queue.put({"event": "status", "data": "completed"})
|
|
107
|
+
|
|
108
|
+
except Exception as e:
|
|
109
|
+
await queue.put({"event": "artifact", "data": f"Error: {e}"})
|
|
110
|
+
await queue.put({"event": "status", "data": "failed"})
|
|
111
|
+
finally:
|
|
112
|
+
await queue.put(None) # Sentinel to close stream
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@app.post("/tasks/send", dependencies=[Depends(verify_token)])
|
|
116
|
+
async def create_task(request: TaskRequest) -> dict[str, str]:
|
|
117
|
+
"""Receive a task, spin up background executor, and return task_id."""
|
|
118
|
+
task_id = str(uuid.uuid4())
|
|
119
|
+
_task_queues[task_id] = asyncio.Queue()
|
|
120
|
+
|
|
121
|
+
# Spin up background task
|
|
122
|
+
asyncio.create_task(background_task_runner(task_id, request.prompt))
|
|
123
|
+
|
|
124
|
+
return {"task_id": task_id}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.get("/tasks/{task_id}/stream", dependencies=[Depends(verify_token)])
|
|
128
|
+
async def stream_task(task_id: str, request: Request) -> EventSourceResponse:
|
|
129
|
+
"""Stream SSE events for a specific task."""
|
|
130
|
+
if task_id not in _task_queues:
|
|
131
|
+
raise HTTPException(status_code=404, detail="Task not found")
|
|
132
|
+
|
|
133
|
+
queue = _task_queues[task_id]
|
|
134
|
+
|
|
135
|
+
async def event_generator() -> AsyncGenerator[dict[str, str], None]:
|
|
136
|
+
while True:
|
|
137
|
+
if await request.is_disconnected():
|
|
138
|
+
break
|
|
139
|
+
|
|
140
|
+
event = await queue.get()
|
|
141
|
+
if event is None:
|
|
142
|
+
# Task finished
|
|
143
|
+
del _task_queues[task_id]
|
|
144
|
+
break
|
|
145
|
+
|
|
146
|
+
yield event
|
|
147
|
+
|
|
148
|
+
return EventSourceResponse(event_generator())
|
agent/cli.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
agent/cli.py
|
|
3
|
+
─────────────
|
|
4
|
+
CLI entry point for DevPilot.
|
|
5
|
+
Parses arguments, runs first-run setup wizard if needed,
|
|
6
|
+
initializes components, and starts the interactive TUI or CI loop.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import asyncio
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from agent.a2a_server import app as a2a_app
|
|
19
|
+
from agent.config import Config, ConfigError
|
|
20
|
+
from agent.history import HistoryManager
|
|
21
|
+
from agent.loop import run_agent_loop
|
|
22
|
+
from agent.mcp_client import MCPManager
|
|
23
|
+
from agent.providers.factory import create_provider
|
|
24
|
+
from agent.tools import ToolRegistry
|
|
25
|
+
from agent.ui import UI
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def parse_args() -> argparse.Namespace:
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
prog="devpilot",
|
|
31
|
+
description="DevPilot — Autonomous AI coding agent for your terminal",
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument("--provider", choices=["anthropic", "openai"], help="Model provider")
|
|
34
|
+
parser.add_argument("--model", help="Model name")
|
|
35
|
+
parser.add_argument("--base-url", help="OpenAI-compatible base URL (e.g. Ollama)")
|
|
36
|
+
parser.add_argument("--no-confirm", action="store_true", help="Skip confirmation prompts")
|
|
37
|
+
parser.add_argument("--thinking", action="store_true", help="Enable extended thinking (Claude only)")
|
|
38
|
+
parser.add_argument("--thinking-budget", type=int, help="Extended thinking token budget (default: 10000)")
|
|
39
|
+
parser.add_argument("--no-web-search", action="store_true", help="Disable web search tool")
|
|
40
|
+
parser.add_argument("--no-memory", action="store_true", help="Disable long-term memory")
|
|
41
|
+
parser.add_argument("--workdir", help="Working directory for file operations (default: cwd)")
|
|
42
|
+
parser.add_argument("--task", help="Run a single task and exit (CI mode)")
|
|
43
|
+
parser.add_argument("--resume", help="Resume a previous session by ID")
|
|
44
|
+
parser.add_argument("--a2a-port", type=int, help="A2A server port (default: 8000)")
|
|
45
|
+
parser.add_argument("--no-a2a", action="store_true", help="Disable A2A server")
|
|
46
|
+
parser.add_argument("--setup", action="store_true", help="Re-run the setup wizard")
|
|
47
|
+
return parser.parse_args()
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _apply_cli_overrides(args: argparse.Namespace) -> None:
|
|
51
|
+
"""Push CLI flag values into env vars so Config.load() picks them up."""
|
|
52
|
+
if args.provider:
|
|
53
|
+
os.environ["DEVPILOT_PROVIDER"] = args.provider
|
|
54
|
+
if args.model:
|
|
55
|
+
os.environ["DEVPILOT_MODEL"] = args.model
|
|
56
|
+
if args.base_url:
|
|
57
|
+
os.environ["DEVPILOT_BASE_URL"] = args.base_url
|
|
58
|
+
if args.no_confirm:
|
|
59
|
+
os.environ["DEVPILOT_NO_CONFIRM"] = "true"
|
|
60
|
+
if args.thinking:
|
|
61
|
+
os.environ["DEVPILOT_THINKING"] = "true"
|
|
62
|
+
if args.thinking_budget:
|
|
63
|
+
os.environ["DEVPILOT_THINKING_BUDGET"] = str(args.thinking_budget)
|
|
64
|
+
if args.no_web_search:
|
|
65
|
+
os.environ["DEVPILOT_NO_WEB_SEARCH"] = "true"
|
|
66
|
+
if args.no_memory:
|
|
67
|
+
os.environ["DEVPILOT_NO_MEMORY"] = "true"
|
|
68
|
+
if args.workdir:
|
|
69
|
+
os.environ["DEVPILOT_WORKDIR"] = args.workdir
|
|
70
|
+
if args.a2a_port:
|
|
71
|
+
os.environ["DEVPILOT_A2A_PORT"] = str(args.a2a_port)
|
|
72
|
+
if args.no_a2a:
|
|
73
|
+
os.environ["DEVPILOT_NO_A2A"] = "true"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _ensure_api_key(config: Config, args: argparse.Namespace) -> Config:
|
|
77
|
+
"""
|
|
78
|
+
Check for a valid API key. If missing, run the setup wizard.
|
|
79
|
+
Re-loads config after wizard so the new key is picked up.
|
|
80
|
+
Returns the (possibly reloaded) config.
|
|
81
|
+
"""
|
|
82
|
+
from agent.config import REQUIRED_ENV_KEYS
|
|
83
|
+
key_name = REQUIRED_ENV_KEYS[config.provider]
|
|
84
|
+
|
|
85
|
+
if config.active_api_key:
|
|
86
|
+
return config # Key present — nothing to do
|
|
87
|
+
|
|
88
|
+
# Force re-run wizard if --setup flag passed
|
|
89
|
+
if args.setup or not config.active_api_key:
|
|
90
|
+
from agent.setup_wizard import run_setup_wizard
|
|
91
|
+
success = run_setup_wizard(env_path=Path(".env"))
|
|
92
|
+
if not success:
|
|
93
|
+
# Wizard skipped (CI/no TTY) or failed — print helpful error
|
|
94
|
+
UI.print_error(
|
|
95
|
+
f"Missing API key for provider '{config.provider}'.\n"
|
|
96
|
+
f" Set {key_name} in your environment or .env file.\n"
|
|
97
|
+
f" Run 'devpilot --setup' to configure interactively."
|
|
98
|
+
)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
# Reload config so the new env vars are picked up
|
|
102
|
+
try:
|
|
103
|
+
config = Config.load()
|
|
104
|
+
_apply_cli_overrides(args)
|
|
105
|
+
config = Config.load()
|
|
106
|
+
except ConfigError as e:
|
|
107
|
+
UI.print_error(str(e))
|
|
108
|
+
sys.exit(1)
|
|
109
|
+
|
|
110
|
+
return config
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
async def main_async() -> None:
|
|
114
|
+
args = parse_args()
|
|
115
|
+
_apply_cli_overrides(args)
|
|
116
|
+
|
|
117
|
+
# Load config
|
|
118
|
+
try:
|
|
119
|
+
config = Config.load()
|
|
120
|
+
except ConfigError as e:
|
|
121
|
+
UI.print_error(str(e))
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
# Handle --setup flag (force wizard even if key exists)
|
|
125
|
+
if args.setup:
|
|
126
|
+
from agent.setup_wizard import run_setup_wizard
|
|
127
|
+
run_setup_wizard(env_path=Path(".env"))
|
|
128
|
+
try:
|
|
129
|
+
config = Config.load()
|
|
130
|
+
except ConfigError as e:
|
|
131
|
+
UI.print_error(str(e))
|
|
132
|
+
sys.exit(1)
|
|
133
|
+
else:
|
|
134
|
+
# Ensure API key — runs wizard if missing
|
|
135
|
+
config = _ensure_api_key(config, args)
|
|
136
|
+
|
|
137
|
+
# Final key validation (catches invalid key format etc.)
|
|
138
|
+
try:
|
|
139
|
+
config.validate_api_key()
|
|
140
|
+
except ConfigError as e:
|
|
141
|
+
UI.print_error(str(e))
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
|
|
144
|
+
# Wire up provider, context, registry
|
|
145
|
+
provider = create_provider(config)
|
|
146
|
+
|
|
147
|
+
from agent.context import RepoContext
|
|
148
|
+
repo_context = RepoContext(config.workdir)
|
|
149
|
+
registry = ToolRegistry(config, _context=repo_context)
|
|
150
|
+
|
|
151
|
+
# MCP
|
|
152
|
+
mcp_config_path = Path("mcp_servers.json")
|
|
153
|
+
if not mcp_config_path.exists():
|
|
154
|
+
mcp_config_path = Path(__file__).parent.parent / "mcp_servers.json"
|
|
155
|
+
mcp_manager = MCPManager(mcp_config_path)
|
|
156
|
+
await mcp_manager.connect_all(registry)
|
|
157
|
+
|
|
158
|
+
# A2A server
|
|
159
|
+
a2a_app.state.config = config
|
|
160
|
+
a2a_app.state.registry = registry
|
|
161
|
+
|
|
162
|
+
if config.a2a_enabled:
|
|
163
|
+
import uvicorn
|
|
164
|
+
a2a_cfg = uvicorn.Config(
|
|
165
|
+
app=a2a_app,
|
|
166
|
+
host="0.0.0.0",
|
|
167
|
+
port=config.a2a_port,
|
|
168
|
+
log_level="error",
|
|
169
|
+
)
|
|
170
|
+
a2a_server = uvicorn.Server(a2a_cfg)
|
|
171
|
+
a2a_task = asyncio.create_task(a2a_server.serve())
|
|
172
|
+
else:
|
|
173
|
+
a2a_server = None
|
|
174
|
+
a2a_task = None
|
|
175
|
+
|
|
176
|
+
# History / session
|
|
177
|
+
history = HistoryManager()
|
|
178
|
+
session_id = args.resume or datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
179
|
+
session_file = config.sessions_dir / f"{session_id}.json"
|
|
180
|
+
|
|
181
|
+
if args.resume:
|
|
182
|
+
if session_file.exists():
|
|
183
|
+
history.load(session_file)
|
|
184
|
+
if args.task:
|
|
185
|
+
UI.print_info(f"Resumed session: {session_id}")
|
|
186
|
+
else:
|
|
187
|
+
UI.print_error(f"Session '{session_id}' not found at {session_file}")
|
|
188
|
+
sys.exit(1)
|
|
189
|
+
else:
|
|
190
|
+
if args.task:
|
|
191
|
+
UI.print_info(f"New session: {session_id}")
|
|
192
|
+
|
|
193
|
+
# ── CI / single-task mode ─────────────────────────────────────────────────
|
|
194
|
+
if args.task:
|
|
195
|
+
history.append(provider.make_user_message(args.task))
|
|
196
|
+
await run_agent_loop(
|
|
197
|
+
provider=provider,
|
|
198
|
+
registry=registry,
|
|
199
|
+
history=history,
|
|
200
|
+
config=config,
|
|
201
|
+
max_iterations=config.max_iterations,
|
|
202
|
+
context=repo_context,
|
|
203
|
+
)
|
|
204
|
+
history.save(session_file)
|
|
205
|
+
if a2a_server and a2a_task:
|
|
206
|
+
a2a_server.should_exit = True
|
|
207
|
+
await a2a_task
|
|
208
|
+
await mcp_manager.close()
|
|
209
|
+
sys.exit(0)
|
|
210
|
+
|
|
211
|
+
# ── Interactive TUI mode ──────────────────────────────────────────────────
|
|
212
|
+
from agent.tui.app import DevPilotApp
|
|
213
|
+
app = DevPilotApp(
|
|
214
|
+
provider=provider,
|
|
215
|
+
registry=registry,
|
|
216
|
+
history=history,
|
|
217
|
+
config=config,
|
|
218
|
+
repo_context=repo_context,
|
|
219
|
+
)
|
|
220
|
+
await app.run_async()
|
|
221
|
+
|
|
222
|
+
if a2a_server and a2a_task:
|
|
223
|
+
a2a_server.should_exit = True
|
|
224
|
+
await a2a_task
|
|
225
|
+
await mcp_manager.close()
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def main() -> None:
|
|
229
|
+
asyncio.run(main_async())
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
if __name__ == "__main__":
|
|
233
|
+
main()
|