agentlink-cli 0.1.8__tar.gz → 0.1.9__tar.gz
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.
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/PKG-INFO +1 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/adapter.py +11 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/runtime.py +10 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/version.py +1 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/pyproject.toml +1 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_codex_adapter.py +60 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_runtime.py +64 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_version.py +1 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/uv.lock +1 -1
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/.gitignore +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/README.md +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/config_options.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/discovery.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifest.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifests/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifests/codebuddy.json +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifests/cursor.json +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifests/gemini.json +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/manifests/grok_build.json +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/reducer.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/acp/rpc.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/attachments.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/capabilities.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/history_adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/normalized.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/normalizers.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/path_utils.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/preferences.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/sdk_adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/timeline_identity.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/timeline_reducer.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/claude/trust.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/cli.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/collaboration.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/history.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/reducer.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/codex/rpc.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/control.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/json_rpc.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/launch.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/common.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/file_ops.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/ops.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/shell.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local/terminal.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local_ops.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/local_runtime.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/logging.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/perf.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/protocol.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/registry.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/sync_state.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/connector/time.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/run.sh +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/fake_agent/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/fake_agent/__main__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/test_acp_adapter_fake.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/test_acp_reducer_golden.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/acp/test_config_options.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/contract/__init__.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/contract/helpers.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_claude_history_adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_claude_preferences.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_claude_sdk_adapter.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_claude_timeline_parity.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_claude_trust.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_capabilities.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_cli.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_control.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_json_rpc.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_connector_logging.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_launch_command.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_local_runtime.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_perf.py +0 -0
- {agentlink_cli-0.1.8 → agentlink_cli-0.1.9}/tests/test_terminal_backend.py +0 -0
|
@@ -585,6 +585,17 @@ class CodexAdapter:
|
|
|
585
585
|
raw_args=raw_args,
|
|
586
586
|
phase="started",
|
|
587
587
|
)
|
|
588
|
+
if self.notification_sink is not None:
|
|
589
|
+
for notification in _item_notifications(session_id, started_items):
|
|
590
|
+
try:
|
|
591
|
+
await self.notification_sink(notification["method"], notification["params"])
|
|
592
|
+
except Exception as exc: # noqa: BLE001
|
|
593
|
+
logger.warning(
|
|
594
|
+
"codex command started notification enqueue failed session_id={} command={} error={}",
|
|
595
|
+
session_id,
|
|
596
|
+
command,
|
|
597
|
+
exc,
|
|
598
|
+
)
|
|
588
599
|
try:
|
|
589
600
|
if command == "compact":
|
|
590
601
|
await self.rpc.request("thread/compact/start", {"threadId": thread_id})
|
|
@@ -497,7 +497,16 @@ class BackendRpcClient:
|
|
|
497
497
|
if execute_command is None:
|
|
498
498
|
raise ValueError("command_unavailable")
|
|
499
499
|
result = await execute_command({**params, "connectorId": self.config.connector_id})
|
|
500
|
-
|
|
500
|
+
notifications = result.get("backendNotifications", [])
|
|
501
|
+
if isinstance(notifications, list):
|
|
502
|
+
try:
|
|
503
|
+
await self.enqueue_backend_notifications(notifications)
|
|
504
|
+
except Exception: # noqa: BLE001
|
|
505
|
+
logger.exception(
|
|
506
|
+
"command timeline notification enqueue failed session_id={} command={}",
|
|
507
|
+
params.get("sessionId"),
|
|
508
|
+
params.get("command"),
|
|
509
|
+
)
|
|
501
510
|
return _strip_backend_notifications(result)
|
|
502
511
|
if method == "turn.interrupt":
|
|
503
512
|
return await self._resolve_adapter(params).interrupt_turn(params)
|
|
@@ -1149,6 +1149,66 @@ def test_codex_commands_use_native_app_server_methods() -> None:
|
|
|
1149
1149
|
asyncio.run(exercise())
|
|
1150
1150
|
|
|
1151
1151
|
|
|
1152
|
+
def test_review_command_publishes_started_items_before_native_review_events() -> None:
|
|
1153
|
+
async def exercise() -> None:
|
|
1154
|
+
published: list[tuple[str, dict[str, Any]]] = []
|
|
1155
|
+
|
|
1156
|
+
class ReviewRpc(FakeCodexRpc):
|
|
1157
|
+
async def request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
1158
|
+
if method == "review/start":
|
|
1159
|
+
assert [entry[1]["item"]["content"].get("phase") for entry in published] == [None, "started"]
|
|
1160
|
+
return {"turn": {"id": "turn_review"}}
|
|
1161
|
+
return await super().request(method, params)
|
|
1162
|
+
|
|
1163
|
+
async def sink(method: str, params: dict[str, Any]) -> None:
|
|
1164
|
+
published.append((method, params))
|
|
1165
|
+
|
|
1166
|
+
adapter = CodexAdapter(rpc=ReviewRpc(), notification_sink=sink) # type: ignore[arg-type]
|
|
1167
|
+
result = await adapter.execute_command(
|
|
1168
|
+
{
|
|
1169
|
+
"sessionId": "sess_1",
|
|
1170
|
+
"externalSessionId": "thr_1",
|
|
1171
|
+
"command": "review",
|
|
1172
|
+
"clientCommandId": "cmd_review",
|
|
1173
|
+
}
|
|
1174
|
+
)
|
|
1175
|
+
|
|
1176
|
+
started_user = published[0][1]["item"]
|
|
1177
|
+
started_command = published[1][1]["item"]
|
|
1178
|
+
completed_user = result["backendNotifications"][0]["params"]["item"]
|
|
1179
|
+
accepted_command = result["backendNotifications"][1]["params"]["item"]
|
|
1180
|
+
assert started_user["id"] == completed_user["id"]
|
|
1181
|
+
assert started_command["id"] == accepted_command["id"]
|
|
1182
|
+
assert accepted_command["content"]["phase"] == "accepted"
|
|
1183
|
+
assert accepted_command["content"]["data"]["turnId"] == "turn_review"
|
|
1184
|
+
|
|
1185
|
+
asyncio.run(exercise())
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def test_review_command_continues_when_started_notification_enqueue_fails() -> None:
|
|
1189
|
+
async def exercise() -> None:
|
|
1190
|
+
rpc = FakeCodexRpc()
|
|
1191
|
+
|
|
1192
|
+
async def failing_sink(_method: str, _params: dict[str, Any]) -> None:
|
|
1193
|
+
raise OSError("temporary ingest failure")
|
|
1194
|
+
|
|
1195
|
+
adapter = CodexAdapter(rpc=rpc, notification_sink=failing_sink) # type: ignore[arg-type]
|
|
1196
|
+
result = await adapter.execute_command(
|
|
1197
|
+
{
|
|
1198
|
+
"sessionId": "sess_1",
|
|
1199
|
+
"externalSessionId": "thr_1",
|
|
1200
|
+
"command": "review",
|
|
1201
|
+
"clientCommandId": "cmd_review",
|
|
1202
|
+
}
|
|
1203
|
+
)
|
|
1204
|
+
|
|
1205
|
+
assert result["status"] == "accepted"
|
|
1206
|
+
assert any(method == "review/start" for method, _params in rpc.requests)
|
|
1207
|
+
assert result["backendNotifications"][1]["params"]["item"]["content"]["phase"] == "accepted"
|
|
1208
|
+
|
|
1209
|
+
asyncio.run(exercise())
|
|
1210
|
+
|
|
1211
|
+
|
|
1152
1212
|
def test_status_command_returns_thread_and_cached_token_usage() -> None:
|
|
1153
1213
|
async def exercise() -> None:
|
|
1154
1214
|
rpc = FakeCodexRpc()
|
|
@@ -142,6 +142,70 @@ def test_connector_runtime_dispatches_request_and_forwards_notifications() -> No
|
|
|
142
142
|
asyncio.run(_exercise_runtime())
|
|
143
143
|
|
|
144
144
|
|
|
145
|
+
def test_connector_runtime_queues_command_notifications_without_synchronous_ingest() -> None:
|
|
146
|
+
async def exercise() -> None:
|
|
147
|
+
class CommandAdapter(FakeAdapter):
|
|
148
|
+
async def execute_command(self, params: dict[str, Any]) -> dict[str, Any]:
|
|
149
|
+
self.calls.append(("command.execute", params))
|
|
150
|
+
return {
|
|
151
|
+
"status": "accepted",
|
|
152
|
+
"backendNotifications": [
|
|
153
|
+
{"method": "timeline.itemUpsert", "params": {"sessionId": "sess_1", "item": {"id": "cmd"}}},
|
|
154
|
+
{"method": "timeline.itemUpsert", "params": {"sessionId": "sess_1", "item": {"id": "result"}}},
|
|
155
|
+
],
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
adapter = CommandAdapter()
|
|
159
|
+
client = BackendRpcClient(
|
|
160
|
+
ConnectorConfig(
|
|
161
|
+
server_url="http://127.0.0.1:8000",
|
|
162
|
+
connector_id="conn_1",
|
|
163
|
+
connector_token="token",
|
|
164
|
+
sync_existing_on_connect=False,
|
|
165
|
+
),
|
|
166
|
+
adapter=adapter, # type: ignore[arg-type]
|
|
167
|
+
)
|
|
168
|
+
queued: list[tuple[str, dict[str, Any]]] = []
|
|
169
|
+
|
|
170
|
+
async def queue(method: str, params: dict[str, Any]) -> None:
|
|
171
|
+
queued.append((method, params))
|
|
172
|
+
|
|
173
|
+
async def fail_sync_ingest(_notifications: list[dict[str, Any]]) -> None:
|
|
174
|
+
raise AssertionError("command notifications must not use synchronous ingest")
|
|
175
|
+
|
|
176
|
+
client.send_backend_notification = queue # type: ignore[method-assign]
|
|
177
|
+
client.ingest_notifications = fail_sync_ingest # type: ignore[method-assign]
|
|
178
|
+
|
|
179
|
+
result = await client.dispatch(
|
|
180
|
+
"command.execute",
|
|
181
|
+
{
|
|
182
|
+
"runtime": "codex",
|
|
183
|
+
"sessionId": "sess_1",
|
|
184
|
+
"externalSessionId": "thr_1",
|
|
185
|
+
"command": "review",
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
assert result == {"status": "accepted"}
|
|
190
|
+
assert [params["item"]["id"] for _method, params in queued] == ["cmd", "result"]
|
|
191
|
+
|
|
192
|
+
async def fail_queue(_method: str, _params: dict[str, Any]) -> None:
|
|
193
|
+
raise OSError("outbox unavailable")
|
|
194
|
+
|
|
195
|
+
client.send_backend_notification = fail_queue # type: ignore[method-assign]
|
|
196
|
+
assert await client.dispatch(
|
|
197
|
+
"command.execute",
|
|
198
|
+
{
|
|
199
|
+
"runtime": "codex",
|
|
200
|
+
"sessionId": "sess_1",
|
|
201
|
+
"externalSessionId": "thr_1",
|
|
202
|
+
"command": "review",
|
|
203
|
+
},
|
|
204
|
+
) == {"status": "accepted"}
|
|
205
|
+
|
|
206
|
+
asyncio.run(exercise())
|
|
207
|
+
|
|
208
|
+
|
|
145
209
|
def test_connector_config_saves_and_loads_local_json(tmp_path) -> None:
|
|
146
210
|
path = tmp_path / "connector.json"
|
|
147
211
|
config = ConnectorConfig(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|