uni-robot-agent-runtime 0.1.1__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 (309) hide show
  1. uni_robot_agent/__init__.py +50 -0
  2. uni_robot_agent/__main__.py +5 -0
  3. uni_robot_agent/agents/__init__.py +19 -0
  4. uni_robot_agent/agents/claude_code/__init__.py +2 -0
  5. uni_robot_agent/agents/claude_code/agent.py +163 -0
  6. uni_robot_agent/agents/claude_code/artifacts.py +29 -0
  7. uni_robot_agent/agents/claude_code/config.py +50 -0
  8. uni_robot_agent/agents/claude_code/events.py +121 -0
  9. uni_robot_agent/agents/claude_code/model_binding.py +163 -0
  10. uni_robot_agent/agents/claude_code/sdk_compat.py +27 -0
  11. uni_robot_agent/agents/claude_code/tools.py +131 -0
  12. uni_robot_agent/agents/codex/__init__.py +2 -0
  13. uni_robot_agent/agents/codex/agent.py +523 -0
  14. uni_robot_agent/agents/codex/artifacts.py +811 -0
  15. uni_robot_agent/agents/codex/config.py +91 -0
  16. uni_robot_agent/agents/codex/events.py +463 -0
  17. uni_robot_agent/agents/codex/model_binding.py +200 -0
  18. uni_robot_agent/agents/contracts.py +93 -0
  19. uni_robot_agent/agents/registry.py +53 -0
  20. uni_robot_agent/agents/runtime.py +79 -0
  21. uni_robot_agent/artifacts/__init__.py +5 -0
  22. uni_robot_agent/artifacts/actor.py +55 -0
  23. uni_robot_agent/artifacts/mirror.py +109 -0
  24. uni_robot_agent/artifacts/records.py +71 -0
  25. uni_robot_agent/artifacts/run.py +118 -0
  26. uni_robot_agent/artifacts/sft/__init__.py +1 -0
  27. uni_robot_agent/artifacts/store.py +191 -0
  28. uni_robot_agent/artifacts/writer.py +156 -0
  29. uni_robot_agent/cli.py +275 -0
  30. uni_robot_agent/config.py +302 -0
  31. uni_robot_agent/context/__init__.py +37 -0
  32. uni_robot_agent/context/history.py +206 -0
  33. uni_robot_agent/context/policies/__init__.py +6 -0
  34. uni_robot_agent/context/policies/identity.py +34 -0
  35. uni_robot_agent/context/policies/sliding_window.py +389 -0
  36. uni_robot_agent/context/prompt.py +215 -0
  37. uni_robot_agent/events/__init__.py +39 -0
  38. uni_robot_agent/events/core.py +202 -0
  39. uni_robot_agent/events/subscribers.py +214 -0
  40. uni_robot_agent/execution/__init__.py +179 -0
  41. uni_robot_agent/execution/correlation.py +18 -0
  42. uni_robot_agent/execution/parallel.py +303 -0
  43. uni_robot_agent/execution/scheduler.py +324 -0
  44. uni_robot_agent/execution/serial.py +60 -0
  45. uni_robot_agent/integrations/__init__.py +40 -0
  46. uni_robot_agent/integrations/base.py +91 -0
  47. uni_robot_agent/integrations/gripper.py +147 -0
  48. uni_robot_agent/integrations/prepared_episode.py +201 -0
  49. uni_robot_agent/integrations/robodojo/SOURCE.md +134 -0
  50. uni_robot_agent/integrations/robodojo/UPSTREAM_LICENSE +21 -0
  51. uni_robot_agent/integrations/robodojo/__init__.py +56 -0
  52. uni_robot_agent/integrations/robodojo/action_binding.py +331 -0
  53. uni_robot_agent/integrations/robodojo/assets.py +33 -0
  54. uni_robot_agent/integrations/robodojo/build_compat.py +24 -0
  55. uni_robot_agent/integrations/robodojo/deployment.py +135 -0
  56. uni_robot_agent/integrations/robodojo/deployment.toml +14 -0
  57. uni_robot_agent/integrations/robodojo/environment/__init__.py +1 -0
  58. uni_robot_agent/integrations/robodojo/environment/adapter.py +435 -0
  59. uni_robot_agent/integrations/robodojo/environment/camera_projection.py +77 -0
  60. uni_robot_agent/integrations/robodojo/environment/geometry.py +228 -0
  61. uni_robot_agent/integrations/robodojo/environment/gripper_geometry.py +82 -0
  62. uni_robot_agent/integrations/robodojo/environment/ik/__init__.py +1 -0
  63. uni_robot_agent/integrations/robodojo/environment/ik/curobo.py +175 -0
  64. uni_robot_agent/integrations/robodojo/environment/ik/goal_region.py +106 -0
  65. uni_robot_agent/integrations/robodojo/environment/ik/recovery.py +129 -0
  66. uni_robot_agent/integrations/robodojo/environment/kinematics.py +290 -0
  67. uni_robot_agent/integrations/robodojo/environment/native_compat.py +155 -0
  68. uni_robot_agent/integrations/robodojo/environment/robot_geometry.py +72 -0
  69. uni_robot_agent/integrations/robodojo/environment/server.py +202 -0
  70. uni_robot_agent/integrations/robodojo/environment/x5_grasp_site.urdf +13 -0
  71. uni_robot_agent/integrations/robodojo/prompt.py +56 -0
  72. uni_robot_agent/integrations/robolab/SOURCE.md +15 -0
  73. uni_robot_agent/integrations/robolab/__init__.py +39 -0
  74. uni_robot_agent/integrations/robolab/environment/__init__.py +1 -0
  75. uni_robot_agent/integrations/robolab/environment/adapter.py +254 -0
  76. uni_robot_agent/integrations/robolab/environment/controller.py +61 -0
  77. uni_robot_agent/integrations/robolab/environment/drives.py +25 -0
  78. uni_robot_agent/integrations/robolab/environment/executor.py +47 -0
  79. uni_robot_agent/integrations/robolab/environment/gripper.py +63 -0
  80. uni_robot_agent/integrations/robolab/environment/kinematics.py +213 -0
  81. uni_robot_agent/integrations/robolab/environment/native_compat.py +62 -0
  82. uni_robot_agent/integrations/robolab/environment/server.py +187 -0
  83. uni_robot_agent/integrations/robolab/environment/video.py +75 -0
  84. uni_robot_agent/integrations/robolab/prompt.py +24 -0
  85. uni_robot_agent/integrations/robolab/session.py +42 -0
  86. uni_robot_agent/integrations/robouse/__init__.py +19 -0
  87. uni_robot_agent/integrations/robouse/deployment.py +174 -0
  88. uni_robot_agent/integrations/robouse/environment/__init__.py +1 -0
  89. uni_robot_agent/integrations/robouse/environment/adapter.py +106 -0
  90. uni_robot_agent/integrations/robouse/environment/backends/__init__.py +16 -0
  91. uni_robot_agent/integrations/robouse/environment/backends/bimanual.py +55 -0
  92. uni_robot_agent/integrations/robouse/environment/backends/cartesian.py +85 -0
  93. uni_robot_agent/integrations/robouse/environment/backends/osc.py +77 -0
  94. uni_robot_agent/integrations/robouse/environment/backends/worker_backend.py +69 -0
  95. uni_robot_agent/integrations/robouse/environment/backends/worker_control.py +85 -0
  96. uni_robot_agent/integrations/robouse/environment/control.py +34 -0
  97. uni_robot_agent/integrations/robouse/environment/geometry.py +75 -0
  98. uni_robot_agent/integrations/robouse/environment/native_worker.py +134 -0
  99. uni_robot_agent/integrations/robouse/environment/server.py +136 -0
  100. uni_robot_agent/integrations/robouse/environment/worker_cameras.py +65 -0
  101. uni_robot_agent/integrations/robouse/environment/worker_frames.py +28 -0
  102. uni_robot_agent/integrations/robouse/prompt.py +22 -0
  103. uni_robot_agent/integrations/robouse/recipes/base.toml +1 -0
  104. uni_robot_agent/integrations/robouse/recipes/factory.toml +9 -0
  105. uni_robot_agent/integrations/robouse/recipes/genesis.toml +6 -0
  106. uni_robot_agent/integrations/robouse/recipes/libero.toml +8 -0
  107. uni_robot_agent/integrations/robouse/recipes/maniskill_cpu.toml +6 -0
  108. uni_robot_agent/integrations/robouse/recipes/maniskill_gpu.toml +6 -0
  109. uni_robot_agent/integrations/robouse/recipes/mujoco.toml +6 -0
  110. uni_robot_agent/integrations/robouse/recipes/robocasa.toml +28 -0
  111. uni_robot_agent/integrations/robouse/tasks.py +73 -0
  112. uni_robot_agent/integrations/robouse/worker_tasks.py +66 -0
  113. uni_robot_agent/observation/__init__.py +36 -0
  114. uni_robot_agent/observation/episode.py +107 -0
  115. uni_robot_agent/observation/hub.py +432 -0
  116. uni_robot_agent/provider/__init__.py +19 -0
  117. uni_robot_agent/provider/backends/__init__.py +1 -0
  118. uni_robot_agent/provider/backends/aliyun/__init__.py +1 -0
  119. uni_robot_agent/provider/backends/aliyun/chat_completions.py +37 -0
  120. uni_robot_agent/provider/backends/aliyun/messages.py +56 -0
  121. uni_robot_agent/provider/backends/deepseek/__init__.py +1 -0
  122. uni_robot_agent/provider/backends/deepseek/chat_completions.py +35 -0
  123. uni_robot_agent/provider/backends/deepseek/file_broker.py +358 -0
  124. uni_robot_agent/provider/backends/deepseek/files.py +256 -0
  125. uni_robot_agent/provider/backends/minimax/__init__.py +1 -0
  126. uni_robot_agent/provider/backends/minimax/chat_completions.py +42 -0
  127. uni_robot_agent/provider/backends/moonshot/__init__.py +1 -0
  128. uni_robot_agent/provider/backends/moonshot/chat_completions.py +34 -0
  129. uni_robot_agent/provider/backends/passthrough.py +46 -0
  130. uni_robot_agent/provider/backends/volcengine/__init__.py +1 -0
  131. uni_robot_agent/provider/backends/volcengine/responses.py +68 -0
  132. uni_robot_agent/provider/client.py +285 -0
  133. uni_robot_agent/provider/contracts.py +124 -0
  134. uni_robot_agent/provider/conversions/__init__.py +1 -0
  135. uni_robot_agent/provider/conversions/responses_chat.py +411 -0
  136. uni_robot_agent/provider/conversions/tool_names.py +154 -0
  137. uni_robot_agent/provider/gateway.py +424 -0
  138. uni_robot_agent/provider/image_encoding.py +20 -0
  139. uni_robot_agent/provider/protocols/__init__.py +1 -0
  140. uni_robot_agent/provider/protocols/chat_completions.py +94 -0
  141. uni_robot_agent/provider/protocols/messages.py +138 -0
  142. uni_robot_agent/provider/protocols/messages_keepalive.py +73 -0
  143. uni_robot_agent/provider/protocols/responses.py +144 -0
  144. uni_robot_agent/provider/protocols/sse.py +35 -0
  145. uni_robot_agent/provider/recording/__init__.py +1 -0
  146. uni_robot_agent/provider/recording/exchanges.py +469 -0
  147. uni_robot_agent/provider/recording/images.py +43 -0
  148. uni_robot_agent/provider/registry.py +108 -0
  149. uni_robot_agent/py.typed +0 -0
  150. uni_robot_agent/resources/__init__.py +1 -0
  151. uni_robot_agent/resources/prompts/actor/robodojo_vla_decision.md +85 -0
  152. uni_robot_agent/resources/prompts/guides/meta_skill.md +33 -0
  153. uni_robot_agent/resources/prompts/observer/main_agent.md +28 -0
  154. uni_robot_agent/resources/prompts/observer/observer.md +37 -0
  155. uni_robot_agent/resources/prompts/observer/robodojo_main.md +12 -0
  156. uni_robot_agent/resources/prompts/robodojo/hybrid.md +17 -0
  157. uni_robot_agent/resources/prompts/robodojo/instruction_supplements.json +198 -0
  158. uni_robot_agent/resources/prompts/robodojo/system.md +143 -0
  159. uni_robot_agent/resources/prompts/robodojo/task_requirements.json +213 -0
  160. uni_robot_agent/resources/prompts/robodojo/user.md +2 -0
  161. uni_robot_agent/resources/prompts/runtime/batch.md +15 -0
  162. uni_robot_agent/resources/prompts/runtime/common.md +55 -0
  163. uni_robot_agent/resources/prompts/runtime/hybrid.md +27 -0
  164. uni_robot_agent/resources/prompts/runtime/resource_global-serial.md +1 -0
  165. uni_robot_agent/resources/prompts/runtime/resource_per-resource.md +1 -0
  166. uni_robot_agent/resources/prompts/runtime/return_background.md +21 -0
  167. uni_robot_agent/resources/prompts/runtime/return_immediate.md +1 -0
  168. uni_robot_agent/resources/prompts/runtime/storage_access.md +11 -0
  169. uni_robot_agent/resources/skills/general/SKILL.md +222 -0
  170. uni_robot_agent/resources/skills/general/gripper/contact-effect/SKILL.md +104 -0
  171. uni_robot_agent/resources/skills/general/gripper/contact-effect/references/exact-count-cycles.md +26 -0
  172. uni_robot_agent/resources/skills/general/gripper/contents-transfer/SKILL.md +38 -0
  173. uni_robot_agent/resources/skills/general/gripper/deformable-material/SKILL.md +34 -0
  174. uni_robot_agent/resources/skills/general/gripper/grasp-geometry/SKILL.md +122 -0
  175. uni_robot_agent/resources/skills/general/gripper/grasp-geometry/references/contact-calculation.md +147 -0
  176. uni_robot_agent/resources/skills/general/gripper/grasp-geometry/references/shape-based-grasping.md +104 -0
  177. uni_robot_agent/resources/skills/general/gripper/handover/SKILL.md +75 -0
  178. uni_robot_agent/resources/skills/general/gripper/held-object-geometry/SKILL.md +58 -0
  179. uni_robot_agent/resources/skills/general/gripper/motion-recovery/SKILL.md +123 -0
  180. uni_robot_agent/resources/skills/general/gripper/moving-target-control/SKILL.md +42 -0
  181. uni_robot_agent/resources/skills/general/gripper/placement-and-engagement/SKILL.md +111 -0
  182. uni_robot_agent/resources/skills/general/gripper/pose-targeting/SKILL.md +110 -0
  183. uni_robot_agent/resources/skills/general/gripper/reachability-and-recovery/SKILL.md +76 -0
  184. uni_robot_agent/resources/skills/general/gripper/return-to-start/SKILL.md +57 -0
  185. uni_robot_agent/resources/skills/general/gripper/scene-memory/SKILL.md +91 -0
  186. uni_robot_agent/resources/skills/general/gripper/scene-memory/references/pose-and-order-memory.md +26 -0
  187. uni_robot_agent/resources/skills/general/gripper/state-verification/SKILL.md +106 -0
  188. uni_robot_agent/resources/skills/general/gripper/supported-contact/SKILL.md +100 -0
  189. uni_robot_agent/resources/skills/general/gripper/tool-selection/SKILL.md +181 -0
  190. uni_robot_agent/resources/skills/general/gripper/tool-selection/references/depth-grounding.md +47 -0
  191. uni_robot_agent/resources/skills/general/gripper/tool-selection/references/rgb-grounding.md +58 -0
  192. uni_robot_agent/resources/skills/general/index.md +65 -0
  193. uni_robot_agent/resources/skills/robodojo/gripper/motion-control/SKILL.md +95 -0
  194. uni_robot_agent/resources/skills/robodojo/gripper/return-to-start/SKILL.md +19 -0
  195. uni_robot_agent/resources/skills/robodojo/index.md +26 -0
  196. uni_robot_agent/resources/skills/robodojo/perception-reference.md +64 -0
  197. uni_robot_agent/resources/skills/robodojo/robot-reference.md +126 -0
  198. uni_robot_agent/services/__init__.py +0 -0
  199. uni_robot_agent/services/actor/__init__.py +0 -0
  200. uni_robot_agent/services/actor/backend.py +24 -0
  201. uni_robot_agent/services/actor/backends/__init__.py +0 -0
  202. uni_robot_agent/services/actor/backends/pi05/__init__.py +0 -0
  203. uni_robot_agent/services/actor/backends/pi05/backend.py +46 -0
  204. uni_robot_agent/services/actor/backends/pi05/config.py +41 -0
  205. uni_robot_agent/services/actor/backends/pi05/openpi_jax.py +227 -0
  206. uni_robot_agent/services/actor/client.py +187 -0
  207. uni_robot_agent/services/actor/contracts.py +95 -0
  208. uni_robot_agent/services/actor/deployment.py +80 -0
  209. uni_robot_agent/services/actor/images.py +28 -0
  210. uni_robot_agent/services/actor/recipes/robodojo_pi05.toml +8 -0
  211. uni_robot_agent/services/actor/routes.py +50 -0
  212. uni_robot_agent/services/actor/server.py +126 -0
  213. uni_robot_agent/services/actor/service.py +303 -0
  214. uni_robot_agent/services/batching.py +165 -0
  215. uni_robot_agent/services/deployment/__init__.py +4 -0
  216. uni_robot_agent/services/deployment/config.py +214 -0
  217. uni_robot_agent/services/deployment/manager.py +181 -0
  218. uni_robot_agent/services/deployment/process.py +185 -0
  219. uni_robot_agent/services/deployment/runtime.py +106 -0
  220. uni_robot_agent/services/deployment/setup.py +225 -0
  221. uni_robot_agent/services/environment/__init__.py +0 -0
  222. uni_robot_agent/services/environment/backend.py +48 -0
  223. uni_robot_agent/services/environment/client.py +272 -0
  224. uni_robot_agent/services/environment/contracts.py +221 -0
  225. uni_robot_agent/services/environment/device_contracts.py +150 -0
  226. uni_robot_agent/services/environment/interrupts.py +51 -0
  227. uni_robot_agent/services/environment/primitive.py +54 -0
  228. uni_robot_agent/services/environment/routes.py +164 -0
  229. uni_robot_agent/services/environment/service.py +389 -0
  230. uni_robot_agent/services/environment/snapshots.py +158 -0
  231. uni_robot_agent/services/errors.py +19 -0
  232. uni_robot_agent/services/hosting.py +251 -0
  233. uni_robot_agent/services/http.py +72 -0
  234. uni_robot_agent/services/mcp/__init__.py +1 -0
  235. uni_robot_agent/services/mcp/http.py +139 -0
  236. uni_robot_agent/services/mcp/results.py +40 -0
  237. uni_robot_agent/services/perception/__init__.py +0 -0
  238. uni_robot_agent/services/perception/backend.py +67 -0
  239. uni_robot_agent/services/perception/backends/__init__.py +0 -0
  240. uni_robot_agent/services/perception/backends/sam3.py +400 -0
  241. uni_robot_agent/services/perception/client.py +39 -0
  242. uni_robot_agent/services/perception/codec.py +83 -0
  243. uni_robot_agent/services/perception/contracts.py +79 -0
  244. uni_robot_agent/services/perception/deployment.py +56 -0
  245. uni_robot_agent/services/perception/recipes/sam3.toml +7 -0
  246. uni_robot_agent/services/perception/routes.py +43 -0
  247. uni_robot_agent/services/perception/server.py +102 -0
  248. uni_robot_agent/services/perception/service.py +62 -0
  249. uni_robot_agent/session.py +851 -0
  250. uni_robot_agent/telemetry/__init__.py +22 -0
  251. uni_robot_agent/telemetry/logging.py +18 -0
  252. uni_robot_agent/telemetry/model.py +47 -0
  253. uni_robot_agent/telemetry/session.py +66 -0
  254. uni_robot_agent/telemetry/sinks.py +146 -0
  255. uni_robot_agent/telemetry/tracer.py +154 -0
  256. uni_robot_agent/tools/__init__.py +21 -0
  257. uni_robot_agent/tools/client.py +60 -0
  258. uni_robot_agent/tools/episode.py +36 -0
  259. uni_robot_agent/tools/gripper/__init__.py +4 -0
  260. uni_robot_agent/tools/gripper/actor.py +126 -0
  261. uni_robot_agent/tools/gripper/contracts.py +78 -0
  262. uni_robot_agent/tools/gripper/motion.py +536 -0
  263. uni_robot_agent/tools/gripper/opening.py +62 -0
  264. uni_robot_agent/tools/gripper/pose.py +130 -0
  265. uni_robot_agent/tools/gripper/provider.py +298 -0
  266. uni_robot_agent/tools/gripper/reviewer.py +88 -0
  267. uni_robot_agent/tools/jobs.py +249 -0
  268. uni_robot_agent/tools/observation.py +156 -0
  269. uni_robot_agent/tools/presentation.py +39 -0
  270. uni_robot_agent/tools/scene/__init__.py +1 -0
  271. uni_robot_agent/tools/scene/geometry.py +149 -0
  272. uni_robot_agent/tools/scene/perception.py +190 -0
  273. uni_robot_agent/tools/scene/provider.py +173 -0
  274. uni_robot_agent/tools/scene/triangulation.py +71 -0
  275. uni_robot_agent/tools/selection.py +56 -0
  276. uni_robot_agent/tools/skills.py +137 -0
  277. uni_robot_agent/tools/toolkit.py +245 -0
  278. uni_robot_agent/tools/workspace.py +75 -0
  279. uni_robot_agent/ui/__init__.py +1 -0
  280. uni_robot_agent/ui/console.py +631 -0
  281. uni_robot_agent/ui/state.py +122 -0
  282. uni_robot_agent/ui/textual/__init__.py +1 -0
  283. uni_robot_agent/ui/textual/agent_panel.py +62 -0
  284. uni_robot_agent/ui/textual/app.py +292 -0
  285. uni_robot_agent/ui/textual/widgets.py +164 -0
  286. uni_robot_agent/ui/web/__init__.py +1 -0
  287. uni_robot_agent/ui/web/archive.py +80 -0
  288. uni_robot_agent/ui/web/media.py +55 -0
  289. uni_robot_agent/ui/web/reader.py +102 -0
  290. uni_robot_agent/ui/web/runtime.py +108 -0
  291. uni_robot_agent/ui/web/server.py +150 -0
  292. uni_robot_agent/ui/web/state.py +179 -0
  293. uni_robot_agent/ui/web/static/app.js +299 -0
  294. uni_robot_agent/ui/web/static/common.js +51 -0
  295. uni_robot_agent/ui/web/static/console.js +91 -0
  296. uni_robot_agent/ui/web/static/frames.js +17 -0
  297. uni_robot_agent/ui/web/static/index.html +181 -0
  298. uni_robot_agent/ui/web/static/metrics.js +90 -0
  299. uni_robot_agent/ui/web/static/motion.js +18 -0
  300. uni_robot_agent/ui/web/static/playback.js +28 -0
  301. uni_robot_agent/ui/web/static/styles.css +1018 -0
  302. uni_robot_agent/ui/web/static/timeline.js +84 -0
  303. uni_robot_agent/ui/web/static/viewer.js +128 -0
  304. uni_robot_agent_runtime-0.1.1.dist-info/METADATA +47 -0
  305. uni_robot_agent_runtime-0.1.1.dist-info/RECORD +309 -0
  306. uni_robot_agent_runtime-0.1.1.dist-info/WHEEL +5 -0
  307. uni_robot_agent_runtime-0.1.1.dist-info/entry_points.txt +2 -0
  308. uni_robot_agent_runtime-0.1.1.dist-info/licenses/LICENSE +21 -0
  309. uni_robot_agent_runtime-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,50 @@
