Skip to content

Commit f5fa3c7

Browse files
author
zhangtao7
committed
Merge upstream/main: add agy provider, keep kimi/mmx
Resolved conflicts in builtin_backends.py and runtime_shared.py to keep kimi and mmx providers alongside the new agy (Antigravity). Upstream changes: - feat(provider): add agy (Google Antigravity CLI) backend (SeemSeam#211) - test(provider): cover agy env override + yolo/continue start args - docs: list agy/antigravity in supported providers - Cleanup: remove webhook import from bootstrap.py - Cleanup: delete various documentation files - Revert job heartbeat intervals from 120s to 600s Our changes preserved: - Kimi/mx providers kept in optional provider list - All local fixes (webhook, pane-foreign, Kimi wire, ccb restart)
2 parents c14613b + f856eb3 commit f5fa3c7

11 files changed

Lines changed: 274 additions & 6 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ CCB is a project-level agent CLI workspace. It uses tmux to manage multiple real
7575

7676
- **Real CLI sessions, not fake panels**: every agent pane runs the actual provider CLI.
7777
- **Visible collaboration**: the sidebar shows windows, agents, status, and communication; users can switch panes by mouse.
78-
- **Mixed providers**: one project can run Codex, Claude, Gemini, OpenCode, and Droid together.
78+
- **Mixed providers**: one project can run Codex, Claude, Gemini, OpenCode, Droid, and Antigravity (`agy`) together.
7979
- **Project config**: `.ccb/ccb.config` defines the team, layout, windows, worktrees, model, key, and url.
8080
- **Recoverable runtime**: CCB supervises agent panes and supports attach, restore, and project-scoped cleanup.
8181
- **Explicit collaboration channel**: agents can delegate through `/ask`, `$ask`, callback, and silence routes.

README_zh.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ CCB 是一个项目级 agent CLI 工作台。它用 tmux 管理多个真实 CLI
7575

7676
- **真实 CLI,不是模拟面板**:每个 agent pane 都运行对应 provider 的真实 CLI。
7777
- **可见协作**:sidebar 展示窗口、agent 状态和通信区;用户可以用鼠标直接切 pane。
78-
- **混合 provider**:一个项目里可以同时跑 Codex、Claude、Gemini、OpenCode、Droid。
78+
- **混合 provider**:一个项目里可以同时跑 Codex、Claude、Gemini、OpenCode、Droid 和 Antigravity(`agy`
7979
- **项目级配置**`.ccb/ccb.config` 决定团队、布局、窗口、worktree、model、key、url。
8080
- **可恢复运行态**:CCB 后台守护 agent pane,支持 attach、恢复和项目级清理。
8181
- **显式协作通道**:agent 可以通过 `/ask``$ask`、callback 和 silence 进行委派与交接。
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from provider_core.contracts import ProviderBackend
2+
3+
from .launcher import build_runtime_launcher
4+
from .manifest import build_manifest
5+
from .session import build_session_binding
6+
7+
8+
def build_backend() -> ProviderBackend:
9+
return ProviderBackend(
10+
manifest=build_manifest(),
11+
execution_adapter=None,
12+
session_binding=build_session_binding(),
13+
runtime_launcher=build_runtime_launcher(),
14+
)
15+
16+
17+
__all__ = ['build_backend']
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
from __future__ import annotations
2+
3+
import shlex
4+
from pathlib import Path
5+
6+
from agents.models import AgentSpec
7+
from cli.context import CliContext
8+
from cli.models import ParsedStartCommand
9+
from provider_core.caller_env import (
10+
caller_context_env,
11+
export_env_clause,
12+
join_env_prefix,
13+
provider_user_session_env,
14+
)
15+
from provider_core.contracts import ProviderRuntimeLauncher
16+
from provider_core.runtime_shared import provider_start_parts
17+
from workspace.models import WorkspacePlan
18+
19+
20+
_YOLO_FLAG = '--dangerously-skip-permissions'
21+
22+
23+
def build_runtime_launcher() -> ProviderRuntimeLauncher:
24+
return ProviderRuntimeLauncher(
25+
provider='agy',
26+
launch_mode='simple_tmux',
27+
prepare_launch_context=prepare_launch_context,
28+
build_start_cmd=build_start_cmd,
29+
build_session_payload=build_session_payload,
30+
)
31+
32+
33+
def prepare_launch_context(
34+
context: CliContext,
35+
spec: AgentSpec,
36+
plan: WorkspacePlan,
37+
runtime_dir: Path,
38+
prepared_state: dict[str, object],
39+
) -> dict[str, object]:
40+
del runtime_dir
41+
payload = dict(prepared_state or {})
42+
payload['agent_name'] = spec.name
43+
payload['project_root'] = str(context.project.project_root)
44+
payload['workspace_path'] = str(prepared_state.get('run_cwd') or plan.workspace_path)
45+
payload['agent_events_path'] = str(context.paths.agent_events_path(spec.name))
46+
return payload
47+
48+
49+
def build_start_cmd(
50+
command: ParsedStartCommand,
51+
spec: AgentSpec,
52+
runtime_dir,
53+
launch_session_id: str,
54+
*,
55+
prepared_state: dict[str, object] | None = None,
56+
) -> str:
57+
del prepared_state
58+
cmd_parts = provider_start_parts('agy')
59+
if command.auto_permission and _YOLO_FLAG not in cmd_parts and _YOLO_FLAG not in spec.startup_args:
60+
cmd_parts.append(_YOLO_FLAG)
61+
if command.restore and not _has_restore_arg(cmd_parts) and not _has_restore_arg(spec.startup_args):
62+
cmd_parts.append('--continue')
63+
cmd_parts.extend(spec.startup_args)
64+
cmd = ' '.join(shlex.quote(str(part)) for part in cmd_parts)
65+
runtime_dir = Path(runtime_dir)
66+
env_prefix = join_env_prefix(
67+
export_env_clause(provider_user_session_env()),
68+
export_env_clause(spec.env),
69+
export_env_clause(
70+
caller_context_env(actor=spec.name, runtime_dir=runtime_dir, launch_session_id=launch_session_id)
71+
),
72+
)
73+
if env_prefix:
74+
return f'{env_prefix}; {cmd}'
75+
return cmd
76+
77+
78+
def build_session_payload(
79+
context: CliContext,
80+
spec: AgentSpec,
81+
plan: WorkspacePlan,
82+
runtime_dir,
83+
run_cwd,
84+
pane_id: str,
85+
pane_title_marker: str,
86+
start_cmd: str,
87+
launch_session_id: str,
88+
prepared_state: dict[str, object],
89+
) -> dict[str, object]:
90+
del context, spec, prepared_state
91+
return {
92+
'ccb_session_id': launch_session_id,
93+
'runtime_dir': str(runtime_dir),
94+
'completion_artifact_dir': str(runtime_dir / 'completion'),
95+
'terminal': 'tmux',
96+
'tmux_session': pane_id,
97+
'pane_id': pane_id,
98+
'pane_title_marker': pane_title_marker,
99+
'workspace_path': str(plan.workspace_path),
100+
'work_dir': str(run_cwd),
101+
'start_cmd': start_cmd,
102+
}
103+
104+
105+
def _has_restore_arg(parts: tuple[str, ...] | list[str]) -> bool:
106+
normalized = {str(part).strip() for part in parts}
107+
return bool({'--continue', '-c', '--conversation'} & normalized)
108+
109+
110+
__all__ = ['build_runtime_launcher', 'build_start_cmd', 'prepare_launch_context']
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from __future__ import annotations
2+
3+
from agents.models import RuntimeMode
4+
from completion.models import CompletionFamily, CompletionSourceKind, SelectorFamily
5+
from completion.profiles import CompletionManifest
6+
from provider_core.manifests import ProviderManifest
7+
8+
9+
def build_manifest() -> ProviderManifest:
10+
return ProviderManifest(
11+
provider='agy',
12+
supports_resume=True,
13+
supports_permission_auto=True,
14+
supports_stream_watch=False,
15+
supports_subagents=True,
16+
supports_workspace_attach=True,
17+
runtime_profiles={
18+
RuntimeMode.PANE_BACKED: CompletionManifest(
19+
provider='agy',
20+
runtime_mode=RuntimeMode.PANE_BACKED.value,
21+
completion_family=CompletionFamily.TERMINAL_TEXT_QUIET,
22+
completion_source_kind=CompletionSourceKind.TERMINAL_TEXT,
23+
supports_exact_completion=False,
24+
supports_observed_completion=False,
25+
supports_anchor_binding=False,
26+
supports_reply_stability=False,
27+
supports_terminal_reason=False,
28+
selector_family=SelectorFamily.FINAL_MESSAGE,
29+
),
30+
},
31+
)
32+
33+
34+
__all__ = ['build_manifest']
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from pathlib import Path
5+
from typing import Optional
6+
7+
from provider_backends.pane_log_support.session import (
8+
PaneLogProjectSessionBase,
9+
build_session_binding_for_provider,
10+
compute_session_key_for_provider,
11+
load_project_session_for_provider,
12+
)
13+
from provider_core.contracts import ProviderSessionBinding
14+
15+
16+
@dataclass
17+
class AgyProjectSession(PaneLogProjectSessionBase):
18+
@property
19+
def agy_session_id(self) -> str:
20+
return str(self.data.get('agy_session_id') or self.data.get('ccb_session_id') or '').strip()
21+
22+
@property
23+
def agy_session_path(self) -> str:
24+
return str(self.session_file)
25+
26+
def backend(self):
27+
from terminal_runtime import get_backend_for_session
28+
29+
return get_backend_for_session(self.data)
30+
31+
32+
def find_project_session_file(work_dir: Path, instance: Optional[str] = None) -> Optional[Path]:
33+
from provider_backends.pane_log_support.session import find_project_session_file_for_provider
34+
35+
return find_project_session_file_for_provider(
36+
work_dir,
37+
session_filename='.agy-session',
38+
instance=instance,
39+
)
40+
41+
42+
def load_project_session(work_dir: Path, instance: Optional[str] = None) -> Optional[AgyProjectSession]:
43+
return load_project_session_for_provider(
44+
work_dir,
45+
session_filename='.agy-session',
46+
session_cls=AgyProjectSession,
47+
instance=instance,
48+
)
49+
50+
51+
def compute_session_key(session: AgyProjectSession, instance: Optional[str] = None) -> str:
52+
return compute_session_key_for_provider(session, provider='agy', instance=instance)
53+
54+
55+
def build_session_binding() -> ProviderSessionBinding:
56+
return build_session_binding_for_provider(provider='agy', load_session=load_project_session)
57+
58+
59+
__all__ = [
60+
'AgyProjectSession',
61+
'build_session_binding',
62+
'compute_session_key',
63+
'find_project_session_file',
64+
'load_project_session',
65+
]

lib/provider_core/registry_runtime/builtin_backends.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,11 @@
33
from provider_core.contracts import ProviderBackend
44

55
CORE_PROVIDER_NAMES = ("codex", "claude", "gemini")
6-
OPTIONAL_PROVIDER_NAMES = ("opencode", "droid", "mmx", "kimi")
6+
OPTIONAL_PROVIDER_NAMES = ("opencode", "droid", "mmx", "kimi", "agy")
77

88

99
def build_builtin_backends(*, include_optional: bool = True) -> list[ProviderBackend]:
10+
from provider_backends.agy import build_backend as build_agy_backend
1011
from provider_backends.claude import build_backend as build_claude_backend
1112
from provider_backends.codex import build_backend as build_codex_backend
1213
from provider_backends.droid import build_backend as build_droid_backend
@@ -26,6 +27,7 @@ def build_builtin_backends(*, include_optional: bool = True) -> list[ProviderBac
2627
build_droid_backend(),
2728
build_mmx_backend(),
2829
build_kimi_backend(),
30+
build_agy_backend(),
2931
])
3032
return backends
3133

lib/provider_core/runtime_shared.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
'droid': 'DROID_START_CMD',
1313
'kimi': 'KIMI_START_CMD',
1414
'mmx': 'MMX_START_CMD',
15+
'agy': 'AGY_START_CMD',
1516
}
1617

