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.
- agentbridge/__init__.py +5 -0
- agentbridge/__main__.py +3 -0
- agentbridge/agents/__init__.py +0 -0
- agentbridge/agents/antigravity/__init__.py +0 -0
- agentbridge/agents/antigravity/adapter.py +211 -0
- agentbridge/agents/base.py +138 -0
- agentbridge/agents/capabilities.py +48 -0
- agentbridge/agents/claude/__init__.py +0 -0
- agentbridge/agents/claude/adapter.py +327 -0
- agentbridge/agents/claude/container.py +118 -0
- agentbridge/agents/generic_cli/__init__.py +0 -0
- agentbridge/agents/generic_cli/adapter.py +104 -0
- agentbridge/agents/mock/__init__.py +0 -0
- agentbridge/agents/mock/adapter.py +215 -0
- agentbridge/agents/prompts.py +86 -0
- agentbridge/agents/registry.py +58 -0
- agentbridge/cli/__init__.py +0 -0
- agentbridge/cli/app.py +67 -0
- agentbridge/cli/commands/__init__.py +0 -0
- agentbridge/cli/commands/dashboard.py +54 -0
- agentbridge/cli/commands/project.py +137 -0
- agentbridge/cli/commands/remote.py +49 -0
- agentbridge/cli/commands/tasks.py +281 -0
- agentbridge/cli/context.py +60 -0
- agentbridge/cli/output.py +52 -0
- agentbridge/config/__init__.py +0 -0
- agentbridge/config/loader.py +132 -0
- agentbridge/config/settings.py +351 -0
- agentbridge/core/__init__.py +0 -0
- agentbridge/core/events.py +49 -0
- agentbridge/core/orchestrator.py +970 -0
- agentbridge/core/planner.py +152 -0
- agentbridge/core/router.py +102 -0
- agentbridge/dashboard/__init__.py +0 -0
- agentbridge/dashboard/server.py +339 -0
- agentbridge/dashboard/static/assets/index-C4LNM7Ks.js +42 -0
- agentbridge/dashboard/static/assets/index-CuuZIrox.css +1 -0
- agentbridge/dashboard/static/index.html +13 -0
- agentbridge/errors.py +66 -0
- agentbridge/execution/__init__.py +0 -0
- agentbridge/execution/process.py +164 -0
- agentbridge/execution/sandbox.py +79 -0
- agentbridge/main.py +22 -0
- agentbridge/reporting/__init__.py +0 -0
- agentbridge/reporting/generator.py +290 -0
- agentbridge/security/__init__.py +0 -0
- agentbridge/security/paths.py +53 -0
- agentbridge/security/permissions.py +137 -0
- agentbridge/security/policies.py +59 -0
- agentbridge/security/secrets.py +80 -0
- agentbridge/storage/__init__.py +0 -0
- agentbridge/storage/database.py +211 -0
- agentbridge/storage/migrations/__init__.py +0 -0
- agentbridge/storage/migrations/env.py +20 -0
- agentbridge/storage/migrations/script.py.mako +25 -0
- agentbridge/storage/migrations/versions/0001_initial_schema.py +176 -0
- agentbridge/storage/migrations/versions/__init__.py +0 -0
- agentbridge/storage/repositories.py +237 -0
- agentbridge/tasks/__init__.py +0 -0
- agentbridge/tasks/dependencies.py +126 -0
- agentbridge/tasks/models.py +126 -0
- agentbridge/tasks/recovery.py +91 -0
- agentbridge/tasks/state_machine.py +54 -0
- agentbridge/validation/__init__.py +0 -0
- agentbridge/validation/engine.py +155 -0
- agentbridge/validation/models.py +52 -0
- agentbridge/validation/review.py +78 -0
- agentbridge/validation/runners.py +120 -0
- agentbridge/workspace/__init__.py +0 -0
- agentbridge/workspace/conflicts.py +58 -0
- agentbridge/workspace/diff.py +71 -0
- agentbridge/workspace/git.py +99 -0
- agentbridge/workspace/integration.py +54 -0
- agentbridge/workspace/manager.py +189 -0
- agentbridge_core-0.1.0.dist-info/METADATA +364 -0
- agentbridge_core-0.1.0.dist-info/RECORD +79 -0
- agentbridge_core-0.1.0.dist-info/WHEEL +4 -0
- agentbridge_core-0.1.0.dist-info/entry_points.txt +2 -0
- agentbridge_core-0.1.0.dist-info/licenses/LICENSE +21 -0
agentbridge/__init__.py
ADDED
agentbridge/__main__.py
ADDED
|
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
|