agentlink-cli 0.1.6__tar.gz → 0.1.8__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.
Files changed (85) hide show
  1. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/PKG-INFO +1 -1
  2. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/adapter.py +41 -35
  3. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/discovery.py +19 -8
  4. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifest.py +1 -2
  5. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/rpc.py +4 -5
  6. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/adapter.py +0 -1
  7. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/attachments.py +0 -1
  8. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/capabilities.py +9 -9
  9. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/history_adapter.py +3 -4
  10. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/path_utils.py +1 -1
  11. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/sdk_adapter.py +33 -9
  12. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/timeline_reducer.py +2 -2
  13. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/adapter.py +33 -18
  14. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/collaboration.py +0 -1
  15. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/history.py +1 -2
  16. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/reducer.py +1 -1
  17. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/rpc.py +1 -1
  18. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/control.py +34 -9
  19. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/json_rpc.py +1 -2
  20. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/launch.py +1 -1
  21. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/common.py +33 -8
  22. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/file_ops.py +56 -11
  23. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/ops.py +4 -0
  24. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/shell.py +108 -27
  25. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/terminal.py +29 -17
  26. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local_runtime.py +18 -7
  27. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/logging.py +1 -2
  28. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/perf.py +1 -1
  29. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/registry.py +0 -1
  30. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/runtime.py +285 -47
  31. agentlink_cli-0.1.8/connector/sync_state.py +318 -0
  32. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/version.py +1 -1
  33. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/pyproject.toml +1 -1
  34. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/contract/helpers.py +0 -1
  35. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_claude_history_adapter.py +6 -6
  36. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_claude_sdk_adapter.py +2 -2
  37. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_claude_timeline_parity.py +4 -1
  38. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_codex_adapter.py +10 -10
  39. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_capabilities.py +6 -7
  40. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_cli.py +7 -7
  41. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_control.py +2 -2
  42. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_runtime.py +305 -16
  43. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_terminal_backend.py +115 -1
  44. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_version.py +1 -1
  45. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/uv.lock +1 -1
  46. agentlink_cli-0.1.6/connector/sync_state.py +0 -155
  47. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/.gitignore +0 -0
  48. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/README.md +0 -0
  49. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/__init__.py +0 -0
  50. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/__init__.py +0 -0
  51. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/config_options.py +0 -0
  52. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifests/__init__.py +0 -0
  53. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifests/codebuddy.json +0 -0
  54. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifests/cursor.json +0 -0
  55. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifests/gemini.json +0 -0
  56. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/manifests/grok_build.json +0 -0
  57. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/acp/reducer.py +0 -0
  58. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/__init__.py +0 -0
  59. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/normalized.py +0 -0
  60. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/normalizers.py +0 -0
  61. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/preferences.py +0 -0
  62. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/timeline_identity.py +0 -0
  63. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/claude/trust.py +0 -0
  64. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/cli.py +0 -0
  65. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/codex/__init__.py +0 -0
  66. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local/__init__.py +0 -0
  67. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/local_ops.py +0 -0
  68. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/protocol.py +0 -0
  69. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/connector/time.py +0 -0
  70. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/run.sh +0 -0
  71. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/__init__.py +0 -0
  72. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/__init__.py +0 -0
  73. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/fake_agent/__init__.py +0 -0
  74. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/fake_agent/__main__.py +0 -0
  75. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/test_acp_adapter_fake.py +0 -0
  76. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/test_acp_reducer_golden.py +0 -0
  77. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/acp/test_config_options.py +0 -0
  78. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/contract/__init__.py +0 -0
  79. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_claude_preferences.py +0 -0
  80. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_claude_trust.py +0 -0
  81. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_json_rpc.py +0 -0
  82. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_connector_logging.py +0 -0
  83. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_launch_command.py +0 -0
  84. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_local_runtime.py +0 -0
  85. {agentlink_cli-0.1.6 → agentlink_cli-0.1.8}/tests/test_perf.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: agentlink-cli
