Skip to content

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()
['so101_sim', 'run_policy', 'pose_tool', 'download_assets']

The gate in front of real motion

Left, the motion commands that enter: execute and start on the robot tool, the ros, serial, pose and unitree tools. Middle, gate_motion and under it a dashed layer with four steps decided in order: the allowlist variable names the command, allow silently; BYPASS_TOOL_CONSENT=true, allow with a warning; nobody to ask, refuse naming the variable; ask the operator, the one green element, an interrupt where y dispatches and anything else declines. Each step's outcome is written to the right and every outcome flows to the audit log under the layer. Bottom left, the one path around the gate: the native drivers' move_to is not gated today; reading and stopping are never gated. Footnote: the operator's reply goes to the audit log, never to the model.Left, the motion commands that enter: execute and start on the robot tool, the ros, serial, pose and unitree tools. Middle, gate_motion and under it a dashed layer with four steps decided in order: the allowlist variable names the command, allow silently; BYPASS_TOOL_CONSENT=true, allow with a warning; nobody to ask, refuse naming the variable; ask the operator, the one green element, an interrupt where y dispatches and anything else declines. Each step's outcome is written to the right and every outcome flows to the audit log under the layer. Bottom left, the one path around the gate: the native drivers' move_to is not gated today; reading and stopping are never gated. Footnote: the operator's reply goes to the audit log, never to the model.

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.

Edit page