devagent-ai 0.3.1__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 ADDED
@@ -0,0 +1,5 @@
1
+ """Compatibility imports for the pre-0.2 package layout."""
2
+
3
+ from devagent import __version__
4
+
5
+ __all__ = ["__version__"]
agent/llm.py ADDED
@@ -0,0 +1,6 @@
1
+ """Compatibility exports for provider abstractions."""
2
+
3
+ from devagent.providers import ModelProvider as LLMClient
4
+ from devagent.providers import ProviderError as LLMError
5
+
6
+ __all__ = ["LLMClient", "LLMError"]
agent/loop.py ADDED
@@ -0,0 +1,16 @@
1
+ """Compatibility wrapper around the deterministic DevAgent orchestrator."""
2
+
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ from devagent.config import ProviderConfig, load_config
7
+ from devagent.orchestrator import run_devagent
8
+ from devagent.providers import create_provider
9
+
10
+
11
+ def run_bugfix_loop(repo_path: str, task: str, max_steps: int = 8, provider: Optional[str] = None) -> str:
12
+ del max_steps # The deterministic lifecycle owns bounded correction budgets.
13
+ configured = load_config()
14
+ selected = ProviderConfig(provider or configured.provider, configured.model, configured.base_url, configured.api_key_env, configured.timeout_seconds)
15
+ _, report = run_devagent(Path(repo_path), task, create_provider(selected))
16
+ return report
agent/memory.py ADDED
@@ -0,0 +1,5 @@
1
+ """Compatibility export for repository memory."""
2
+
3
+ from devagent.memory import RepositoryMemory as AgentMemory
4
+
5
+ __all__ = ["AgentMemory"]
agent/prompts.py ADDED
@@ -0,0 +1,3 @@
1
+ """Prompts now use strict state-specific schemas in :mod:`devagent.orchestrator`."""
2
+
3
+ SYSTEM_PROMPT = "DevAgent uses deterministic state-specific JSON schemas."
agent/tools.py ADDED
@@ -0,0 +1,6 @@
1
+ """Compatibility exports for workspace safety types."""
2
+
3
+ from devagent.safety import SafetyError as ToolError
4
+ from devagent.workspace import Workspace as WorkspaceTools
5
+
6
+ __all__ = ["ToolError", "WorkspaceTools"]
devagent/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """DevAgent: evidence-driven local software engineering automation."""
2
+
3
+ __version__ = "0.3.1"
devagent/__main__.py ADDED
@@ -0,0 +1,4 @@
1
+ from devagent.cli import main
2
+
3
+ raise SystemExit(main())
4
+
devagent/artifacts.py ADDED
@@ -0,0 +1,46 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import shutil
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ from secrets import token_hex
8
+ from typing import Any
9
+
10
+ from devagent.models import jsonable
11
+
12
+
13
+ def new_run_id() -> str:
14
+ return datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%SZ-") + token_hex(3)
15
+
16
+
17
+ class RunArtifacts:
18
+ def __init__(self, repository_root: Path | str, run_id: str | None = None) -> None:
19
+ self.repository_root = Path(repository_root).resolve()
20
+ self.run_id = run_id or new_run_id()
21
+ self.root = self.repository_root / ".devagent" / "runs" / self.run_id
22
+ self.backups = self.root / "backups"
23
+ self.backups.mkdir(parents=True, exist_ok=False)
24
+ self.observations = self.root / "observations.jsonl"
25
+ self.verification = self.root / "verification.json"
26
+ self.report = self.root / "report.json"
27
+
28
+ def write_json(self, name: str, value: Any) -> None:
29
+ target = self.root / name
30
+ target.write_text(json.dumps(jsonable(value), indent=2, sort_keys=True) + "\n", encoding="utf-8")
31
+
32
+ def record(self, event: str, **data: Any) -> None:
33
+ entry = {"timestamp": datetime.now(timezone.utc).isoformat(), "event": event, **jsonable(data)}
34
+ with self.observations.open("a", encoding="utf-8") as handle:
35
+ handle.write(json.dumps(entry, sort_keys=True) + "\n")
36
+
37
+ def backup(self, target: Path, *, relative_to: Path | None = None) -> Path | None:
38
+ if not target.exists():
39
+ return None
40
+ relative = target.resolve().relative_to((relative_to or self.repository_root).resolve())
41
+ destination = self.backups / relative
42
+ destination.parent.mkdir(parents=True, exist_ok=True)
43
+ if not destination.exists():
44
+ shutil.copy2(target, destination)
45
+ self.record("backup_created", path=relative.as_posix(), backup=str(destination))
46
+ return destination
devagent/cli.py ADDED
@@ -0,0 +1,164 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import json
6
+ import os
7
+ import shutil
8
+ import sys
9
+ from pathlib import Path
10
+ from typing import Sequence
11
+
12
+ from devagent import __version__
13
+ from devagent.config import ProviderConfig, config_path, load_config, save_config
14
+ from devagent.models import Outcome
15
+ from devagent.orchestrator import run_devagent
16
+ from devagent.providers import ProviderError, create_provider
17
+ from devagent.safety import is_secret_path
18
+
19
+
20
+ def _top_parser() -> argparse.ArgumentParser:
21
+ parser = argparse.ArgumentParser(prog="devagent", description="Evidence-driven local software engineering agent")
22
+ parser.add_argument("--version", action="version", version=f"DevAgent {__version__}")
23
+ parser.add_argument("task", nargs="?", help="Engineering requirement; reads stdin or prompts when omitted")
24
+ parser.add_argument("--repo", "-r", type=Path, default=Path.cwd(), help="Target repository (default: current directory)")
25
+ parser.add_argument("--input", "-i", type=Path, help="Read the requirement/error context from a file")
26
+ parser.add_argument("--provider", choices=("openai", "anthropic", "claude", "xai", "grok", "compatible"))
27
+ parser.add_argument("--model")
28
+ parser.add_argument("--base-url")
29
+ parser.add_argument("--verbose", action="store_true", help="Show state transitions")
30
+ parser.add_argument("--no-isolation", action="store_true", help="Work in place instead of creating a local detached worktree")
31
+ return parser
32
+
33
+
34
+ def _setup_parser() -> argparse.ArgumentParser:
35
+ parser = argparse.ArgumentParser(prog="devagent setup", description="Configure the default model provider")
36
+ parser.add_argument("--provider", choices=("openai", "anthropic", "xai", "compatible"), default="openai")
37
+ parser.add_argument("--model")
38
+ parser.add_argument("--base-url")
39
+ parser.add_argument("--api-key-env")
40
+ return parser
41
+
42
+
43
+ def _defaults(provider: str) -> tuple[str, str]:
44
+ provider = {"claude": "anthropic", "grok": "xai"}.get(provider, provider)
45
+ return {
46
+ "openai": ("gpt-5", "OPENAI_API_KEY"),
47
+ "anthropic": ("claude-sonnet-4-20250514", "ANTHROPIC_API_KEY"),
48
+ "xai": ("grok-4", "XAI_API_KEY"),
49
+ "compatible": ("local-model", "DEVAGENT_API_KEY"),
50
+ }[provider]
51
+
52
+
53
+ def _setup(argv: Sequence[str]) -> int:
54
+ args = _setup_parser().parse_args(argv)
55
+ model, key_env = _defaults(args.provider)
56
+ target = save_config(ProviderConfig(args.provider, args.model or model, args.base_url, args.api_key_env or key_env))
57
+ print(f"Configured {args.provider} in {target}")
58
+ print(f"API keys are not stored; set {args.api_key_env or key_env} in your environment.")
59
+ return 0
60
+
61
+
62
+ def _doctor() -> int:
63
+ config = load_config()
64
+ checks = {
65
+ "git": shutil.which("git") is not None,
66
+ "configuration": config_path().is_file(),
67
+ "provider_sdk": importlib.util.find_spec("anthropic" if config.provider in {"anthropic", "claude"} else "openai") is not None,
68
+ "api_key": config.provider == "compatible" or not config.api_key_env or bool(os.getenv(config.api_key_env)),
69
+ }
70
+ print("DEVAGENT DOCTOR")
71
+ for name, okay in checks.items():
72
+ print(f"{'OK' if okay else 'WARN'} {name}")
73
+ if not checks["configuration"]:
74
+ print("Run `devagent setup` before a cloud-provider engineering run.")
75
+ return 0
76
+
77
+
78
+ def _status(repo: Path) -> int:
79
+ runs = repo.resolve() / ".devagent" / "runs"
80
+ reports = sorted(runs.glob("*/report.json")) if runs.is_dir() else []
81
+ if not reports:
82
+ print("No DevAgent runs found for this repository.")
83
+ return 0
84
+ latest = reports[-1]
85
+ try:
86
+ data = json.loads(latest.read_text(encoding="utf-8"))
87
+ except (OSError, json.JSONDecodeError):
88
+ print(f"Latest run report is unreadable: {latest}")
89
+ return 1
90
+ print(f"Run: {data.get('run_id', latest.parent.name)}")
91
+ print(f"Status: {data.get('outcome', 'UNKNOWN')}")
92
+ print(f"Report: {latest}")
93
+ return 0
94
+
95
+
96
+ def _requirement(args: argparse.Namespace) -> str:
97
+ if args.input:
98
+ if is_secret_path(args.input):
99
+ raise ValueError("Refusing to read a sensitive input file")
100
+ if not args.input.is_file() or args.input.stat().st_size > 2_000_000:
101
+ raise ValueError("Input must be a bounded local text file")
102
+ return args.input.read_text(encoding="utf-8").strip()
103
+ if args.task:
104
+ return args.task.strip()
105
+ if not sys.stdin.isatty():
106
+ return sys.stdin.read(2_000_001).strip()
107
+ return input("Engineering requirement: ").strip()
108
+
109
+
110
+ def _run(argv: Sequence[str]) -> int:
111
+ args = _top_parser().parse_args(argv)
112
+ try:
113
+ requirement = _requirement(args)
114
+ if not requirement:
115
+ raise ValueError("Engineering requirement cannot be empty")
116
+ if not args.repo.resolve().is_dir():
117
+ raise ValueError(f"Repository does not exist: {args.repo}")
118
+ configured = load_config()
119
+ selected_provider = args.provider or configured.provider
120
+ if args.provider and args.provider != configured.provider:
121
+ default_model, default_key_env = _defaults(args.provider)
122
+ else:
123
+ default_model, default_key_env = configured.model, configured.api_key_env
124
+ config = ProviderConfig(
125
+ provider=selected_provider,
126
+ model=args.model or default_model,
127
+ base_url=args.base_url or configured.base_url,
128
+ api_key_env=default_key_env,
129
+ timeout_seconds=configured.timeout_seconds,
130
+ )
131
+ print("DevAgent is working...")
132
+ from devagent.orchestrator import DevAgent
133
+ from devagent.report import render_report
134
+
135
+ result = DevAgent(
136
+ create_provider(config),
137
+ isolate=not args.no_isolation,
138
+ verbose=args.verbose,
139
+ status=print,
140
+ ).run(args.repo, requirement)
141
+ report = render_report(result)
142
+ print(report)
143
+ return {Outcome.VERIFIED: 0, Outcome.PARTIALLY_VERIFIED: 2, Outcome.BLOCKED: 1}[result.outcome]
144
+ except (ValueError, OSError, ProviderError) as exc:
145
+ print(f"DevAgent failed: {exc}", file=sys.stderr)
146
+ return 1
147
+
148
+
149
+ def main(argv: Sequence[str] | None = None) -> int:
150
+ arguments = list(sys.argv[1:] if argv is None else argv)
151
+ if arguments and arguments[0] == "setup":
152
+ return _setup(arguments[1:])
153
+ if arguments and arguments[0] == "doctor":
154
+ argparse.ArgumentParser(prog="devagent doctor", description="Check local DevAgent readiness").parse_args(arguments[1:])
155
+ return _doctor()
156
+ if arguments and arguments[0] == "status":
157
+ parser = argparse.ArgumentParser(prog="devagent status", description="Show the latest local run")
158
+ parser.add_argument("--repo", "-r", type=Path, default=Path.cwd())
159
+ return _status(parser.parse_args(arguments[1:]).repo)
160
+ return _run(arguments)
161
+
162
+
163
+ if __name__ == "__main__": # pragma: no cover
164
+ raise SystemExit(main())
devagent/config.py ADDED
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import json
5
+ from dataclasses import dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ try:
10
+ import tomllib
11
+ except ModuleNotFoundError: # pragma: no cover - Python 3.10 only
12
+ import tomli as tomllib # type: ignore[no-redef]
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class ProviderConfig:
17
+ provider: str = "openai"
18
+ model: str = "gpt-5"
19
+ base_url: str | None = None
20
+ api_key_env: str = "OPENAI_API_KEY"
21
+ timeout_seconds: int = 120
22
+
23
+
24
+ def config_path() -> Path:
25
+ override = os.getenv("DEVAGENT_CONFIG")
26
+ if override:
27
+ return Path(override).expanduser()
28
+ base = Path(os.getenv("XDG_CONFIG_HOME", Path.home() / ".config"))
29
+ return base / "devagent" / "config.toml"
30
+
31
+
32
+ def load_config(path: Path | None = None) -> ProviderConfig:
33
+ target = path or config_path()
34
+ data: dict[str, Any] = {}
35
+ if target.is_file():
36
+ with target.open("rb") as handle:
37
+ loaded = tomllib.load(handle)
38
+ data = loaded.get("provider", loaded)
39
+ provider = os.getenv("DEVAGENT_PROVIDER", str(data.get("name", "openai"))).lower()
40
+ defaults = {
41
+ "openai": ("gpt-5", "OPENAI_API_KEY"),
42
+ "anthropic": ("claude-sonnet-4-20250514", "ANTHROPIC_API_KEY"),
43
+ "claude": ("claude-sonnet-4-20250514", "ANTHROPIC_API_KEY"),
44
+ "xai": ("grok-4", "XAI_API_KEY"),
45
+ "grok": ("grok-4", "XAI_API_KEY"),
46
+ "compatible": ("local-model", "DEVAGENT_API_KEY"),
47
+ "fake": ("fake", ""),
48
+ }
49
+ default_model, default_env = defaults.get(provider, defaults["compatible"])
50
+ return ProviderConfig(
51
+ provider=provider,
52
+ model=os.getenv("DEVAGENT_MODEL", str(data.get("model", default_model))),
53
+ base_url=os.getenv("DEVAGENT_BASE_URL") or data.get("base_url"),
54
+ api_key_env=str(data.get("api_key_env", default_env)),
55
+ timeout_seconds=int(data.get("timeout_seconds", 120)),
56
+ )
57
+
58
+
59
+ def save_config(config: ProviderConfig, path: Path | None = None) -> Path:
60
+ target = path or config_path()
61
+ target.parent.mkdir(parents=True, exist_ok=True)
62
+ lines = [
63
+ "[provider]",
64
+ f"name = {json.dumps(config.provider)}",
65
+ f"model = {json.dumps(config.model)}",
66
+ f"api_key_env = {json.dumps(config.api_key_env)}",
67
+ f"timeout_seconds = {config.timeout_seconds}",
68
+ ]
69
+ if config.base_url:
70
+ lines.append(f"base_url = {json.dumps(config.base_url)}")
71
+ target.write_text("\n".join(lines) + "\n", encoding="utf-8")
72
+ return target