agentbridge-core 0.1.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.
Files changed (79) hide show
  1. agentbridge/__init__.py +5 -0
  2. agentbridge/__main__.py +3 -0
  3. agentbridge/agents/__init__.py +0 -0
  4. agentbridge/agents/antigravity/__init__.py +0 -0
  5. agentbridge/agents/antigravity/adapter.py +211 -0
  6. agentbridge/agents/base.py +138 -0
  7. agentbridge/agents/capabilities.py +48 -0
  8. agentbridge/agents/claude/__init__.py +0 -0
  9. agentbridge/agents/claude/adapter.py +327 -0
  10. agentbridge/agents/claude/container.py +118 -0
  11. agentbridge/agents/generic_cli/__init__.py +0 -0
  12. agentbridge/agents/generic_cli/adapter.py +104 -0
  13. agentbridge/agents/mock/__init__.py +0 -0
  14. agentbridge/agents/mock/adapter.py +215 -0
  15. agentbridge/agents/prompts.py +86 -0
  16. agentbridge/agents/registry.py +58 -0
  17. agentbridge/cli/__init__.py +0 -0
  18. agentbridge/cli/app.py +67 -0
  19. agentbridge/cli/commands/__init__.py +0 -0
  20. agentbridge/cli/commands/dashboard.py +54 -0
  21. agentbridge/cli/commands/project.py +137 -0
  22. agentbridge/cli/commands/remote.py +49 -0
  23. agentbridge/cli/commands/tasks.py +281 -0
  24. agentbridge/cli/context.py +60 -0
  25. agentbridge/cli/output.py +52 -0
  26. agentbridge/config/__init__.py +0 -0
  27. agentbridge/config/loader.py +132 -0
  28. agentbridge/config/settings.py +351 -0
  29. agentbridge/core/__init__.py +0 -0
  30. agentbridge/core/events.py +49 -0
  31. agentbridge/core/orchestrator.py +970 -0
  32. agentbridge/core/planner.py +152 -0
  33. agentbridge/core/router.py +102 -0
  34. agentbridge/dashboard/__init__.py +0 -0
  35. agentbridge/dashboard/server.py +339 -0
  36. agentbridge/dashboard/static/assets/index-C4LNM7Ks.js +42 -0
  37. agentbridge/dashboard/static/assets/index-CuuZIrox.css +1 -0
  38. agentbridge/dashboard/static/index.html +13 -0
  39. agentbridge/errors.py +66 -0
  40. agentbridge/execution/__init__.py +0 -0
  41. agentbridge/execution/process.py +164 -0
  42. agentbridge/execution/sandbox.py +79 -0
  43. agentbridge/main.py +22 -0
  44. agentbridge/reporting/__init__.py +0 -0
  45. agentbridge/reporting/generator.py +290 -0
  46. agentbridge/security/__init__.py +0 -0
  47. agentbridge/security/paths.py +53 -0
  48. agentbridge/security/permissions.py +137 -0
  49. agentbridge/security/policies.py +59 -0
  50. agentbridge/security/secrets.py +80 -0
  51. agentbridge/storage/__init__.py +0 -0
  52. agentbridge/storage/database.py +211 -0
  53. agentbridge/storage/migrations/__init__.py +0 -0
  54. agentbridge/storage/migrations/env.py +20 -0
  55. agentbridge/storage/migrations/script.py.mako +25 -0
  56. agentbridge/storage/migrations/versions/0001_initial_schema.py +176 -0
  57. agentbridge/storage/migrations/versions/__init__.py +0 -0
  58. agentbridge/storage/repositories.py +237 -0
  59. agentbridge/tasks/__init__.py +0 -0
  60. agentbridge/tasks/dependencies.py +126 -0
  61. agentbridge/tasks/models.py +126 -0
  62. agentbridge/tasks/recovery.py +91 -0
  63. agentbridge/tasks/state_machine.py +54 -0
  64. agentbridge/validation/__init__.py +0 -0
  65. agentbridge/validation/engine.py +155 -0
  66. agentbridge/validation/models.py +52 -0
  67. agentbridge/validation/review.py +78 -0
  68. agentbridge/validation/runners.py +120 -0
  69. agentbridge/workspace/__init__.py +0 -0
  70. agentbridge/workspace/conflicts.py +58 -0
  71. agentbridge/workspace/diff.py +71 -0
  72. agentbridge/workspace/git.py +99 -0
  73. agentbridge/workspace/integration.py +54 -0
  74. agentbridge/workspace/manager.py +189 -0
  75. agentbridge_core-0.1.0.dist-info/METADATA +364 -0
  76. agentbridge_core-0.1.0.dist-info/RECORD +79 -0
  77. agentbridge_core-0.1.0.dist-info/WHEEL +4 -0
  78. agentbridge_core-0.1.0.dist-info/entry_points.txt +2 -0
  79. agentbridge_core-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,5 @@