3
- Version: 0.1.6
3
+ Version: 0.1.8
4
4
  Summary: Local runtime connector for AgentLink
5
5
  Requires-Python: >=3.12
6
6
  Requires-Dist: claude-agent-sdk
@@ -125,7 +125,7 @@ class AcpAdapter:
125
125
  },
126
126
  timeout=_SESSION_NEW_TIMEOUT_S,
127
127
  )
128
- except (AcpJsonRpcError, TimeoutError, asyncio.TimeoutError) as exc:
128
+ except (AcpJsonRpcError, TimeoutError) as exc:
129
129
  stderr = client.stderr_excerpt if client else ""
130
130
  detail = str(exc)
131
131
  if _is_authish_error(detail):
@@ -257,7 +257,7 @@ class AcpAdapter:
257
257
  if isinstance(item, dict) and item.get("sessionId")
258
258
  ]
259
259
  return {"threads": threads[:limit], "skippedThreads": [], "backendNotifications": []}
260
- except Exception:
260
+ except Exception: # noqa: BLE001
261
261
  logger.exception("{} session/list failed", self.runtime)
262
262
  return {"threads": [], "skippedThreads": [], "backendNotifications": []}
263
263
 
@@ -305,7 +305,7 @@ class AcpAdapter:
305
305
  client = await self._ensure_client()
306
306
  await client.notify("session/cancel", {"sessionId": runtime.external_session_id})
307
307
  return {"interrupted": True}
308
- except Exception as exc:
308
+ except Exception as exc: # noqa: BLE001
309
309
  logger.exception("{} session/cancel failed", self.runtime)
310
310
  return {"interrupted": False, "reason": str(exc)}
311
311
 
@@ -382,7 +382,7 @@ class AcpAdapter:
382
382
  },
383
383
  timeout=max_turn_s,
384
384
  )
385
- except AcpJsonRpcError as exc:
385
+ except AcpJsonRpcError:
386
386
  if runtime.interrupted:
387
387
  outcome = "cancelled"
388
388
  end = runtime.reducer.turn_end(
@@ -394,7 +394,7 @@ class AcpAdapter:
394
394
  )
395
395
  await self._emit_reduction(end)
396
396
  return
397
- raise exc
397
+ raise
398
398
  stop_reason = _optional_string(result.get("stopReason") or result.get("stop_reason"))
399
399
  if runtime.interrupted:
400
400
  outcome = "cancelled"
@@ -409,7 +409,7 @@ class AcpAdapter:
409
409
  except asyncio.CancelledError:
410
410
  outcome = "cancelled"
411
411
  raise
412
- except Exception as exc:
412
+ except Exception as exc: # noqa: BLE001
413
413
  outcome = "cancelled" if runtime.interrupted else "failed"
414
414
  logger.exception(
415
415
  "ACP turn failed runtime={} session_id={} turn_id={}",
@@ -477,7 +477,7 @@ class AcpAdapter:
477
477
  data, original_name, detected_type = await self.attachment_downloader(
478
478
  runtime.session_id, file_id
479
479
  )
480
- except Exception:
480
+ except Exception: # noqa: BLE001
481
481
  logger.exception("attachment download failed file_id={}", file_id)
482
482
  blocks.append({"type": "text", "text": f"\n\nAttached file: {name}"})
483
483
  continue
@@ -542,7 +542,7 @@ class AcpAdapter:
542
542
  },
543
543
  timeout=45.0,
544
544
  )
545
- except (AcpJsonRpcError, TimeoutError, asyncio.TimeoutError) as exc:
545
+ except (AcpJsonRpcError, TimeoutError) as exc:
546
546
  stderr = client.stderr_excerpt
547
547
  await self._shutdown_client_unlocked()
548
548
  detail = str(exc)
@@ -575,7 +575,7 @@ class AcpAdapter:
575
575
  """Best-effort pre-spawn of the ACP process so first session is faster."""