1718
_PROVIDER_DEFAULT_EXECUTABLES = {
@@ -22,6 +23,7 @@
2223
'droid': 'droid',
2324
'kimi': 'kimi',
2425
'mmx': 'mmx-daemon',
26+
'agy': 'agy',
2527
}
2628

2729

test/test_v2_provider_catalog.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ def test_default_provider_catalog_contains_expected_profiles() -> None:
2828
'gemini',
2929
'opencode',
3030
'droid',
31+
'agy',
3132
}
3233
codex = catalog.resolve_completion_manifest('codex', RuntimeMode.PANE_BACKED)
3334
assert codex.completion_family is CompletionFamily.PROTOCOL_TURN
@@ -39,6 +40,9 @@ def test_default_provider_catalog_contains_expected_profiles() -> None:
3940
assert fake_codex.completion_family is CompletionFamily.PROTOCOL_TURN
4041
fake_gemini = catalog.resolve_completion_manifest('fake-gemini', RuntimeMode.PANE_BACKED)
4142
assert fake_gemini.completion_family is CompletionFamily.ANCHORED_SESSION_STABILITY
43+
assert catalog.get('agy').supports_resume is True
44+
agy = catalog.resolve_completion_manifest('agy', RuntimeMode.PANE_BACKED)
45+
assert agy.completion_family is CompletionFamily.TERMINAL_TEXT_QUIET
4246
fake_legacy = catalog.resolve_completion_manifest('fake-legacy', RuntimeMode.PANE_BACKED)
4347
assert fake_legacy.completion_family is CompletionFamily.TERMINAL_TEXT_QUIET
4448

