Talk to it¶
Stage 2, ten minutes: the robot as a Strands Agent tool, a sentence that moves it, and the operator gate stopping a real rollout.
At the end of this page a Strands Agent has moved the simulated arm from a sentence you typed, and you have watched the same agent stop at the approval gate in front of a real one. See it shows a captured run of exactly this; here you run it. The sim fences run without a model. The two fences that call agent("...") need a model provider configured for strands-agents; Bedrock is the default.
The robot is a tool¶
from strands import Agent
from strands_robots import Robot
robot = Robot("so101")
agent = Agent(tools=[robot], callback_handler=None)
print(agent.tool_names)
spec = robot.tool_spec
print(spec["description"][:160])
print(len(spec["inputSchema"]["json"]["properties"]["action"]["enum"]), "actions")
result = agent.tool.so101_sim(action="get_robot_state")
print(result["content"][0]["text"].splitlines()[1])
robot.cleanup()
You should see:
['so101_sim']
Programmatic MuJoCo simulation environment (stateful session). One world per instance. The world is ALREADY CREATED and holds robot(s) 'so101' (6 joints: 1, 2,
77 actions
1 (shoulder_pan): pos=0.0000, vel=0.0000
Nothing was wrapped. The object Robot() returned is a Strands AgentTool: a name (so101_sim in sim, so101 on hardware, or whatever tool_name= says), a description the model reads, one action enum. agent.tool.so101_sim(...) calls it directly, no model in the loop, and returns the envelope Run it showed. Two robots in one agent need two names: Robot("so101", tool_name="left").
Ask in words¶
from strands import Agent
from strands_robots import Robot
robot = Robot("so101")
agent = Agent(tools=[robot], callback_handler=None)
result = agent("Read the arm's joint state, then move joint 1 to 0.5 rad and report where the gripper ended up.")
print(result)
robot.cleanup()
The model calls get_robot_state, then set_joint_positions or actuate_robot with some step calls, then get_robot_state again, and writes what it found. One run on this checkout reported the gripper moving from [+0.020, -0.376, +0.259] to [-0.150, -0.335, +0.237], a 17 cm sweep along -X for a 0.5 rad pan. Your model picks its own actions and words; the joint it reports is read from physics, not invented.
Other tools mount the same way (tool reference); pose_tool talks to a Feetech bus and needs pip install 'strands-robots[serial]':
from strands import Agent
from strands_robots import Robot, run_policy, pose_tool, download_assets
robot = Robot("so101")
agent = Agent(tools=[robot, run_policy, pose_tool, download_assets], callback_handler=None)
print(agent.tool_names)
robot.cleanup()
The gate in front of real motion¶
The SO-101 defaults to its native driver, which is not a Strands tool, so this call spells driver="lerobot". That tool has eight actions. Six read or halt, ungated: get_state, get_robot_state, list_cameras, render, status, stop. execute and start dispatch a policy rollout to real actuators once a human approves. No arm is needed: the gate runs before the driver opens port="/dev/null", so the interrupt is reached (approving then fails to connect):
from strands import Agent
from strands_robots import Robot
arm = Robot("so101", mode="real", driver="lerobot", port="/dev/null")
agent = Agent(tools=[arm], callback_handler=None)
result = agent("Run the mock policy on so101 for 2 seconds with the instruction 'wave'. Call the tool directly.")
print(result.stop_reason)
for interrupt in result.interrupts:
print(interrupt.name)
print(interrupt.reason["warning"])
responses = [{"interruptResponse": {"interruptId": interrupt.id, "response": "n"}}]
result = agent(responses)
print(result.stop_reason)
arm.cleanup()
You should see the agent pause instead of finishing, and then finish once you answer:
interrupt
robot-command-approval
'execute' drives the real robot 'so101' for up to 2s with 'wave' (policy mock built in this process, no server); it needs operator approval before it is dispatched. Note: MockPolicy does not read the instruction. Its actions - a test motion on every joint - are commanded to the robot whatever the task says; no status or completion that follows will mean the task was performed. Reply 'y' to approve, anything else to deny.
end_turn
The warning says how long the arm may move and, when the policy does not read the instruction, that the words will not shape the motion. "y" approves and the rollout is dispatched; anything else denies and nothing moves. interrupt.reason["how_to_answer"] carries the resume line, so a script that prints a paused result prints how to continue it.
With no agent, the same call is refused outright and the refusal names the variable that pre-approves it (what a refusal looks like; First learned policy shows it with a Hub checkpoint). STRANDS_ROBOT_COMMAND_ALLOW names pre-approved actions (execute, start, or *); with nobody to ask the call fails closed; every answer lands in the audit log. The operator gate gives the full order, the other tools' allow variables, and the one path that is not gated yet.
Where next¶
You now have an agent that drives the simulated arm from a sentence, and you saw the operator gate stop a real rollout. Next rung: Real arm. Agents and robots covers what the model sees, multi-robot agents and the dashboard's approval flow; Policies replaces mock with a model that acts on the words.