576
576
  try:
577
577
  await self._ensure_client()
578
- except Exception as exc:
578
+ except Exception as exc: # noqa: BLE001
579
579
  logger.info("{} warm_start skipped: {}", self.runtime, exc)
580
580
 
581
581
  async def _shutdown_client(self) -> None:
@@ -589,7 +589,7 @@ class AcpAdapter:
589
589
  if client is not None:
590
590
  try:
591
591
  await client.close()
592
- except Exception:
592
+ except Exception: # noqa: BLE001
593
593
  logger.exception("{} failed to close ACP client", self.runtime)
594
594
 
595
595
  async def _on_client_exit(self) -> None:
@@ -613,7 +613,7 @@ class AcpAdapter:
613
613
  interrupted=True,
614
614
  )
615
615
  await self._emit_reduction(end)
616
- except Exception:
616
+ except Exception: # noqa: BLE001
617
617
  logger.exception("failed to finalize turn after process exit")
618
618
  await self._emit(
619
619
  "runtime.error",
@@ -793,7 +793,7 @@ class AcpAdapter:
793
793
  )
794
794
  used_method = mid
795
795
  break
796
- except Exception as exc:
796
+ except Exception as exc: # noqa: BLE001
797
797
  last_error = str(exc)
798
798
  logger.warning(
799
799
  "{} interactive authenticate method={} failed: {}",
@@ -886,8 +886,8 @@ class AcpAdapter:
886
886
  "sourceObservedAt": utc_now(),
887
887
  },
888
888
  )
889
- except Exception:
890
- pass
889
+ except Exception: # noqa: BLE001
890
+ logger.exception("{} failed to emit required authentication status", self.runtime)
891
891
  return detail
892
892
 
893
893
  async def _probe_auth_and_options(self, client: AcpJsonRpcClient) -> dict[str, Any]:
@@ -946,7 +946,14 @@ class AcpAdapter:
946
946
  else:
947
947
  await client.request(method, {"sessionId": session_id}, timeout=3.0)
948
948
  break
949
- except Exception:
949
+ except Exception as exc: # noqa: BLE001
950
+ logger.debug(
951
+ "{} best-effort session cleanup failed method={} session_id={} error={}",
952
+ self.runtime,
953
+ method,
954
+ session_id,
955
+ exc,
956
+ )
950
957
  continue
951
958
  return probe
952
959
 
@@ -997,7 +1004,7 @@ class AcpAdapter:
997
1004
  )
998
1005
  logger.info("{} authenticated via method={}", self.runtime, mid)
999
1006
  return True