@@ -81,4 +85,4 @@ def test_provider_catalog_can_build_core_only_catalog() -> None:
8185
catalog = build_default_provider_catalog(include_optional=False, include_test_doubles=False)
8286
assert set(catalog.providers()) == set(CORE_PROVIDER_NAMES)
8387
assert set(CORE_PROVIDER_NAMES) == {'codex', 'claude', 'gemini'}
84-
assert set(OPTIONAL_PROVIDER_NAMES) == {'opencode', 'droid'}
88+
assert set(OPTIONAL_PROVIDER_NAMES) == {'opencode', 'droid', 'agy'}

test/test_v2_provider_core_registry.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,19 @@ def test_backend_registry_exposes_manifests_execution_and_session_bindings() ->
3131
def test_default_session_binding_map_uses_backend_owned_entries() -> None:
3232
bindings = build_default_session_binding_map(include_optional=True)
3333

34-
assert set(bindings) == {'codex', 'claude', 'gemini', 'opencode', 'droid'}
34+
assert set(bindings) == {'codex', 'claude', 'gemini', 'opencode', 'droid', 'agy'}
3535
assert bindings['codex'].session_id_attr == 'codex_session_id'
3636
assert bindings['opencode'].session_path_attr == 'session_file'
37+
assert bindings['agy'].session_path_attr == 'agy_session_path'
3738

3839

3940
def test_default_runtime_launcher_map_uses_backend_owned_entries() -> None:
4041
launchers = build_default_runtime_launcher_map(include_optional=True)
4142

42-
assert set(launchers) == {'codex', 'claude', 'gemini', 'opencode', 'droid'}
43+
assert set(launchers) == {'codex', 'claude', 'gemini', 'opencode', 'droid', 'agy'}
4344
assert launchers['codex'].launch_mode == 'codex_tmux'
4445
assert launchers['gemini'].launch_mode == 'simple_tmux'
46+
assert launchers['agy'].launch_mode == 'simple_tmux'
4547

4648

4749
def test_session_filename_for_agent_follows_agent_first_naming() -> None:

0 commit comments

Comments
 (0)