1
+ """AgentBridge: provider-agnostic orchestration for AI coding agents."""
2
+
3
+ __version__ = "0.1.0"
4
+
5
+ __all__ = ["__version__"]
@@ -0,0 +1,3 @@
1
+ from agentbridge.main import main
2
+
3
+ main()
File without changes
File without changes
@@ -0,0 +1,211 @@
1
+ """Google Antigravity remote agent: human-in-the-loop handoff.
2
+
3
+ No documented, verified headless task API was available when this adapter was
4
+ written, so AgentBridge does **not** start remote executions. ``execute``
5
+ generates a task packet and instructions; a developer launches the remote
6
+ session with Antigravity's own supported workflow, and the resulting branch is
7
+ imported with ``agentbridge remote import``. The adapter never reports that a
8
+ remote task ran.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+ import re
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ from agentbridge.agents.base import (
19
+ AgentAdapter,
20
+ AvailabilityReport,
21
+ ExecutionRequest,
22
+ ExecutionResult,
23
+ ExecutionState,
24
+ )
25
+ from agentbridge.agents.capabilities import Capability, OperationSupport, ProviderCapabilities
26
+ from agentbridge.config.settings import ProviderSettings
27
+ from agentbridge.execution.process import resolve_executable
28
+ from agentbridge.workspace.git import Git
29
+
30
+ DOCS_URL = "https://antigravity.google/docs/remote-agents"
31
+ _USERINFO = re.compile(r"(?<=://)[^/@\s]+@")
32
+
33
+
34
+ def sanitize_remote_url(url: str) -> str:
35
+ """Strip any embedded credentials from a git remote URL."""
36
+ return _USERINFO.sub("", url)
37
+
38
+
39
+ def remote_branch_name(task_id: str) -> str:
40
+ return f"agentbridge-remote/{task_id}"
41
+
42
+
43
+ class AntigravityRemoteAdapter(AgentAdapter):
44
+ provider_type = "antigravity_remote"
45
+
46
+ def __init__(self, name: str, settings: ProviderSettings) -> None:
47
+ super().__init__(name, settings)
48
+ self.command = settings.command or "agy"
49
+
50
+ async def check_availability(self) -> AvailabilityReport:
51
+ executable = resolve_executable(self.command)
52
+ details = [
53
+ "Execution mode: remote_handoff (manual). AgentBridge generates a task packet; "
54
+ "you launch the remote agent yourself.",
55
+ f"Documentation: {DOCS_URL}",
56
+ ]
57
+ if executable:
58
+ details.append(f"Antigravity CLI found at {executable} (commands not auto-invoked)")
59
+ else:
60
+ details.append(
61
+ f"Antigravity CLI '{self.command}' not found; packets can still be generated, "
62
+ "but you need Antigravity installed to launch the remote session."
63
+ )
64
+ # Packet generation is local and always possible. Remote work also needs the
65
+ # repository to be reachable from the remote environment (a git remote).
66
+ return AvailabilityReport(
67
+ available=True, executable=executable, auth="unknown", details=details
68
+ )
69
+
70
+ async def get_capabilities(self) -> ProviderCapabilities:
71
+ return ProviderCapabilities(
72
+ provider_type=self.provider_type,
73
+ capabilities=frozenset(
74
+ {
75
+ Capability.EDIT_CODE,
76
+ Capability.ANALYZE,
77
+ Capability.REMOTE_WORKSPACE,
78
+ Capability.HUMAN_IN_THE_LOOP,
79
+ }
80
+ ),
81
+ execution=OperationSupport.MANUAL,
82
+ cancellation=OperationSupport.UNSUPPORTED,
83
+ status_query=OperationSupport.UNSUPPORTED,
84
+ notes=[
85
+ "No verified headless API: execution is a manual handoff.",
86
+ "Remote agents cannot see local worktrees; results return via git.",
87
+ "Token and cost data are not available to AgentBridge.",
88
+ ],
89
+ )
90
+
91
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult:
92
+ repo_root = Path(request.repository_info.get("root", "."))
93
+ packet = build_packet(request, repo_root, self.settings.git_remote)
94
+ request.output_dir.mkdir(parents=True, exist_ok=True)
95
+ json_path = request.output_dir / "remote_packet.json"
96
+ md_path = request.output_dir / "remote_packet.md"
97
+ json_path.write_text(json.dumps(packet, indent=2), encoding="utf-8")
98
+ md_path.write_text(render_packet_markdown(packet), encoding="utf-8")
99
+ return ExecutionResult(
100
+ execution_id=request.execution_id,
101
+ provider=self.name,
102
+ state=ExecutionState.HANDOFF_PREPARED,
103
+ summary=(
104
+ "Remote handoff packet prepared. No remote execution was started by "
105
+ f"AgentBridge. Follow {md_path} to launch it."
106
+ ),
107
+ output_path=str(md_path),
108
+ artifacts={"packet_json": str(json_path), "packet_markdown": str(md_path)},
109
+ metadata={"warnings": packet["warnings"]},
110
+ )
111
+
112
+
113
+ def build_packet(request: ExecutionRequest, repo_root: Path, git_remote: str) -> dict[str, Any]:
114
+ git = Git(repo_root)
115
+ warnings: list[str] = []
116
+ remote_url: str | None = None
117
+ result = git.run("remote", "get-url", git_remote, check=False)
118
+ if result.returncode == 0:
119
+ remote_url = sanitize_remote_url(result.stdout.strip())
120
+ else:
121
+ warnings.append(
122
+ f"Git remote '{git_remote}' is not configured. The remote agent needs the "
123
+ "repository on a hosted remote (e.g. GitHub)."
124
+ )
125
+ base_commit = request.repository_info.get("base_commit")
126
+ base_branch = request.repository_info.get("base_branch")
127
+ if remote_url and base_commit:
128
+ contains = git.run("branch", "-r", "--contains", str(base_commit), check=False)
129
+ if contains.returncode != 0 or not contains.stdout.strip():
130
+ warnings.append(
131
+ f"Base commit {str(base_commit)[:12]} is not on any remote-tracking branch; "
132
+ f"push it ('git push {git_remote} {base_branch}') before launching."
133
+ )
134
+ result_branch = remote_branch_name(request.task_id)
135
+ return {
136
+ "packet_version": 1,
137
+ "task_id": request.task_id,
138
+ "title": request.title,
139
+ "description": request.description,
140
+ "role": request.role,
141
+ "acceptance_criteria": request.acceptance_criteria,
142
+ "expected_files": request.expected_files,
143
+ "repository": {
144
+ "remote": git_remote,
145
+ "url": remote_url,
146
+ "base_branch": base_branch,
147
+ "base_commit": base_commit,
148
+ },
149
+ "deliverables": {
150
+ "result_branch": result_branch,
151
+ "instructions": (
152
+ f"Create branch '{result_branch}' from the base commit, implement the task, "
153
+ "commit, and push the branch (or open a pull request from it). Do not push "
154
+ "to the base branch."
155
+ ),
156
+ },
157
+ "prompt": request.prompt,
158
+ "import_command": (
159
+ f"agentbridge remote import {request.task_id} --branch {result_branch} "
160
+ f"--remote {git_remote}"
161
+ ),
162
+ "warnings": warnings,
163
+ "documentation": DOCS_URL,
164
+ "status_note": "This packet does not mean the task ran. It is a handoff.",
165
+ }
166
+
167
+
168
+ def render_packet_markdown(packet: dict[str, Any]) -> str:
169
+ repo = packet["repository"]
170
+ lines = [
171
+ f"# Remote handoff: {packet['task_id']}",
172
+ "",
173
+ "> AgentBridge has **not** executed this task. Launch it yourself using a",
174
+ f"> supported Antigravity workflow ({packet['documentation']}).",
175
+ "",
176
+ f"**Title:** {packet['title']}",
177
+ "",
178
+ "## Repository",
179
+ "",
180
+ f"- Remote: `{repo['remote']}` {repo['url'] or '(not configured)'}",
181
+ f"- Base branch: `{repo['base_branch']}`",
182
+ f"- Base commit: `{repo['base_commit']}`",
183
+ "",
184
+ "## Acceptance criteria",
185
+ "",
186
+ *[f"- {item}" for item in packet["acceptance_criteria"] or ["(none specified)"]],
187
+ "",
188
+ "## Steps",
189
+ "",
190
+ "1. Make sure the base commit is pushed to the remote.",
191
+ "2. Start a remote agent session attached to this repository using Antigravity.",
192
+ "3. Give it the prompt below.",
193
+ f"4. Ask it to push its work to branch `{packet['deliverables']['result_branch']}`.",
194
+ "5. When it finishes, import and validate the result locally:",
195
+ "",
196
+ "```bash",
197
+ packet["import_command"],
198
+ "```",
199
+ "",
200
+ "6. Review the validation report, then approve or reject with",
201
+ f" `agentbridge tasks approve {packet['task_id']}` / `agentbridge tasks reject ...`.",
202
+ "",
203
+ "## Prompt",
204
+ "",
205
+ "```text",
206
+ packet["prompt"],
207
+ "```",
208
+ ]
209
+ if packet["warnings"]:
210
+ lines += ["", "## Warnings", "", *[f"- {w}" for w in packet["warnings"]]]
211
+ return "\n".join(lines) + "\n"
@@ -0,0 +1,138 @@
1
+ """Provider-independent agent adapter contract."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ from abc import ABC, abstractmethod
7
+ from enum import StrEnum
8
+ from pathlib import Path
9
+ from typing import Any, Literal
10
+
11
+ from pydantic import BaseModel, ConfigDict, Field
12
+
13
+ from agentbridge.agents.capabilities import ProviderCapabilities
14
+ from agentbridge.config.settings import ProviderSettings
15
+
16
+
17
+ class ExecutionState(StrEnum):
18
+ COMPLETED = "completed"
19
+ FAILED = "failed"
20
+ TIMED_OUT = "timed_out"
21
+ CANCELLED = "cancelled"
22
+ INTERRUPTED = "interrupted"
23
+ INVALID_OUTPUT = "invalid_output"
24
+ UNAVAILABLE = "unavailable"
25
+ HANDOFF_PREPARED = "handoff_prepared" # nothing executed; a human must act
26
+ RUNNING = "running"
27
+ UNKNOWN = "unknown"
28
+
29
+
30
+ class UnsupportedOperationError(Exception):
31
+ """Raised when an adapter is asked for an operation it does not support."""
32
+
33
+
34
+ class AvailabilityReport(BaseModel):
35
+ available: bool
36
+ version: str | None = None
37
+ executable: str | None = None
38
+ auth: Literal["ok", "missing", "unknown", "not_required"] = "unknown"
39
+ details: list[str] = Field(default_factory=list)
40
+
41
+
42
+ class UsageMetrics(BaseModel):
43
+ """Usage data. ``None`` means *unavailable*, never zero."""
44
+
45
+ model: str | None = None
46
+ input_tokens: int | None = None
47
+ output_tokens: int | None = None
48
+ cost_usd: float | None = None
49
+ # "measured" (billing data) vs "estimated" (e.g. CLI-computed figure).
50
+ cost_kind: Literal["measured", "estimated"] | None = None
51
+ cost_source: str | None = None
52
+
53
+
54
+ class ExecutionRequest(BaseModel):
55
+ model_config = ConfigDict(arbitrary_types_allowed=True)
56
+
57
+ task_id: str
58
+ execution_id: str
59
+ attempt: int = 1
60
+ title: str
61
+ description: str = ""
62
+ role: str
63
+ prompt: str
64
+ acceptance_criteria: list[str] = Field(default_factory=list)
65
+ expected_files: list[str] = Field(default_factory=list)
66
+ workdir: Path | None = None
67
+ output_dir: Path
68
+ timeout_seconds: int
69
+ agents_may_edit: bool = True
70
+ permission_mode: str | None = None
71
+ repository_info: dict[str, Any] = Field(default_factory=dict)
72
+ metadata: dict[str, Any] = Field(default_factory=dict)
73
+
74
+
75
+ class ExecutionResult(BaseModel):
76
+ execution_id: str
77
+ provider: str
78
+ state: ExecutionState
79
+ summary: str = ""
80
+ exit_code: int | None = None
81
+ duration_seconds: float | None = None
82
+ output_path: str | None = None
83
+ error: str | None = None
84
+ retryable: bool = False
85
+ usage: UsageMetrics = Field(default_factory=UsageMetrics)
86
+ structured_output: dict[str, Any] | None = None
87
+ artifacts: dict[str, str] = Field(default_factory=dict)
88
+ metadata: dict[str, Any] = Field(default_factory=dict)
89
+
90
+
91
+ class AgentAdapter(ABC):
92
+ """Contract every provider adapter implements.
93
+
94
+ Unsupported operations raise ``UnsupportedOperationError`` (and are declared
95
+ as such in ``get_capabilities``); adapters must never fake support.
96
+ """
97
+
98
+ provider_type: str = "abstract"
99
+
100
+ def __init__(self, name: str, settings: ProviderSettings) -> None:
101
+ self.name = name
102
+ self.settings = settings
103
+ self._cancel_events: dict[str, asyncio.Event] = {}
104
+
105
+ @abstractmethod
106
+ async def check_availability(self) -> AvailabilityReport: ...
107
+
108
+ @abstractmethod
109
+ async def get_capabilities(self) -> ProviderCapabilities: ...
110
+
111
+ @abstractmethod
112
+ async def execute(self, request: ExecutionRequest) -> ExecutionResult: ...
113
+
114
+ async def get_status(self, execution_id: str) -> ExecutionState:
115
+ """Status of an execution, possibly from a previous process.
116
+
117
+ The default is honest: without provider support the state is UNKNOWN.
118
+ """
119
+ return ExecutionState.UNKNOWN
120
+
121
+ async def cancel(self, execution_id: str) -> bool:
122
+ """Request cancellation. Returns True if a running execution was signalled."""
123
+ event = self._cancel_events.get(execution_id)
124
+ if event is None:
125
+ return False
126
+ event.set()
127
+ return True
128
+
129
+ # Helpers for subclasses ------------------------------------------------
130
+ def _cancel_event(self, execution_id: str) -> asyncio.Event:
131
+ event = self._cancel_events.get(execution_id)
132
+ if event is None:
133
+ event = asyncio.Event()
134
+ self._cancel_events[execution_id] = event
135
+ return event
136
+
137
+ def _forget(self, execution_id: str) -> None:
138
+ self._cancel_events.pop(execution_id, None)
@@ -0,0 +1,48 @@
1
+ """Provider capability model.
2
+
3
+ Capabilities are *declared* by adapters. The router only assigns a task to a
4
+ provider that declares every capability the task requires; nothing is assumed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from enum import StrEnum
10
+
11
+ from pydantic import BaseModel, Field
12
+
13
+
14
+ class Capability(StrEnum):
15
+ PLAN = "plan" # can return a structured task plan
16
+ EDIT_CODE = "edit_code" # can modify files in an assigned workspace
17
+ ANALYZE = "analyze" # can read a repository and produce analysis text
18
+ REVIEW = "review" # can review a diff and report findings
19
+ LOCAL_WORKSPACE = "local_workspace" # operates on a local directory
20
+ REMOTE_WORKSPACE = "remote_workspace" # operates in a provider-managed environment
21
+ STRUCTURED_OUTPUT = "structured_output" # machine-readable output format
22
+ CANCEL = "cancel" # execution can be cancelled
23
+ STATUS = "status" # execution status can be queried after a restart
24
+ COST_REPORTING = "cost_reporting" # reports cost figures
25
+ TOKEN_REPORTING = "token_reporting" # reports token usage
26
+ HUMAN_IN_THE_LOOP = "human_in_the_loop" # requires a person to complete execution
27
+
28
+
29
+ class OperationSupport(StrEnum):
30
+ SUPPORTED = "supported"
31
+ UNSUPPORTED = "unsupported"
32
+ EXPERIMENTAL = "experimental"
33
+ MANUAL = "manual" # performed by a human following generated instructions
34
+
35
+
36
+ class ProviderCapabilities(BaseModel):
37
+ provider_type: str
38
+ capabilities: frozenset[Capability] = Field(default_factory=frozenset)
39
+ execution: OperationSupport = OperationSupport.SUPPORTED
40
+ cancellation: OperationSupport = OperationSupport.UNSUPPORTED
41
+ status_query: OperationSupport = OperationSupport.UNSUPPORTED
42
+ notes: list[str] = Field(default_factory=list)
43
+
44
+ def supports(self, required: set[Capability] | frozenset[Capability]) -> bool:
45
+ return set(required).issubset(self.capabilities)
46
+
47
+ def missing(self, required: set[Capability] | frozenset[Capability]) -> set[Capability]:
48
+ return set(required) - set(self.capabilities)
File without changes