1000
- except Exception as exc:
1007
+ except Exception as exc: # noqa: BLE001
1001
1008
  logger.debug(
1002
1009
  "{} authenticate method={} failed: {}",
1003
1010
  self.runtime,
@@ -1132,23 +1139,24 @@ class AcpAdapter:
1132
1139
  if isinstance(session_id, str):
1133
1140
  session_runtime = self._sessions.get(session_id)
1134
1141
  timer = session_runtime.turn_timer if session_runtime is not None else None
1135
- if timer is not None and item_type not in {"turn.start", "turn.end"}:
1136
- if (
1137
- item_type == "message"
1138
- and isinstance(item, dict)
1139
- and item.get("role") == "assistant"
1140
- and isinstance(item.get("content"), dict)
1141
- and (
1142
- (isinstance(item["content"].get("text"), str) and item["content"]["text"].strip())
1143
- or (isinstance(item["content"].get("rawText"), str) and item["content"]["rawText"].strip())
1144
- )
1145
- ):
1146
- timer.mark_first_timeline(
1147
- runtime=self.runtime,
1148
- session_id=session_id,
1149
- turn_id=item.get("turnId") if isinstance(item, dict) else None,
1150
- stage_alias="adapter.first_assistant_token",
1151
- )
1142
+ if (
1143
+ timer is not None
1144
+ and item_type not in {"turn.start", "turn.end"}
1145
+ and item_type == "message"
1146
+ and isinstance(item, dict)
1147
+ and item.get("role") == "assistant"
1148
+ and isinstance(item.get("content"), dict)
1149
+ and (
1150
+ (isinstance(item["content"].get("text"), str) and item["content"]["text"].strip())
1151
+ or (isinstance(item["content"].get("rawText"), str) and item["content"]["rawText"].strip())
1152
+ )
1153
+ ):
1154
+ timer.mark_first_timeline(
1155
+ runtime=self.runtime,
1156
+ session_id=session_id,
1157
+ turn_id=item.get("turnId"),
1158
+ stage_alias="adapter.first_assistant_token",
1159
+ )
1152
1160
  await self._emit("timeline.itemUpsert", {"sessionId": item["sessionId"], "item": item})
1153
1161
  if reduced.approval:
1154
1162
  await self._emit("approval.requested", reduced.approval)
@@ -1217,5 +1225,3 @@ def _max_turn_seconds(manifest: AgentManifest) -> float | None:
1217
1225
 
1218
1226
  def _connector_version() -> str:
1219
1227
  return connector_version()
1220
-
1221
-
@@ -19,7 +19,6 @@ from connector.acp.rpc import AcpJsonRpcClient, AcpJsonRpcError
19
19
  from connector.launch import LaunchTarget, launch_target, path_exists_for_launch
20
20
  from connector.logging import logger
21
21
 
22
-
23
22
  _COMMAND_CHECK_TIMEOUT_S = 6.0
24
23
  # Full auth/model probe is expensive (spawn + session/new). Discovery stays light.
25
24
  _LIGHT_INIT_TIMEOUT_S = 10.0
@@ -164,7 +163,7 @@ async def probe_acp_agent(manifest: AgentManifest, target: LaunchTarget) -> dict
164
163
  if isinstance(session_id, str) and session_id:
165
164
  await _best_effort_close_session(client, session_id)
166
165
  return probe
167
- except Exception as exc:
166
+ except Exception as exc: # noqa: BLE001
168
167
  logger.warning("ACP probe failed runtime={} error={}", manifest.id, exc)
169
168
  probe["authStatus"] = "unknown"
170
169
  probe["probeError"] = str(exc)
@@ -174,8 +173,8 @@ async def probe_acp_agent(manifest: AgentManifest, target: LaunchTarget) -> dict
174
173
  finally:
175
174
  try:
176
175
  await client.close()
177
- except Exception:
178
- pass
176
+ except Exception: # noqa: BLE001
177
+ logger.exception("ACP probe client close failed runtime={}", manifest.id)
179
178
 
180
179
 
181
180
  def _looks_like_auth_error(message: str) -> bool:
@@ -217,7 +216,7 @@ async def _probe_session_new(
217
216
  return None, True
218
217
  logger.debug("ACP probe session/new failed non-auth: {}", exc)
219
218
  return None, False
220
- except Exception as exc:
219
+ except Exception as exc: # noqa: BLE001
221
220
  message = str(exc)
222
221
  if "timeout" in message.lower() or _looks_like_auth_error(message):
223
222
  return None, True
@@ -233,7 +232,13 @@ async def _best_effort_close_session(client: AcpJsonRpcClient, session_id: str)
233
232
  else:
234
233
  await client.request(method, {"sessionId": session_id}, timeout=2.0)
235
234
  break
236
- except Exception:
235
+ except Exception as exc: # noqa: BLE001
236
+ logger.debug(
237
+ "ACP best-effort session cleanup failed method={} session_id={} error={}",
238
+ method,
239
+ session_id,
240
+ exc,
241
+ )
237
242
  continue
238
243
 
239
244
 
@@ -268,7 +273,13 @@ async def _try_authenticate(
268
273
  )
269
274
  logger.info("ACP discovery authenticated runtime={} method={}", manifest.id, mid)
270
275
  return
271
- except Exception:
276
+ except Exception as exc: # noqa: BLE001
277
+ logger.debug(
278
+ "ACP headless authentication failed runtime={} method={} error={}",
279
+ manifest.id,
280
+ mid,
281
+ exc,
282
+ )
272
283
  continue
273
284
 
274
285
 
@@ -368,7 +379,7 @@ async def _run_command(command: list[str]) -> dict[str, Any]:
368
379
  return {"status": "ok", "stdout": (stdout or stderr)[:200]}
369
380
  except FileNotFoundError:
370
381
  return {"status": "missing", "reason": "file not found"}
371
- except Exception as exc:
382
+ except Exception as exc: # noqa: BLE001
372
383
  logger.exception("ACP version check failed command={}", command)
373
384
  return {"status": "failed", "reason": str(exc)}
374
385
 
@@ -6,7 +6,6 @@ from importlib import resources
6
6
  from pathlib import Path
7
7
  from typing import Any, Literal
8
8
 
9
-
10
9
  ProcessModel = Literal["shared", "per_session"]
11
10
  CapabilityLevel = Literal["required", "optional", "unsupported"]
12
11
 
@@ -86,7 +85,7 @@ def manifest_from_dict(raw: dict[str, Any]) -> AgentManifest:
86
85
  def load_manifest_file(path: str | Path) -> AgentManifest:
87
86
  data = json.loads(Path(path).read_text(encoding="utf-8"))
88
87
  if not isinstance(data, dict):
89
- raise ValueError(f"manifest file must be a JSON object: {path}")
88
+ raise TypeError(f"manifest file must be a JSON object: {path}")
90
89
  return manifest_from_dict(data)
91
90
 
92
91
 
@@ -8,7 +8,6 @@ from typing import Any
8
8
 
9
9
  from connector.logging import logger
10
10
 
11
-
12
11
  NotificationHandler = Callable[[dict[str, Any]], Awaitable[None]]
13
12
  ServerRequestHandler = Callable[[str | int, str, dict[str, Any]], Awaitable[dict[str, Any] | None]]
14
13
  ExitHandler = Callable[[], Awaitable[None]]
@@ -232,7 +231,7 @@ class AcpJsonRpcClient:
232
231
  if handler is not None and not self._closed:
233
232
  try:
234
233
  await handler()
235
- except Exception:
234
+ except Exception: # noqa: BLE001
236
235
  logger.exception("ACP exit handler failed")
237
236
 
238
237
  async def _read_stderr(self) -> None:
@@ -293,16 +292,16 @@ class AcpJsonRpcClient:
293
292
  if handler is None:
294
293
  try:
295
294
  await self.respond_error(request_id, code=-32601, message=f"Method not found: {method}")
296
- except Exception:
295
+ except Exception: # noqa: BLE001
297
296
  logger.exception("ACP respond_error failed method={}", method)
298
297
  return
299
298
  try:
300
299
  result = await handler(request_id, method, params)
301
300
  if result is not None:
302
301
  await self.respond(request_id, result)
303
- except Exception as exc:
302
+ except Exception as exc: # noqa: BLE001
304
303
  logger.exception("ACP server request handler failed method={}", method)
305
304
  try:
306
305
  await self.respond_error(request_id, code=-32000, message=str(exc))
307
- except Exception:
306
+ except Exception: # noqa: BLE001
308
307
  logger.exception("ACP respond_error failed method={}", method)
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from collections.abc import Awaitable, Callable
4
4
  from typing import Any, Protocol, runtime_checkable
5
5
 
6
-
7
6
  NotificationSink = Callable[[str, dict[str, Any]], Awaitable[None]] | None
8
7
 
9
8
 
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  import re
4
4
  from pathlib import Path
5
5
 
6
-
7
6
  ATTACHMENTS_ROOT_ENV = "AGENT_CONNECTOR_ATTACHMENTS_ROOT"
8
7
  DEFAULT_ATTACHMENTS_DIR = ".agent-link/attachments"
9
8
  _SAFE_FILENAME_RE = re.compile(r"[^\w.\-+]+")
@@ -264,7 +264,7 @@ async def _check_codex_candidate(candidate: dict[str, str] | LaunchTarget | Laun
264
264
  _read_codex_collaboration_mode_options(client),
265
265
  timeout=_CODEX_CHECK_TIMEOUT_S,
266
266
  )
267
- except Exception as exc:
267
+ except Exception as exc: # noqa: BLE001
268
268
  stage = "model-list" if "model_options" in locals() or _is_model_list_error(exc) else "app-server"
269
269
  return {
270
270
  **base,
@@ -276,8 +276,8 @@ async def _check_codex_candidate(candidate: dict[str, str] | LaunchTarget | Laun
276
276
  finally:
277
277
  try:
278
278
  await client.close()
279
- except Exception:
280
- pass
279
+ except Exception: # noqa: BLE001
280
+ logger.exception("closing Codex capability probe client failed")
281
281
 
282
282
  return {
283
283
  **base,
@@ -398,7 +398,7 @@ async def _read_codex_model_options(client: JsonRpcStdioClient) -> list[dict[str
398
398
 
399
399
  def _codex_model_page(result: Any) -> tuple[list[Any], str | None]:
400
400
  if not isinstance(result, dict):
401
- raise RuntimeError("model/list returned a non-object response")
401
+ raise RuntimeError("model/list returned a non-object response") # noqa: TRY004
402
402
  raw_models: Any = result.get("models")
403
403
  if raw_models is None:
404
404
  raw_models = result.get("items")
@@ -407,7 +407,7 @@ def _codex_model_page(result: Any) -> tuple[list[Any], str | None]:
407
407
  if isinstance(raw_models, dict):
408
408
  raw_models = raw_models.get("items") or raw_models.get("models")
409
409
  if not isinstance(raw_models, list):
410
- raise RuntimeError("model/list response has no models list")
410
+ raise RuntimeError("model/list response has no models list") # noqa: TRY004
411
411
  raw_cursor = (
412
412
  result.get("nextCursor")
413
413
  or result.get("nextPageToken")
@@ -463,7 +463,7 @@ def _is_model_list_error(exc: BaseException) -> bool:
463
463
  async def _read_codex_collaboration_mode_options(client: JsonRpcStdioClient) -> list[dict[str, Any]]:
464
464
  try:
465
465
  result = await client.request("collaborationMode/list", {})
466
- except Exception:
466
+ except Exception: # noqa: BLE001
467
467
  return []
468
468
  return normalize_collaboration_modes(result)
469
469
 
@@ -498,7 +498,7 @@ def _check_claude_history() -> dict[str, Any]:
498
498
  api = "list_sessions"
499
499
  try:
500
500
  sessions = _list_claude_sdk_sessions()
501
- except Exception as exc:
501
+ except Exception as exc: # noqa: BLE001
502
502
  return {
503
503
  "status": "unavailable",
504
504
  "source": source,
@@ -515,7 +515,7 @@ def _check_claude_history() -> dict[str, Any]:
515
515
 
516
516
  def _list_claude_sdk_sessions() -> list[Any]:
517
517
  sdk = importlib.import_module("claude_agent_sdk")
518
- list_sessions = getattr(sdk, "list_sessions")
518
+ list_sessions = sdk.list_sessions
519
519
  return list(list_sessions())
520
520
 
521
521
 
@@ -546,7 +546,7 @@ async def _run_version(command: list[str], *, start_new_session: bool = False) -
546
546
  if proc is not None and proc.returncode is None:
547
547
  await _terminate_process(proc, process_group=start_new_session)
548
548
  raise
549
- except Exception as exc:
549
+ except Exception as exc: # noqa: BLE001
550
550
  if proc is not None and proc.returncode is None:
551
551
  await _terminate_process(proc, process_group=start_new_session)
552
552
  return {"status": "failed", "reason": _exception_reason(exc)}
@@ -6,12 +6,11 @@ from dataclasses import dataclass, field
6
6
  from datetime import UTC, datetime
7
7
  from typing import Any
8
8
 
9
- from connector.logging import logger
10
-
11
9
  from connector.claude.normalizers import ClaudeTranscriptNormalizer
12
10
  from connector.claude.path_utils import stable_claude_session_id
13
11
  from connector.claude.timeline_identity import content_hash
14
12
  from connector.claude.timeline_reducer import ClaudeTimelineReducer
13
+ from connector.logging import logger
15
14
  from connector.sync_state import SyncStateStore
16
15
  from connector.time import utc_now
17
16
 
@@ -503,7 +502,7 @@ def _turn_boundary_item(
503
502
  def _list_sessions(sdk: Any, *, limit: int) -> list[Any]:
504
503
  list_sessions = getattr(sdk, "list_sessions", None)
505
504
  if not callable(list_sessions):
506
- raise RuntimeError("claude-agent-sdk does not expose list_sessions()")
505
+ raise RuntimeError("claude-agent-sdk does not expose list_sessions()") # noqa: TRY004
507
506
  return list(list_sessions(limit=limit))
508
507
 
509
508
 
@@ -520,7 +519,7 @@ def _get_session_info(sdk: Any, session_id: str, *, directory: str | None) -> An
520
519
  def _get_session_messages(sdk: Any, session_id: str, *, directory: str | None) -> list[Any]:
521
520
  get_session_messages = getattr(sdk, "get_session_messages", None)
522
521
  if not callable(get_session_messages):
523
- raise RuntimeError("claude-agent-sdk does not expose get_session_messages()")
522
+ raise RuntimeError("claude-agent-sdk does not expose get_session_messages()") # noqa: TRY004
524
523
  try:
525
524
  return list(get_session_messages(session_id, directory=directory))
526
525
  except TypeError:
@@ -9,5 +9,5 @@ def stable_claude_session_id(connector_id: str, claude_uuid: str) -> str:
9
9
  Mirrors connector.codex.adapter.stable_session_id so the backend never
10
10
  sees two ids referring to the same upstream session.
11
11
  """
12
- digest = hashlib.sha256(f"{connector_id}:claude:{claude_uuid}".encode("utf-8")).hexdigest()[:24]
12
+ digest = hashlib.sha256(f"{connector_id}:claude:{claude_uuid}".encode()).hexdigest()[:24]
13
13
  return f"sess_claude_{digest}"
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import asyncio
4
4
  import base64
5
5
  import hashlib
6
+ import inspect
6
7
  import json
7
8
  import re
8
9
  import secrets
@@ -10,18 +11,19 @@ from collections.abc import Awaitable, Callable
10
11
  from dataclasses import dataclass, field
11
12
  from typing import Any
12
13
 
13
- from connector.logging import logger
14
-
15
- from connector.attachments import attachment_target
16
14
  from connector.adapter import NotificationSink
15
+ from connector.attachments import attachment_target
17
16
  from connector.claude.history_adapter import ClaudeHistoryAdapter
18
17
  from connector.claude.normalized import NormalizedClaudeEvent
19
18
  from connector.claude.normalizers import ClaudeLiveNormalizer
20
- from connector.claude.timeline_reducer import ClaudeTimelineReducer, is_task_event_tool_name
19
+ from connector.claude.timeline_reducer import (
20
+ ClaudeTimelineReducer,
21
+ is_task_event_tool_name,
22
+ )
21
23
  from connector.launch import LaunchTarget, launch_target
24
+ from connector.logging import logger
22
25
  from connector.time import utc_now
23
26
 
24
-
25
27
  AttachmentDownloader = Callable[[str, str], Awaitable[tuple[bytes, str, str]]]
26
28
  """(session_id, file_id) -> (data, original_name, media_type)"""
27
29
 
@@ -97,6 +99,28 @@ class ClaudeSdkAdapter:
97
99
  def apply_history_sync_state(self, state: list[dict[str, Any]]) -> None:
98
100
  self.history_adapter.apply_history_sync_state(state)
99
101
 
102
+ async def close(self) -> None:
103
+ tasks = [
104
+ runtime.active_task
105
+ for runtime in self._sessions.values()
106
+ if runtime.active_task is not None and not runtime.active_task.done()
107
+ ]
108
+ for runtime in self._sessions.values():
109
+ runtime.interrupted = True
110
+ for pending in runtime.pending_approvals.values():
111
+ if not pending.future.done():
112
+ pending.future.set_result("cancelled")
113
+ client = runtime.client
114
+ if client is not None:
115
+ disconnect = getattr(client, "disconnect", None)
116
+ if callable(disconnect):
117
+ result = disconnect()
118
+ if inspect.isawaitable(result):
119
+ await result
120
+ for task in tasks:
121
+ task.cancel()
122
+ await asyncio.gather(*tasks, return_exceptions=True)
123
+
100
124
  async def create_session(self, params: dict[str, Any]) -> dict[str, Any]:
101
125
  session_id = (
102
126
  _optional_string(params.get("sessionId"))
@@ -238,7 +262,7 @@ class ClaudeSdkAdapter:
238
262
  stream_finished = True
239
263
  except asyncio.CancelledError:
240
264
  raise
241
- except Exception as exc:
265
+ except Exception as exc: # noqa: BLE001
242
266
  stderr = _stderr_excerpt(runtime.stderr_lines)
243
267
  logger.exception(
244
268
  "claude sdk turn failed session_id={} turn_id={} cwd={} external_session_id={} "
@@ -509,7 +533,7 @@ class ClaudeSdkAdapter:
509
533
  external_session_id=runtime.external_session_id,
510
534
  cwd=runtime.cwd,
511
535
  )
512
- except Exception:
536
+ except Exception: # noqa: BLE001
513
537
  logger.exception(
514
538
  "claude sdk history consumed marker failed session_id={} external_session_id={}",
515
539
  runtime.session_id,
@@ -529,7 +553,7 @@ class ClaudeSdkAdapter:
529
553
  "pendingClientMessages": _pending_client_messages(runtime),
530
554
  }
531
555
  )
532
- except Exception:
556
+ except Exception: # noqa: BLE001
533
557
  logger.exception(
534
558
  "claude sdk history snapshot failed session_id={} external_session_id={}",
535
559
  runtime.session_id,
@@ -678,7 +702,7 @@ class ClaudeSdkAdapter:
678
702
  data, original_name, media_type = await self.attachment_downloader(
679
703
  session_id, file_id
680
704
  )
681
- except Exception as exc:
705
+ except Exception as exc: # noqa: BLE001
682
706
  logger.exception("Claude attachment download failed file_id={}", file_id)
683
707
  blocks.append({"type": "text", "text": f"\n\n[Failed to load attachment {file_id}: {exc}]"})
684
708
  continue
@@ -183,7 +183,7 @@ def _content_score(content: Any) -> int:
183
183
 
184
184
  def _tool_call_content(event: NormalizedClaudeEvent) -> dict[str, Any]:
185
185
  tool_name = event.toolName or "tool"
186
- tool_input = event.toolInput if isinstance(event.toolInput, dict) else event.toolInput
186
+ tool_input = event.toolInput
187
187
  common = _tool_common(event)
188
188
  kind = _tool_kind(tool_name)
189
189
  if kind == "command":
@@ -235,7 +235,7 @@ def _tool_call_content(event: NormalizedClaudeEvent) -> dict[str, Any]:
235
235
  "kind": "tool",
236
236
  "name": tool_name,
237
237
  "tool": tool_name,
238
- "arguments": tool_input if isinstance(tool_input, dict) else tool_input,
238
+ "arguments": tool_input,
239
239
  })
240
240
 
241
241