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.
- uni_robot_agent/__init__.py +50 -0
- uni_robot_agent/__main__.py +5 -0
- uni_robot_agent/agents/__init__.py +19 -0
- uni_robot_agent/agents/claude_code/__init__.py +2 -0
- uni_robot_agent/agents/claude_code/agent.py +163 -0
- uni_robot_agent/agents/claude_code/artifacts.py +29 -0
- uni_robot_agent/agents/claude_code/config.py +50 -0
- uni_robot_agent/agents/claude_code/events.py +121 -0
- uni_robot_agent/agents/claude_code/model_binding.py +163 -0
- uni_robot_agent/agents/claude_code/sdk_compat.py +27 -0
- uni_robot_agent/agents/claude_code/tools.py +131 -0
- uni_robot_agent/agents/codex/__init__.py +2 -0
- uni_robot_agent/agents/codex/agent.py +523 -0
- uni_robot_agent/agents/codex/artifacts.py +811 -0
- uni_robot_agent/agents/codex/config.py +91 -0
- uni_robot_agent/agents/codex/events.py +463 -0
- uni_robot_agent/agents/codex/model_binding.py +200 -0
- uni_robot_agent/agents/contracts.py +93 -0
- uni_robot_agent/agents/registry.py +53 -0
- uni_robot_agent/agents/runtime.py +79 -0
- uni_robot_agent/artifacts/__init__.py +5 -0
- uni_robot_agent/artifacts/actor.py +55 -0
- uni_robot_agent/artifacts/mirror.py +109 -0
- uni_robot_agent/artifacts/records.py +71 -0
- uni_robot_agent/artifacts/run.py +118 -0
- uni_robot_agent/artifacts/sft/__init__.py +1 -0
- uni_robot_agent/artifacts/store.py +191 -0
- uni_robot_agent/artifacts/writer.py +156 -0
- uni_robot_agent/cli.py +275 -0
- uni_robot_agent/config.py +302 -0
- uni_robot_agent/context/__init__.py +37 -0
- uni_robot_agent/context/history.py +206 -0
- uni_robot_agent/context/policies/__init__.py +6 -0
- uni_robot_agent/context/policies/identity.py +34 -0
- uni_robot_agent/context/policies/sliding_window.py +389 -0
- uni_robot_agent/context/prompt.py +215 -0
- uni_robot_agent/events/__init__.py +39 -0
- uni_robot_agent/events/core.py +202 -0
- uni_robot_agent/events/subscribers.py +214 -0
- uni_robot_agent/execution/__init__.py +179 -0
- uni_robot_agent/execution/correlation.py +18 -0
- uni_robot_agent/execution/parallel.py +303 -0
- uni_robot_agent/execution/scheduler.py +324 -0
- uni_robot_agent/execution/serial.py +60 -0
- uni_robot_agent/integrations/__init__.py +40 -0
- uni_robot_agent/integrations/base.py +91 -0
- uni_robot_agent/integrations/gripper.py +147 -0
- uni_robot_agent/integrations/prepared_episode.py +201 -0
- uni_robot_agent/integrations/robodojo/SOURCE.md +134 -0
- uni_robot_agent/integrations/robodojo/UPSTREAM_LICENSE +21 -0
- uni_robot_agent/integrations/robodojo/__init__.py +56 -0
- uni_robot_agent/integrations/robodojo/action_binding.py +331 -0
- uni_robot_agent/integrations/robodojo/assets.py +33 -0
- uni_robot_agent/integrations/robodojo/build_compat.py +24 -0
- uni_robot_agent/integrations/robodojo/deployment.py +135 -0
- uni_robot_agent/integrations/robodojo/deployment.toml +14 -0
- uni_robot_agent/integrations/robodojo/environment/__init__.py +1 -0
- uni_robot_agent/integrations/robodojo/environment/adapter.py +435 -0
- uni_robot_agent/integrations/robodojo/environment/camera_projection.py +77 -0
- uni_robot_agent/integrations/robodojo/environment/geometry.py +228 -0
- uni_robot_agent/integrations/robodojo/environment/gripper_geometry.py +82 -0
- uni_robot_agent/integrations/robodojo/environment/ik/__init__.py +1 -0
- uni_robot_agent/integrations/robodojo/environment/ik/curobo.py +175 -0
- uni_robot_agent/integrations/robodojo/environment/ik/goal_region.py +106 -0
- uni_robot_agent/integrations/robodojo/environment/ik/recovery.py +129 -0
- uni_robot_agent/integrations/robodojo/environment/kinematics.py +290 -0
- uni_robot_agent/integrations/robodojo/environment/native_compat.py +155 -0
- uni_robot_agent/integrations/robodojo/environment/robot_geometry.py +72 -0
- uni_robot_agent/integrations/robodojo/environment/server.py +202 -0
- uni_robot_agent/integrations/robodojo/environment/x5_grasp_site.urdf +13 -0
- uni_robot_agent/integrations/robodojo/prompt.py +56 -0
- uni_robot_agent/integrations/robolab/SOURCE.md +15 -0
- uni_robot_agent/integrations/robolab/__init__.py +39 -0
- uni_robot_agent/integrations/robolab/environment/__init__.py +1 -0
- uni_robot_agent/integrations/robolab/environment/adapter.py +254 -0
- uni_robot_agent/integrations/robolab/environment/controller.py +61 -0
- uni_robot_agent/integrations/robolab/environment/drives.py +25 -0
- uni_robot_agent/integrations/robolab/environment/executor.py +47 -0
- uni_robot_agent/integrations/robolab/environment/gripper.py +63 -0
- uni_robot_agent/integrations/robolab/environment/kinematics.py +213 -0
- uni_robot_agent/integrations/robolab/environment/native_compat.py +62 -0
- uni_robot_agent/integrations/robolab/environment/server.py +187 -0
- uni_robot_agent/integrations/robolab/environment/video.py +75 -0
- uni_robot_agent/integrations/robolab/prompt.py +24 -0
- uni_robot_agent/integrations/robolab/session.py +42 -0
- uni_robot_agent/integrations/robouse/__init__.py +19 -0
- uni_robot_agent/integrations/robouse/deployment.py +174 -0
- uni_robot_agent/integrations/robouse/environment/__init__.py +1 -0
- uni_robot_agent/integrations/robouse/environment/adapter.py +106 -0
- uni_robot_agent/integrations/robouse/environment/backends/__init__.py +16 -0
- uni_robot_agent/integrations/robouse/environment/backends/bimanual.py +55 -0
- uni_robot_agent/integrations/robouse/environment/backends/cartesian.py +85 -0
- uni_robot_agent/integrations/robouse/environment/backends/osc.py +77 -0
- uni_robot_agent/integrations/robouse/environment/backends/worker_backend.py +69 -0
- uni_robot_agent/integrations/robouse/environment/backends/worker_control.py +85 -0
- uni_robot_agent/integrations/robouse/environment/control.py +34 -0
- uni_robot_agent/integrations/robouse/environment/geometry.py +75 -0
- uni_robot_agent/integrations/robouse/environment/native_worker.py +134 -0
- uni_robot_agent/integrations/robouse/environment/server.py +136 -0
- uni_robot_agent/integrations/robouse/environment/worker_cameras.py +65 -0
- uni_robot_agent/integrations/robouse/environment/worker_frames.py +28 -0
- uni_robot_agent/integrations/robouse/prompt.py +22 -0
- uni_robot_agent/integrations/robouse/recipes/base.toml +1 -0
- uni_robot_agent/integrations/robouse/recipes/factory.toml +9 -0
- uni_robot_agent/integrations/robouse/recipes/genesis.toml +6 -0
- uni_robot_agent/integrations/robouse/recipes/libero.toml +8 -0
- uni_robot_agent/integrations/robouse/recipes/maniskill_cpu.toml +6 -0
- uni_robot_agent/integrations/robouse/recipes/maniskill_gpu.toml +6 -0
- uni_robot_agent/integrations/robouse/recipes/mujoco.toml +6 -0
- uni_robot_agent/integrations/robouse/recipes/robocasa.toml +28 -0
- uni_robot_agent/integrations/robouse/tasks.py +73 -0
- uni_robot_agent/integrations/robouse/worker_tasks.py +66 -0
- uni_robot_agent/observation/__init__.py +36 -0
- uni_robot_agent/observation/episode.py +107 -0
- uni_robot_agent/observation/hub.py +432 -0
- uni_robot_agent/provider/__init__.py +19 -0
- uni_robot_agent/provider/backends/__init__.py +1 -0
- uni_robot_agent/provider/backends/aliyun/__init__.py +1 -0
- uni_robot_agent/provider/backends/aliyun/chat_completions.py +37 -0
- uni_robot_agent/provider/backends/aliyun/messages.py +56 -0
- uni_robot_agent/provider/backends/deepseek/__init__.py +1 -0
- uni_robot_agent/provider/backends/deepseek/chat_completions.py +35 -0
- uni_robot_agent/provider/backends/deepseek/file_broker.py +358 -0
- uni_robot_agent/provider/backends/deepseek/files.py +256 -0
- uni_robot_agent/provider/backends/minimax/__init__.py +1 -0
- uni_robot_agent/provider/backends/minimax/chat_completions.py +42 -0
- uni_robot_agent/provider/backends/moonshot/__init__.py +1 -0
- uni_robot_agent/provider/backends/moonshot/chat_completions.py +34 -0
- uni_robot_agent/provider/backends/passthrough.py +46 -0
- uni_robot_agent/provider/backends/volcengine/__init__.py +1 -0
- uni_robot_agent/provider/backends/volcengine/responses.py +68 -0
- uni_robot_agent/provider/client.py +285 -0
- uni_robot_agent/provider/contracts.py +124 -0
- uni_robot_agent/provider/conversions/__init__.py +1 -0
- uni_robot_agent/provider/conversions/responses_chat.py +411 -0
- uni_robot_agent/provider/conversions/tool_names.py +154 -0
- uni_robot_agent/provider/gateway.py +424 -0
- uni_robot_agent/provider/image_encoding.py +20 -0
- uni_robot_agent/provider/protocols/__init__.py +1 -0
- uni_robot_agent/provider/protocols/chat_completions.py +94 -0
- uni_robot_agent/provider/protocols/messages.py +138 -0
- uni_robot_agent/provider/protocols/messages_keepalive.py +73 -0
- uni_robot_agent/provider/protocols/responses.py +144 -0
- uni_robot_agent/provider/protocols/sse.py +35 -0
- uni_robot_agent/provider/recording/__init__.py +1 -0
- uni_robot_agent/provider/recording/exchanges.py +469 -0
- uni_robot_agent/provider/recording/images.py +43 -0
- uni_robot_agent/provider/registry.py +108 -0
- uni_robot_agent/py.typed +0 -0
- uni_robot_agent/resources/__init__.py +1 -0
- uni_robot_agent/resources/prompts/actor/robodojo_vla_decision.md +85 -0
- uni_robot_agent/resources/prompts/guides/meta_skill.md +33 -0
- uni_robot_agent/resources/prompts/observer/main_agent.md +28 -0
- uni_robot_agent/resources/prompts/observer/observer.md +37 -0
- uni_robot_agent/resources/prompts/observer/robodojo_main.md +12 -0
- uni_robot_agent/resources/prompts/robodojo/hybrid.md +17 -0
- uni_robot_agent/resources/prompts/robodojo/instruction_supplements.json +198 -0
- uni_robot_agent/resources/prompts/robodojo/system.md +143 -0
- uni_robot_agent/resources/prompts/robodojo/task_requirements.json +213 -0
- uni_robot_agent/resources/prompts/robodojo/user.md +2 -0
- uni_robot_agent/resources/prompts/runtime/batch.md +15 -0
- uni_robot_agent/resources/prompts/runtime/common.md +55 -0
- uni_robot_agent/resources/prompts/runtime/hybrid.md +27 -0
- uni_robot_agent/resources/prompts/runtime/resource_global-serial.md +1 -0
- uni_robot_agent/resources/prompts/runtime/resource_per-resource.md +1 -0
- uni_robot_agent/resources/prompts/runtime/return_background.md +21 -0
- uni_robot_agent/resources/prompts/runtime/return_immediate.md +1 -0
- uni_robot_agent/resources/prompts/runtime/storage_access.md +11 -0
- uni_robot_agent/resources/skills/general/SKILL.md +222 -0
- uni_robot_agent/resources/skills/general/gripper/contact-effect/SKILL.md +104 -0
- uni_robot_agent/resources/skills/general/gripper/contact-effect/references/exact-count-cycles.md +26 -0
- uni_robot_agent/resources/skills/general/gripper/contents-transfer/SKILL.md +38 -0
- uni_robot_agent/resources/skills/general/gripper/deformable-material/SKILL.md +34 -0
- uni_robot_agent/resources/skills/general/gripper/grasp-geometry/SKILL.md +122 -0
- uni_robot_agent/resources/skills/general/gripper/grasp-geometry/references/contact-calculation.md +147 -0
- uni_robot_agent/resources/skills/general/gripper/grasp-geometry/references/shape-based-grasping.md +104 -0
- uni_robot_agent/resources/skills/general/gripper/handover/SKILL.md +75 -0
- uni_robot_agent/resources/skills/general/gripper/held-object-geometry/SKILL.md +58 -0
- uni_robot_agent/resources/skills/general/gripper/motion-recovery/SKILL.md +123 -0
- uni_robot_agent/resources/skills/general/gripper/moving-target-control/SKILL.md +42 -0
- uni_robot_agent/resources/skills/general/gripper/placement-and-engagement/SKILL.md +111 -0
- uni_robot_agent/resources/skills/general/gripper/pose-targeting/SKILL.md +110 -0
- uni_robot_agent/resources/skills/general/gripper/reachability-and-recovery/SKILL.md +76 -0
- uni_robot_agent/resources/skills/general/gripper/return-to-start/SKILL.md +57 -0
- uni_robot_agent/resources/skills/general/gripper/scene-memory/SKILL.md +91 -0
- uni_robot_agent/resources/skills/general/gripper/scene-memory/references/pose-and-order-memory.md +26 -0
- uni_robot_agent/resources/skills/general/gripper/state-verification/SKILL.md +106 -0
- uni_robot_agent/resources/skills/general/gripper/supported-contact/SKILL.md +100 -0
- uni_robot_agent/resources/skills/general/gripper/tool-selection/SKILL.md +181 -0
- uni_robot_agent/resources/skills/general/gripper/tool-selection/references/depth-grounding.md +47 -0
- uni_robot_agent/resources/skills/general/gripper/tool-selection/references/rgb-grounding.md +58 -0
- uni_robot_agent/resources/skills/general/index.md +65 -0
- uni_robot_agent/resources/skills/robodojo/gripper/motion-control/SKILL.md +95 -0
- uni_robot_agent/resources/skills/robodojo/gripper/return-to-start/SKILL.md +19 -0
- uni_robot_agent/resources/skills/robodojo/index.md +26 -0
- uni_robot_agent/resources/skills/robodojo/perception-reference.md +64 -0
- uni_robot_agent/resources/skills/robodojo/robot-reference.md +126 -0
- uni_robot_agent/services/__init__.py +0 -0
- uni_robot_agent/services/actor/__init__.py +0 -0
- uni_robot_agent/services/actor/backend.py +24 -0
- uni_robot_agent/services/actor/backends/__init__.py +0 -0
- uni_robot_agent/services/actor/backends/pi05/__init__.py +0 -0
- uni_robot_agent/services/actor/backends/pi05/backend.py +46 -0
- uni_robot_agent/services/actor/backends/pi05/config.py +41 -0
- uni_robot_agent/services/actor/backends/pi05/openpi_jax.py +227 -0
- uni_robot_agent/services/actor/client.py +187 -0
- uni_robot_agent/services/actor/contracts.py +95 -0
- uni_robot_agent/services/actor/deployment.py +80 -0
- uni_robot_agent/services/actor/images.py +28 -0
- uni_robot_agent/services/actor/recipes/robodojo_pi05.toml +8 -0
- uni_robot_agent/services/actor/routes.py +50 -0
- uni_robot_agent/services/actor/server.py +126 -0
- uni_robot_agent/services/actor/service.py +303 -0
- uni_robot_agent/services/batching.py +165 -0
- uni_robot_agent/services/deployment/__init__.py +4 -0
- uni_robot_agent/services/deployment/config.py +214 -0
- uni_robot_agent/services/deployment/manager.py +181 -0
- uni_robot_agent/services/deployment/process.py +185 -0
- uni_robot_agent/services/deployment/runtime.py +106 -0
- uni_robot_agent/services/deployment/setup.py +225 -0
- uni_robot_agent/services/environment/__init__.py +0 -0
- uni_robot_agent/services/environment/backend.py +48 -0
- uni_robot_agent/services/environment/client.py +272 -0
- uni_robot_agent/services/environment/contracts.py +221 -0
- uni_robot_agent/services/environment/device_contracts.py +150 -0
- uni_robot_agent/services/environment/interrupts.py +51 -0
- uni_robot_agent/services/environment/primitive.py +54 -0
- uni_robot_agent/services/environment/routes.py +164 -0
- uni_robot_agent/services/environment/service.py +389 -0
- uni_robot_agent/services/environment/snapshots.py +158 -0
- uni_robot_agent/services/errors.py +19 -0
- uni_robot_agent/services/hosting.py +251 -0
- uni_robot_agent/services/http.py +72 -0
- uni_robot_agent/services/mcp/__init__.py +1 -0
- uni_robot_agent/services/mcp/http.py +139 -0
- uni_robot_agent/services/mcp/results.py +40 -0
- uni_robot_agent/services/perception/__init__.py +0 -0
- uni_robot_agent/services/perception/backend.py +67 -0
- uni_robot_agent/services/perception/backends/__init__.py +0 -0
- uni_robot_agent/services/perception/backends/sam3.py +400 -0
- uni_robot_agent/services/perception/client.py +39 -0
- uni_robot_agent/services/perception/codec.py +83 -0
- uni_robot_agent/services/perception/contracts.py +79 -0
- uni_robot_agent/services/perception/deployment.py +56 -0
- uni_robot_agent/services/perception/recipes/sam3.toml +7 -0
- uni_robot_agent/services/perception/routes.py +43 -0
- uni_robot_agent/services/perception/server.py +102 -0
- uni_robot_agent/services/perception/service.py +62 -0
- uni_robot_agent/session.py +851 -0
- uni_robot_agent/telemetry/__init__.py +22 -0
- uni_robot_agent/telemetry/logging.py +18 -0
- uni_robot_agent/telemetry/model.py +47 -0
- uni_robot_agent/telemetry/session.py +66 -0
- uni_robot_agent/telemetry/sinks.py +146 -0
- uni_robot_agent/telemetry/tracer.py +154 -0
- uni_robot_agent/tools/__init__.py +21 -0
- uni_robot_agent/tools/client.py +60 -0
- uni_robot_agent/tools/episode.py +36 -0
- uni_robot_agent/tools/gripper/__init__.py +4 -0
- uni_robot_agent/tools/gripper/actor.py +126 -0
- uni_robot_agent/tools/gripper/contracts.py +78 -0
- uni_robot_agent/tools/gripper/motion.py +536 -0
- uni_robot_agent/tools/gripper/opening.py +62 -0
- uni_robot_agent/tools/gripper/pose.py +130 -0
- uni_robot_agent/tools/gripper/provider.py +298 -0
- uni_robot_agent/tools/gripper/reviewer.py +88 -0
- uni_robot_agent/tools/jobs.py +249 -0
- uni_robot_agent/tools/observation.py +156 -0
- uni_robot_agent/tools/presentation.py +39 -0
- uni_robot_agent/tools/scene/__init__.py +1 -0
- uni_robot_agent/tools/scene/geometry.py +149 -0
- uni_robot_agent/tools/scene/perception.py +190 -0
- uni_robot_agent/tools/scene/provider.py +173 -0
- uni_robot_agent/tools/scene/triangulation.py +71 -0
- uni_robot_agent/tools/selection.py +56 -0
- uni_robot_agent/tools/skills.py +137 -0
- uni_robot_agent/tools/toolkit.py +245 -0
- uni_robot_agent/tools/workspace.py +75 -0
- uni_robot_agent/ui/__init__.py +1 -0
- uni_robot_agent/ui/console.py +631 -0
- uni_robot_agent/ui/state.py +122 -0
- uni_robot_agent/ui/textual/__init__.py +1 -0
- uni_robot_agent/ui/textual/agent_panel.py +62 -0
- uni_robot_agent/ui/textual/app.py +292 -0
- uni_robot_agent/ui/textual/widgets.py +164 -0
- uni_robot_agent/ui/web/__init__.py +1 -0
- uni_robot_agent/ui/web/archive.py +80 -0
- uni_robot_agent/ui/web/media.py +55 -0
- uni_robot_agent/ui/web/reader.py +102 -0
- uni_robot_agent/ui/web/runtime.py +108 -0
- uni_robot_agent/ui/web/server.py +150 -0
- uni_robot_agent/ui/web/state.py +179 -0
- uni_robot_agent/ui/web/static/app.js +299 -0
- uni_robot_agent/ui/web/static/common.js +51 -0
- uni_robot_agent/ui/web/static/console.js +91 -0
- uni_robot_agent/ui/web/static/frames.js +17 -0
- uni_robot_agent/ui/web/static/index.html +181 -0
- uni_robot_agent/ui/web/static/metrics.js +90 -0
- uni_robot_agent/ui/web/static/motion.js +18 -0
- uni_robot_agent/ui/web/static/playback.js +28 -0
- uni_robot_agent/ui/web/static/styles.css +1018 -0
- uni_robot_agent/ui/web/static/timeline.js +84 -0
- uni_robot_agent/ui/web/static/viewer.js +128 -0
- uni_robot_agent_runtime-0.1.1.dist-info/METADATA +47 -0
- uni_robot_agent_runtime-0.1.1.dist-info/RECORD +309 -0
- uni_robot_agent_runtime-0.1.1.dist-info/WHEEL +5 -0
- uni_robot_agent_runtime-0.1.1.dist-info/entry_points.txt +2 -0
- uni_robot_agent_runtime-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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,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,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)
|