1
+ """uni-robot-agent — A robot agent built on Codex and Claude Code."""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+
5
+ def _installed_version():
6
+ installed = {}
7
+ for name in ("uni-robot-agent", "uni-robot-agent-runtime"):
8
+ try:
9
+ installed[name] = version(name)
10
+ except PackageNotFoundError:
11
+ pass
12
+ if len(installed) > 1:
13
+ raise RuntimeError(
14
+ "uni-robot-agent and uni-robot-agent-runtime share module files and must be "
15
+ "installed in separate environments. Recreate this environment with only one package."
16
+ )
17
+ return next(iter(installed.values()), "unknown (not installed)")
18
+
19
+
20
+ __version__ = _installed_version()
21
+
22
+
23
+ # Keep version/help and record readers independent of planner and native runtimes.
24
+ _EXPORTS = {
25
+ "SessionConfig": ("config", "SessionConfig"),
26
+ "AgentConfig": ("config", "AgentConfig"),
27
+ "Session": ("session", "Session"),
28
+ "run": ("session", "run_session"),
29
+ "ToolClient": ("tools.client", "ToolClient"),
30
+ "ToolJob": ("tools.client", "ToolJob"),
31
+ "ToolOutput": ("tools.toolkit", "ToolOutput"),
32
+ "RunResult": ("artifacts.records", "RunResult"),
33
+ "RunRecord": ("artifacts.records", "RunRecord"),
34
+ "SetupConfig": ("services.deployment.config", "SetupConfig"),
35
+ "DeploymentConfig": ("services.deployment.config", "DeploymentConfig"),
36
+ "Deployment": ("services.deployment.manager", "Deployment"),
37
+ "setup": ("services.deployment.setup", "setup"),
38
+ "deploy": ("services.deployment.manager", "start_deployment"),
39
+ }
40
+ __all__ = ["__version__", *_EXPORTS]
41
+
42
+
43
+ def __getattr__(name):
44
+ if name not in _EXPORTS:
45
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
46
+ from importlib import import_module
47
+ module, attribute = _EXPORTS[name]
48
+ value = getattr(import_module(f"{__name__}.{module}"), attribute)
49
+ globals()[name] = value
50
+ return value
@@ -0,0 +1,5 @@
1
+ """Run uni-robot-agent from Python."""
2
+ from .cli import main
3
+
4
+ if __name__ == "__main__":
5
+ main()
@@ -0,0 +1,19 @@
1
+ """Agent contracts and lazy backend registration."""
2
+
3
+ from .contracts import Agent, AgentCapabilities, AgentResult
4
+ from .registry import (
5
+ create_agent,
6
+ get_agent_backend,
7
+ list_agent_backends,
8
+ register_agent,
9
+ )
10
+
11
+ __all__ = [
12
+ "Agent",
13
+ "AgentCapabilities",
14
+ "AgentResult",
15
+ "create_agent",
16
+ "get_agent_backend",
17
+ "list_agent_backends",
18
+ "register_agent",
19
+ ]
@@ -0,0 +1,2 @@
1
+ """Claude Code agent plugin."""
2
+ from .agent import ClaudeCodeAgent
@@ -0,0 +1,163 @@
1
+ """Claude Code SDK adapter; model transport and robot execution stay in uni-robot-agent."""
2
+ from __future__ import annotations
3
+ import asyncio
4
+ from contextlib import aclosing
5
+ import os
6
+ from pathlib import Path
7
+ import time
8
+ from urllib.parse import urlsplit
9
+
10
+ from uni_robot_agent.events import AgentRunStarted
11
+ from uni_robot_agent.agents.contracts import AgentCapabilities, AgentResult
12
+ from uni_robot_agent.agents.runtime import ModelRuntime
13
+ from uni_robot_agent.provider.protocols.messages import MessagesProtocol
14
+ from uni_robot_agent.provider.registry import build_provider_adapter
15
+ from .artifacts import finalize_model_records
16
+ from .config import ClaudeConfig
17
+ from .events import ClaudeEvents
18
+ from .model_binding import ClaudeModelBinding
19
+ from .sdk_compat import mcp_description_env, context_env
20
+ from .tools import ClaudeTools
21
+
22
+
23
+ # Keep native computation and file access without unrelated coding-agent workflows.
24
+ NATIVE_TOOLS = ("Bash", "Read", "Write", "Edit", "Glob", "Grep")
25
+
26
+
27
+ class ClaudeCodeAgent(ClaudeConfig):
28
+ capabilities = AgentCapabilities()
29
+
30
+ def __init__(self, *, model="sonnet", timeout_s=None, cwd=None, api_key=None,
31
+ max_budget_usd=None, effort=None):
32
+ self._model, self._cwd, self._api_key = model, cwd, api_key
33
+ self._budget, self._effort = max_budget_usd, effort
34
+ self._deadline = None if timeout_s is None else time.monotonic() + timeout_s
35
+ self._loop = self._task = None
36
+ self._cancel_reason = None
37
+ self._cleanup_error = None
38
+ self._binding = ClaudeModelBinding()
39
+
40
+ def model_runtime(self, profile, event_bus=None):
41
+ adapter = build_provider_adapter(profile, client_protocol=self.model_protocol)
42
+ self._binding = ClaudeModelBinding(event_bus)
43
+ return ModelRuntime("claude_messages", adapter, MessagesProtocol(keepalive_interval_s=60),
44
+ self._binding, finalize_model_records)
45
+
46
+ def finalize_without_provider(self, result, session_dir):
47
+ if session_dir is not None:
48
+ result.train_error = "Claude native model records require --provider-url"
49
+
50
+ def cancel(self, reason):
51
+ self._cancel_reason = reason
52
+ self._binding.cancel()
53
+ if self._loop is not None and self._task is not None and not self._task.done():
54
+ self._loop.call_soon_threadsafe(self._task.cancel)
55
+
56
+ def close(self):
57
+ if self._cleanup_error:
58
+ raise RuntimeError(self._cleanup_error)
59
+ if self._task is not None and not self._task.done():
60
+ self.cancel("cancelled")
61
+ raise RuntimeError("Claude SDK run has not finished cleanup")
62
+
63
+ def run(self, *, prompt, task, toolkit, executor, max_turns=0,
64
+ event_bus=None, session_dir=None, model_endpoint=None):
65
+ try:
66
+ return asyncio.run(self._run_async(prompt, task, executor, max_turns,
67
+ event_bus, session_dir, model_endpoint))
68
+ finally:
69
+ self._loop = self._task = None
70
+
71
+ async def _run_async(self, prompt, task, executor, max_turns, bus, session_dir, model_endpoint):
72
+ import claude_agent_sdk as sdk
73
+ self._loop, self._task = asyncio.get_running_loop(), asyncio.current_task()
74
+ self._cancel_reason = None
75
+ tools = ClaudeTools(executor, self._binding, timeout_s=(
76
+ None if self._deadline is None else max(0, self._deadline - time.monotonic())
77
+ ))
78
+ state = ClaudeEvents(bus, self._binding)
79
+ started = time.monotonic()
80
+ cleanup_error = None
81
+ remaining = None if self._deadline is None else max(0, self._deadline - started)
82
+ deadline = asyncio.timeout(remaining)
83
+ try:
84
+ env = {
85
+ "CLAUDE_CODE_USE_BEDROCK": "0", "CLAUDE_CODE_USE_VERTEX": "0",
86
+ "CLAUDE_CODE_USE_FOUNDRY": "0", "CLAUDE_CODE_OAUTH_TOKEN": "",
87
+ "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC": "1",
88
+ # uni-robot-agent retries a complete buffered exchange, before tool delivery.
89
+ "CLAUDE_CODE_MAX_RETRIES": "0",
90
+ # Keep native helpers and subagents on the explicitly selected provider model.
91
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL": self._model,
92
+ "ANTHROPIC_DEFAULT_SONNET_MODEL": self._model,
93
+ "ANTHROPIC_DEFAULT_OPUS_MODEL": self._model,
94
+ "CLAUDE_CODE_SUBAGENT_MODEL": self._model,
95
+ **mcp_description_env(tools.specs.values()),
96
+ **context_env(),
97
+ }
98
+ if model_endpoint:
99
+ env["ANTHROPIC_BASE_URL"] = model_endpoint.rstrip("/").removesuffix("/v1")
100
+ host = urlsplit(model_endpoint).hostname
101
+ bypass = ",".join(filter(None, [os.environ.get("NO_PROXY") or os.environ.get("no_proxy"), host]))
102
+ env.update(NO_PROXY=bypass, no_proxy=bypass)
103
+ token = self._api_key or os.environ.get("ANTHROPIC_AUTH_TOKEN")
104
+ if token:
105
+ env.update(ANTHROPIC_AUTH_TOKEN=token, ANTHROPIC_API_KEY="")
106
+ if session_dir is not None:
107
+ config_dir = Path(session_dir) / ".claude"
108
+ config_dir.mkdir(parents=True, exist_ok=True)
109
+ env["CLAUDE_CONFIG_DIR"] = str(config_dir.resolve())
110
+ options = sdk.ClaudeAgentOptions(
111
+ model=self._model, system_prompt=prompt.system,
112
+ max_turns=max_turns or None, max_budget_usd=self._budget,
113
+ max_buffer_size=64 * 1024 * 1024,
114
+ forward_subagent_text=True,
115
+ tools=list(NATIVE_TOOLS),
116
+ allowed_tools=["mcp__uni_robot_agent__" + name for name in tools.specs],
117
+ mcp_servers={"uni_robot_agent": tools.server()}, strict_mcp_config=True,
118
+ # The hook authorizes native tools, including access outside cwd.
119
+ # Root sessions use hook decisions rather than bypassPermissions.
120
+ permission_mode="dontAsk", sandbox={"enabled": False},
121
+ setting_sources=[], skills=[], cwd=self._cwd,
122
+ env=env, effort=self._effort,
123
+ hooks={
124
+ "PreToolUse": [sdk.HookMatcher(hooks=[tools.pre_tool])],
125
+ "PostToolUse": [sdk.HookMatcher(hooks=[tools.tool_completed])],
126
+ "PostToolUseFailure": [sdk.HookMatcher(hooks=[tools.tool_failed])],
127
+ "SubagentStart": [sdk.HookMatcher(hooks=[tools.subagent_event])],
128
+ "SubagentStop": [sdk.HookMatcher(hooks=[tools.subagent_event])],
129
+ },
130
+ )
131
+ if bus:
132
+ bus.emit(AgentRunStarted(instruction=task, model=self._model,
133
+ agent_backend="claude_code", tool_event_source="execution"))
134
+ async with deadline:
135
+ async with aclosing(sdk.query(prompt=prompt.user, options=options)) as messages:
136
+ async for message in messages:
137
+ state.consume(message)
138
+ except asyncio.CancelledError:
139
+ state.stop_reason = self._cancel_reason or "cancelled"
140
+ except TimeoutError as error:
141
+ if deadline.expired():
142
+ state.stop_reason = "wall_time_limit"
143
+ else:
144
+ state.error = f"TimeoutError: {error}"
145
+ except Exception as error:
146
+ if not (state.stop_reason == "max_turns" and type(error).__name__ == "ResultError"):
147
+ state.error = f"{type(error).__name__}: {error}"
148
+ finally:
149
+ try:
150
+ await tools.close()
151
+ except Exception as error:
152
+ cleanup_error = self._cleanup_error = str(error)
153
+ state.error = state.error or cleanup_error
154
+ finish = state.finish_request
155
+ message = state.error or ((finish.get("summary") or finish.get("message")) if finish else None)
156
+ message = message or "\n".join(state.texts) or "Claude session ended without output"
157
+ return AgentResult(
158
+ success=finish is not None and state.completed and not state.error and not state.stop_reason,
159
+ message=message, error=state.error, cleanup_error=cleanup_error,
160
+ stop_reason=state.stop_reason, turns_used=state.turns, tool_calls=state.tool_calls,
161
+ steps_used=state.steps, elapsed_s=time.monotonic() - started,
162
+ main_thread_id=self._binding.thread_id, trace_format="claude_messages",
163
+ )
@@ -0,0 +1,29 @@
1
+ """Finalize Claude native model records without claiming Codex SFT compatibility."""
2
+ from pathlib import Path
3
+
4
+ from uni_robot_agent.events import ErrorEvent
5
+ from uni_robot_agent.provider.recording.exchanges import get_model_exchange_error
6
+
7
+
8
+ def finalize_model_records(gateway, result, session_dir, event_bus):
9
+ records = gateway.trace_records
10
+ result.provider_error = get_model_exchange_error(
11
+ records, interrupted=result.stop_reason in {"max_turns", "wall_time_limit", "cancelled"},
12
+ )
13
+ if result.provider_error:
14
+ result.error = result.error or result.provider_error
15
+ if event_bus:
16
+ event_bus.emit(ErrorEvent(message=result.provider_error))
17
+ if session_dir is not None:
18
+ result.provider_log_path = str(Path(session_dir) / "claude_messages")
19
+ result.train_error = "Claude native model exchanges are archived; provider-faithful SFT export is not implemented"
20
+ gateway.persist_report({
21
+ "trace_format": "claude_messages", "records_frozen": True,
22
+ "exchange_count": len(records), "provider_error": result.provider_error,
23
+ "agent_error": result.error, "stop_reason": result.stop_reason,
24
+ "main_thread_id": result.main_thread_id, "train_error": result.train_error,
25
+ "exchange_persistence_errors": [
26
+ {"index": item["index"], "error": item["persistence_error"]}
27
+ for item in records if item.get("persistence_error")
28
+ ],
29
+ })
@@ -0,0 +1,50 @@
1
+ """Claude-only settings and validation of supported model services."""
2
+ import os
3
+ import argparse
4
+ import math
5
+ from urllib.parse import urlsplit
6
+
7
+
8
+ class ClaudeConfig:
9
+ model_protocol = "messages"
10
+
11
+ @classmethod
12
+ def add_cli_arguments(cls, parser):
13
+ group = parser.add_argument_group("Claude Code SDK")
14
+ group.add_argument("--claude-max-budget-usd", type=float, default=argparse.SUPPRESS,
15
+ help="Optional SDK cost ceiling; unset disables the SDK budget cap")
16
+ group.add_argument("--claude-effort", choices=("low", "medium", "high", "max"), default=argparse.SUPPRESS)
17
+
18
+ @classmethod
19
+ def resolve_defaults(cls, args):
20
+ args.claude_max_budget_usd = getattr(args, "claude_max_budget_usd", None)
21
+ args.claude_effort = getattr(args, "claude_effort", None)
22
+ if args.reasoning_effort is None:
23
+ args.reasoning_effort = "none"
24
+ if args.provider_url is None:
25
+ from uni_robot_agent.config import runtime_env
26
+ args.provider_url = runtime_env("PROVIDER_URL") or os.environ.get("ANTHROPIC_BASE_URL")
27
+
28
+ @classmethod
29
+ def validate_config(cls, args, explicit):
30
+ if "--codex-base-prompt" in explicit:
31
+ raise ValueError("--codex-base-prompt requires --agent codex")
32
+ if explicit & {"--mcp-parallel-dispatch", "--no-mcp-parallel-dispatch", "--mcp-timeout-s"}:
33
+ raise ValueError("Codex MCP dispatch options require --agent codex")
34
+ if args.reasoning_effort != "none":
35
+ raise ValueError("Use --claude-effort for Claude Code reasoning configuration")
36
+ if args.context_policy != "identity":
37
+ raise ValueError("Claude Code currently supports only identity context policy")
38
+ if not args.provider_url:
39
+ raise ValueError("Claude Code requires --provider-url or ANTHROPIC_BASE_URL")
40
+ if urlsplit(args.provider_url).scheme not in {"http", "https"}:
41
+ raise ValueError("Claude Messages provider URL must use HTTP or HTTPS")
42
+ if args.claude_max_budget_usd is not None and (
43
+ not math.isfinite(args.claude_max_budget_usd) or args.claude_max_budget_usd <= 0
44
+ ):
45
+ raise ValueError("--claude-max-budget-usd must be positive")
46
+
47
+ @classmethod
48
+ def config_options(cls, args):
49
+ return {"api_key": args._api_key, "max_budget_usd": args.claude_max_budget_usd,
50
+ "effort": args.claude_effort}
@@ -0,0 +1,121 @@
1
+ """Claude SDK messages projected into uni-robot-agent events and terminal state."""
2
+ import json
3
+ from uni_robot_agent.events import AssistantText, ToolCall, ReasoningSummary, AgentActivity
4
+
5
+
6
+ def get(value, key, default=None):
7
+ return value.get(key, default) if isinstance(value, dict) else getattr(value, key, default)
8
+
9
+
10
+ def kind(value):
11
+ return str(value.get("type") or value.get("kind") or "") if isinstance(value, dict) else type(value).__name__
12
+
13
+
14
+ def finish_succeeded(block):
15
+ if get(block, "is_error", False):
16
+ return False
17
+ for content in get(block, "content", []) or []:
18
+ if get(content, "type") != "text":
19
+ continue
20
+ try:
21
+ value = json.loads(get(content, "text", ""))
22
+ except ValueError:
23
+ continue
24
+ if isinstance(value, dict) and "call_ok" in value:
25
+ return value["call_ok"] is True and not value.get("error")
26
+ return False
27
+
28
+
29
+ class ClaudeEvents:
30
+ def __init__(self, bus, binding):
31
+ self.bus, self.binding = bus, binding
32
+ self.turns = self.tool_calls = self.steps = 0
33
+ self.texts = []
34
+ self.seen_messages = set()
35
+ self.seen_calls = set()
36
+ self.finish_calls = {}
37
+ self.finish_request = None
38
+ self.completed = False
39
+ self.error = self.stop_reason = None
40
+ self.session_id = None
41
+
42
+ def emit(self, event):
43
+ if self.bus:
44
+ self.bus.emit(event)
45
+
46
+ def consume(self, message):
47
+ message_kind = kind(message)
48
+ parent_call = get(message, "parent_tool_use_id")
49
+ if parent_call:
50
+ # Tool hooks carry the authoritative child ID. Keep child prose
51
+ # separate from the main agent's completion and accounting.
52
+ agent_id, parent_agent_id = self.binding.child_identity(parent_call)
53
+ for block in get(message, "content", []) or []:
54
+ if kind(block) == "TextBlock" and get(block, "text"):
55
+ self.emit(AgentActivity(kind="message", item_id=parent_call,
56
+ agent_id=agent_id, parent_agent_id=parent_agent_id,
57
+ label="subagent", text=get(block, "text")))
58
+ return
59
+ # TaskStartedMessage is a SystemMessage subclass with a distinct class name.
60
+ if get(message, "subtype") == "task_started":
61
+ data = get(message, "data", {})
62
+ agent_id = get(message, "task_id") or data.get("task_id")
63
+ parent_call = get(message, "tool_use_id") or data.get("tool_use_id")
64
+ if agent_id and parent_call:
65
+ self.binding.register_subagent(agent_id, parent_call)
66
+ return
67
+ if message_kind == "SystemMessage":
68
+ subtype = get(message, "subtype", "")
69
+ if subtype in {"init", "compact_boundary"}:
70
+ self.emit(AgentActivity(
71
+ kind="started" if subtype == "init" else "compacted",
72
+ agent_id=self.binding.thread_id, label="main",
73
+ text=json.dumps(get(message, "data", {}).get("compact_metadata", {})),
74
+ ))
75
+ return
76
+ if message_kind == "AssistantMessage":
77
+ identity = get(message, "message_id")
78
+ if identity is None or identity not in self.seen_messages:
79
+ self.turns += 1
80
+ if identity:
81
+ self.seen_messages.add(identity)
82
+ if get(message, "error"):
83
+ self.error = str(get(message, "error"))
84
+ for block in get(message, "content", []) or []:
85
+ if kind(block) == "TextBlock" and get(block, "text"):
86
+ self.texts.append(get(block, "text"))
87
+ self.steps += 1
88
+ self.emit(AssistantText(text=get(block, "text"), agent_step=self.steps, turn=self.turns))
89
+ elif kind(block) == "ThinkingBlock" and get(block, "thinking"):
90
+ self.emit(ReasoningSummary(text=get(block, "thinking")))
91
+ elif kind(block) == "ToolUseBlock":
92
+ call_id = get(block, "id")
93
+ if call_id in self.seen_calls:
94
+ continue
95
+ self.seen_calls.add(call_id)
96
+ self.tool_calls += 1
97
+ self.steps += 1
98
+ name, arguments = get(block, "name", ""), get(block, "input", {})
99
+ self.emit(ToolCall(tool_name=name, arguments=arguments, source_step=self.steps, turn=self.turns))
100
+ if name in {"finish", "mcp__uni_robot_agent__finish"} and isinstance(arguments, dict):
101
+ self.finish_calls[call_id] = arguments
102
+ elif message_kind == "UserMessage":
103
+ for block in get(message, "content", []) or []:
104
+ if kind(block) != "ToolResultBlock":
105
+ continue
106
+ call_id = get(block, "tool_use_id")
107
+ self.binding.tool_result(call_id, failed=get(block, "is_error", False))
108
+ arguments = self.finish_calls.pop(call_id, None)
109
+ if arguments is not None and finish_succeeded(block):
110
+ self.finish_request = arguments
111
+ elif message_kind == "ResultMessage":
112
+ self.session_id = get(message, "session_id")
113
+ subtype, reason = get(message, "subtype", ""), get(message, "terminal_reason")
114
+ self.completed = subtype == "success" and not get(message, "is_error", False) and reason in {None, "completed"}
115
+ if subtype == "error_max_turns":
116
+ self.stop_reason = "max_turns"
117
+ elif not self.completed:
118
+ if subtype == "error_max_budget_usd":
119
+ self.stop_reason = "budget_limit"
120
+ details = get(message, "errors") or [get(message, "result") or reason or subtype]
121
+ self.error = f"Claude Agent SDK ended ({subtype}): {'; '.join(str(x) for x in details)}"
@@ -0,0 +1,163 @@
1
+ """Associate delivered model batches with exact Claude tool-use identities."""
2
+ from __future__ import annotations
3
+
4
+ import threading
5
+ from dataclasses import dataclass
6
+ from uuid import uuid4
7
+
8
+ from uni_robot_agent.events import AgentActivity, ToolCallBatch
9
+ from uni_robot_agent.provider.contracts import PreparedReply, RequestContext
10
+ from uni_robot_agent.provider.protocols.messages import message_response
11
+
12
+
13
+ @dataclass
14
+ class Call:
15
+ name: str
16
+ arguments: dict
17
+ exchange_id: str
18
+ previous: str | None
19
+ delivered: bool = False
20
+ revoked: bool = False
21
+ claimed: bool = False
22
+ finished: bool = False
23
+ agent_id: str = ""
24
+ result_reported: bool = False
25
+
26
+
27
+ class ClaudeModelBinding:
28
+ def __init__(self, event_bus=None):
29
+ self.thread_id = str(uuid4())
30
+ self.bus = event_bus
31
+ self.calls: dict[str, Call] = {}
32
+ self.messages: set[str] = set()
33
+ self.lock = threading.RLock()
34
+ self.cancelled = False
35
+ self.tool_names = None
36
+ self.batches: dict[str, PreparedReply] = {}
37
+ self.observed_batches: set[str] = set()
38
+ self.child_agents: dict[str, str] = {}
39
+ self.agent_parents: dict[str, str] = {}
40
+
41
+ def request_context(self, request):
42
+ # Messages requests do not carry a reliable child identity. Attribute
43
+ # tool exchanges from SDK hook IDs instead of labelling all requests main.
44
+ return RequestContext(metadata={"thread_id": self.thread_id, "thread_source": "claude_session"})
45
+
46
+ def prepare_response(self, body, exchange_id, metadata):
47
+ message = message_response(body)
48
+ calls = tuple({"call_id": block["id"], "name": block["name"], "arguments": block["input"]}
49
+ for block in message.get("content", []) if block.get("type") == "tool_use")
50
+ with self.lock:
51
+ if any(call["call_id"] in self.calls for call in calls):
52
+ raise ValueError("Claude reused a tool-use ID across model exchanges")
53
+ previous = None
54
+ for call in calls:
55
+ self.calls[call["call_id"]] = Call(call["name"], call["arguments"], exchange_id, previous)
56
+ if self.tool_names is None or call["name"] in self.tool_names:
57
+ previous = call["call_id"]
58
+ return PreparedReply(body, exchange_id, {**metadata, "turn_id": message.get("id", exchange_id)}, calls)
59
+
60
+ def response_written(self, reply):
61
+ with self.lock:
62
+ self.messages.add(reply.metadata["turn_id"])
63
+ self.batches[reply.exchange_id] = reply
64
+ for call in reply.tool_calls:
65
+ self.calls[call["call_id"]].delivered = True
66
+
67
+ def observe_call(self, call_id, agent_id):
68
+ """Use the SDK's explicit owner, never timing or argument matching."""
69
+ with self.lock:
70
+ call = self.calls.get(call_id)
71
+ if self.cancelled or call is None or call.revoked:
72
+ raise RuntimeError("Tool call has no active model exchange")
73
+ # The SDK can receive the bytes before the gateway's delivery callback.
74
+ # Do not allow native tools or MCP callbacks until delivery is confirmed.
75
+ if not call.delivered:
76
+ return False
77
+ reply = self.batches.get(call.exchange_id)
78
+ first = call.exchange_id not in self.observed_batches
79
+ self.observed_batches.add(call.exchange_id)
80
+ for item in reply.tool_calls:
81
+ candidate = self.calls[item["call_id"]]
82
+ if candidate.agent_id and candidate.agent_id != agent_id:
83
+ raise ValueError("One Claude model response has conflicting tool owners")
84
+ candidate.agent_id = agent_id
85
+ parent = self.agent_parents.get(agent_id, "")
86
+ if self.bus:
87
+ if first:
88
+ names = tuple(item["name"] for item in reply.tool_calls)
89
+ self.bus.emit(AgentActivity(kind="tool_batch", agent_id=agent_id,
90
+ parent_agent_id=parent, turn_id=reply.metadata["turn_id"],
91
+ exchange_id=reply.exchange_id, tool_names=names, tool_calls=reply.tool_calls))
92
+ if agent_id == self.thread_id:
93
+ self.bus.emit(ToolCallBatch(batch_id=reply.exchange_id, tool_names=names, tool_calls=reply.tool_calls))
94
+ self.bus.emit(AgentActivity(kind="tool_started", agent_id=agent_id,
95
+ parent_agent_id=parent, item_id=call_id, exchange_id=call.exchange_id,
96
+ tool_names=(call.name,), tool_calls=({"call_id": call_id,
97
+ "name": call.name, "arguments": call.arguments},)))
98
+ return True
99
+
100
+ def register_subagent(self, agent_id, parent_call_id):
101
+ """Link the SDK task ID to its exact spawning tool, including nested agents."""
102
+ with self.lock:
103
+ call = self.calls.get(parent_call_id)
104
+ if call is None or call.name not in {"Agent", "Task"} or not call.agent_id:
105
+ return
106
+ self.child_agents[parent_call_id] = agent_id
107
+ self.agent_parents[agent_id] = call.agent_id
108
+ if self.bus:
109
+ self.bus.emit(AgentActivity(kind="started", agent_id=agent_id,
110
+ parent_agent_id=call.agent_id, item_id=parent_call_id, label="subagent"))
111
+
112
+ def child_identity(self, parent_call_id):
113
+ with self.lock:
114
+ call = self.calls.get(parent_call_id)
115
+ return self.child_agents.get(parent_call_id, ""), call.agent_id if call else ""
116
+
117
+ def tool_result(self, call_id, *, failed):
118
+ with self.lock:
119
+ call = self.calls.get(call_id)
120
+ if call is None:
121
+ return
122
+ # An SDK waiter ending does not stop an already claimed robot action.
123
+ if not call.claimed:
124
+ call.finished = True
125
+ if self.bus and not call.result_reported:
126
+ self.bus.emit(AgentActivity(kind="tool_failed" if failed else "tool_completed",
127
+ agent_id=call.agent_id,
128
+ parent_agent_id=self.agent_parents.get(call.agent_id, ""),
129
+ item_id=call_id, exchange_id=call.exchange_id, tool_names=(call.name,),
130
+ tool_calls=({"call_id": call_id, "name": call.name, "arguments": call.arguments},)))
131
+ call.result_reported = True
132
+
133
+ def response_failed(self, reply):
134
+ with self.lock:
135
+ for call in reply.tool_calls:
136
+ self.calls[call["call_id"]].revoked = True
137
+
138
+ def claim(self, call_id, name, arguments):
139
+ """Claim once, after delivery and preceding calls have acknowledged execution."""
140
+ with self.lock:
141
+ call = self.calls.get(call_id)
142
+ if self.cancelled or call is None or call.revoked:
143
+ raise RuntimeError("Tool call has no active, delivered model exchange")
144
+ if call.name != name or call.arguments != arguments:
145
+ raise ValueError("Tool callback differs from its model tool-use ID")
146
+ if name == "mcp__uni_robot_agent__finish" and call.agent_id != self.thread_id:
147
+ call.finished = True
148
+ raise PermissionError("Only the main agent may finish the episode; return findings to the parent")
149
+ if call.claimed:
150
+ raise RuntimeError("Duplicate Claude tool callback")
151
+ previous = self.calls.get(call.previous)
152
+ if not call.delivered or previous is not None and not previous.finished:
153
+ return None
154
+ call.claimed = True
155
+ return call
156
+
157
+ def finish(self, call_id):
158
+ with self.lock:
159
+ self.calls[call_id].finished = True
160
+
161
+ def cancel(self):
162
+ with self.lock:
163
+ self.cancelled = True
@@ -0,0 +1,27 @@
1
+ """Pinned SDK boundary for an MCP server with hook-only call identities.
2
+
3
+ The public SDK helper validates arguments before invoking the tool callback.
4
+ Hook metadata must be removed first, which requires the pinned low-level builder.
5
+ """
6
+ from importlib.metadata import version
7
+
8
+
9
+ def context_env():
10
+ """Disable the pinned CLI's synthetic token-budget reminders."""
11
+ return {"CLAUDE_CODE_TOTAL_TOKENS_REMINDER": "off"}
12
+
13
+
14
+ def mcp_description_env(specs):
15
+ """Keep complete MCP descriptions in the pinned CLI's model requests."""
16
+ # The CLI defaults to 2048 JavaScript UTF-16 code units, not Python characters.
17
+ length = max((len(spec.get("description", "").encode("utf-16-le")) // 2
18
+ for spec in specs), default=0)
19
+ return {"CLAUDE_CODE_MAX_MCP_DESCRIPTION_LENGTH": str(max(2048, length))}
20
+
21
+
22
+ def build_tool_server(tools, callback):
23
+ installed = version("claude-agent-sdk")
24
+ if installed != "0.2.165":
25
+ raise RuntimeError(f"Claude MCP identity bridge requires claude-agent-sdk==0.2.165, got {installed}")
26
+ from claude_agent_sdk._internal._mcp_compat import build_tool_server as build
27
+ return build("uni_robot_agent", "1.0.0", tools, callback)