Drive the whole CommandAGI platform as code: agent threads, memory and integrations, and live embodiments (cloud computers, robots, simulated worlds) that you watch and control.
pip install commandagi
export COMMANDAGI_API_KEY=cagi_... # Settings → API keysfrom commandagi import CommandAGI
cagi = CommandAGI() # reads COMMANDAGI_API_KEY
# Agents: start one on a goal and follow along.
t = cagi.threads.create(intent="Summarise this week's robotics news in five bullets")
cagi.threads.send(t["threadId"], "Add a link for each one.")
# Embodiments: launch a world YOU drive (no agent in it), watch it, control it.
with cagi.launch("simulation/warehouse") as world:
print(world.controls()) # what it accepts right now, with payload schemas
world.sim.ik(target=[0.3, 0.0, 0.4]) # typed robot/sim control
jpeg = world.observe(fresh=True) # the next frame, after the move
# leaving the block stops the world and releases the machine
# Anything else: every platform tool by name.
cagi.call("list_snapshots")A call waits 120 s for its answer. CommandAGI(timeout=600) sets it for every call (None: no limit), and
cagi.call("pcb_route_nets", {...}, timeout=900) for one call. A call with no answer in time raises
CommandAGIError; the platform may still finish the operation.
A Python document can go where only TSX is read: commandagi.design.to_tsx(result) writes its root element as the
TypeScript SDK's code file, which declares the same document.
This SDK, the TypeScript SDK (npm i commandagi) and the commandagi CLI all come from one
schema, commandagi-sdk.schema.json, so they can't drift apart:
-
Tools:
cagi.threads,cagi.embodiments,cagi.memory,cagi.integrations,cagi.social(platform, account),whoami,search,runandpost. Each one is a typed wrapper overcall(tool, args). Options are passed as keyword arguments, e.g.snapshot_id=…, and sent over the wire as camelCase (snapshotId). Where the schema names the options, they are typed keyword-only arguments:cagi.postal.send("Letters/Ada.letter.tsx", provider="click2mail")(the provider is"lob", the default, or"click2mail", US only; omitted, the account'spostal.providersorder picks it). -
Control vocabularies on a live
Session:session.desktopfor computers:click,double_click,move,scroll,type,key,waitsession.robotfor physical robots:joint,gripper,move,turn,home,stop, …session.simfor simulated worlds:reset,sim_mode,ctrl,actuator,ik,trajectory,grab,force,add_robot, …
Every method sends one action to that embodiment. The platform checks it against the controls the embodiment is currently declaring, which
session.controls()lists along with their payload schemas. For anything a runtime declares that no vocabulary covers, usesession.act(action, payload).
The generated part is commandagi/_generated.py. Don't edit it: it is regenerated from the schema in
the CommandAGI monorepo.
cagi.launch(snapshot_id, title=, wait=True, timeout=600) |
a new agentless thread running that snapshot; waits until it declares controls |
cagi.session(thread_id, embodiment_id) |
attach to an embodiment that already exists |
session.frame(fresh=False) / observe() / observe_array() / frames() |
the latest frame, as {url, channel_id, at}, image bytes, or a numpy array (pip install "commandagi[vision]"); frames() yields each new one |
session.describe() |
the live world a sim or robot runtime reports |
session.close() / stop() |
stop watching / also stop a world this session launched |
Snapshot ids come from cagi.call("list_snapshots"), e.g. simulation/warehouse or
computer/software-engineer.
The same structure-declaring primitives as the TypeScript SDK's commandagi/design, producing the same op graph
(plain JSON). commandagi.design itself holds the code-part primitives: CAD (part, box, cylinder, sketch,
extrude, subtract, hole, linear_pattern and the rest), EDA (circuit, board, component, net, connect,
footprints), any graph (graph, node, input_, code) and run_module. There is no kernel, solver, router or
renderer; importing commandagi.design needs nothing but the standard library.
Every document a TypeScript file writes in JSX, a Python file writes as calls, element for element. One module per vocabulary, one function per tag, named by the tag:
| module | documents |
|---|---|
commandagi.design.schematic |
a schematic (group) |
commandagi.design.pcb |
a board in code (board) |
commandagi.design.threed |
a 3D part or assembly |
commandagi.design.fab |
a machining setup (cam), a slicing setup (slicing) |
commandagi.design.study |
a study of a part (study: material, fixtures, load cases, mesh, criteria, revise; docs/formats.md § studies) |
commandagi.design.testplans |
a test plan (testplan: conditions, fixtures, steps) |
commandagi.design.firmware |
a firmware release (firmware: supported revisions), a rollout (rollout: health checks, targets) |
commandagi.design.office |
a workbook, a page, a deck |
commandagi.design.twod |
a drawing, a painting, a photo, a nest |
commandagi.design.media |
a video (clips, composition(src="Intro.vid.py"): another video drawn live from its own code, artboard(src="Poster.draw.py"): a drawing's page drawn live), a song |
commandagi.design.ontology |
a world, a device definition, a dashboard, a geo project, a node graph |
commandagi.design.business |
a company, an RFC, a case |
commandagi.design.tasks |
a task, a project |
commandagi.design.monitors |
a monitor (monitor.py, in the folder of the thing it follows) |
commandagi.design.agents |
an agent harness (harness.py; realtime: what its model takes and gives live), an agent (agent.py: a base or a fork) |
commandagi.design.records |
a contract, a product instance |
commandagi.design.postal |
a letter, a postcard |
commandagi.design.pdf |
a PDF assembled from pages, comments, stamps, signatures, redactions, fields and bookmarks |
The rules of a call, the same in every module:
tag(*children, **attributes). The function's name is the tag:-becomes_(brush_stroke), a Python keyword gets a trailing_(from_), the case is kept (Company).- Children are positional, in order. A tag that holds text takes texts and inline elements:
p("The run ", b("passed"), "."). A loop is a starred argument:layer(*[rect(x=i * 10) for i in range(3)]). A leaf takes no children. - Attributes are keywords in snake_case of the TypeScript name:
frozen_rows=1isfrozenRows,sch_x=114.3isschX. A keyword with an uppercase letter is refused by name. - Only the schematic has positional attributes:
resistor("R1", …),group("Divider", …),trace(from, to),netlabel(net, connection),unit(part, unit). - The file's result is
result = <root>(…)(ormain(), orshow_object).
from commandagi.design.twod import drawing, ellipse, group, layer, rect
result = drawing(
layer(
rect(x=40, y=40, w=200, h=120, fill="#3b82f6"),
group(ellipse(cx=500, cy=300, rx=60, ry=60, fill="#f59e0b"), name="Badge"),
name="Layer 1",
),
name="Poster", width=800, height=600, background="#ffffff",
)from commandagi.design.schematic import ground, group, netlabel, resistor, trace, voltagesource
result = group(
"Divider",
voltagesource("V1", voltage="9", sch_x=114.3, sch_y=114.3),
resistor("R1", resistance="3k", sch_x=114.3, sch_y=88.9, sch_rotation=90),
ground("#PWR1", sch_x=139.7, sch_y=114.3),
trace(".V1 > .pos", ".R1 > .pin1"),
netlabel("OUT", ".R1 > .pin2"),
)A graph document (a schematic, a board, a 3D part, a drawing, a deck, a video, a node graph) leaves run_module as
its graph. Any other document (a workbook, a world, a company, a task …) leaves as document ({format, document, sources}) beside an empty graph.
run_module maps the file's calls with Python's ast (commandagi.design.source). Each element records the call
that made it. A declaration puts that call where the TypeScript SDK puts a JSX element's source: a node's
meta.source, a node's meta.sources by key, a document's sources by key. Each one holds the call's span, each
keyword's span with its literal value or its expression, and how many times the call ran. The whole map rides on the
graph as meta.sourceMap: every call with its arguments, its parent element, its slot among its parent's children,
and whether it runs in a loop. The editor writes each edit back into the file as the smallest text edit. A keyword
that is an expression is never replaced with a literal: the edit is refused with its line.
commandagi.design.cadquery is a CadQuery-style importer: Workplane("XY").box(…).faces(">Z").workplane() .rarray(…).hole(…) records the same features the primitives declare. It refuses, by name, what needs real topology
(fillets, edge selectors).
A .py code part is a script. Its result is main(**inputs) if it defines main; otherwise what it passed to
show_object; otherwise its result variable. param(name, default, unit=…) declares an input and reads its value.
In a CommandAGI editor, a .py code node runs under Pyodide in a sandboxed worker, where import cadquery as cq is
this importer (OCCT does not run there). run_module(source, path, inputs) is what the sandbox calls. Tests:
python -m unittest discover -s tests.
commandagi.design.threed declares a part and builds nothing. Kernel asks the CommandAGI 3D design service
(OpenCascade) to build it. It uses the standard library only.
from commandagi.design.threed import box, cylinder, part
from commandagi.kernel import Kernel
plate = part(
box(id="plate", center=[0, 0, 4], size=[60, 40, 8], operation="new"),
cylinder(id="hole", center=[0, 0, -1], axis=[0, 0, 1], radius=3, height=10, operation="cut"),
name="Plate",
)
kernel = Kernel() # reads COMMANDAGI_THREED
seen = kernel.observe(plate)["observation"] # bodies, faces, edges, mass properties; status and failures
assert seen["status"] == "built", seen["failures"] # "failed": the bodies are built from the features that did not fail
open("plate.step", "w").write(kernel.export(plate, "step")) # refused (KernelError.failures) when a feature failedA part is built in the order of its elements: the cut above follows the plate because it is written after it.
consumes=[…] names the features a feature follows instead.
One setting says where it runs: COMMANDAGI_THREED, the places to try in order, apart by , (default local,cloud).
A place where the kernel is not installed is passed over. A refusal of the op itself (a build that is not whole) is
the answer: it is not asked of the next place. kernel.last_ran says where each answer ran ({"kernel", "build", "place"}); place is where the SDK sent the request, so a result made on this computer says local and is never a
cloud result.
| place | the kernel | needs |
|---|---|---|
local |
this computer, offline, no account. Kernel starts it and stops it when Python exits; COMMANDAGI_THREED_URL names one that runs already (commandagi threed serve --port 8091) |
Node 22 and, once: npm install -g @commandagi/host then commandagi threed install (it downloads OpenCascade as WebAssembly, about 130 MB); for a running one, COMMANDAGI_THREED_TOKEN, the bearer you started it with |
cloud |
the platform's kernel door (/kernels/3d/v1/<op>) |
the network and COMMANDAGI_API_KEY |
A URL in COMMANDAGI_THREED is that running service only. Both places speak one contract (POST <kernel>/v1/<op>,
GET <kernel>/v1/health), and kernel.call(op, doc, params) gives a whole answer (outputs, result, ran).
observe(doc), export(doc, "step" | "iges", bodies=), measure(doc, "distance" | "angle" | "radius" | "closest", a, b), measure_all(doc, [{"quantity", "a", "b"}, …]) (many on one build), interference(doc, clearance=, bodies=),
drawing(doc, direction=, up=, section=), tessellate(doc, deflection). doc is what a file declares, its
graph (run_module(...)["graph"]) or the graph's JSON text. An entity is {"body": "body_0", "kind": "face" | "edge" | "vertex" | "body", "index": n}, with the index as observe lists it.
- A build in which a feature failed is not whole.
observeanswers it withstatus: "failed"andfailures. Every other call refuses it:KernelErrorwithstatus422 andfailures(each failed feature, with why). interferencecompares every pair of bodies:clashes(shared volume),engagements(shared volume that all lies in a declaredthread: a screw in a tapped hole) and, withclearance,close(nearer than it). Each body has its part's name.- An assembly's files go with the call:
parts=linked_parts({"bolt.3d.py": bolt, "bracket.step": text}), each under the name itsexternalPartgives. - A STEP file names its parts: each body is a product of its part's name, an assembly the product they are components of.
dfm(doc, process="print" | "mill", limits=…, setup=…)says whether a machine can make the part:limitsare a device definition'sprocess["print"]orprocess["mill"],setup={"tools": [{"diameter_mm": 3.175}]}the mill's tools. Each finding ispass,failorunknown(neverpasswhen a limit or a tool is missing) with its value, limit andwhere(the body, the B-repfaces, a point);verdictsums them.check(doc, devices=…): acheck(kind="printability" | "mill", options={"device": ref})reads its limits fromdevices,{ref: {"process": …}}, the definitions you read; without them the check is skipped and says why.
commandagi.gym_env.CommandAGIEnv wraps a sim session as a Gymnasium env (pip install "commandagi[gym]"), and commandagi.lerobot records rollouts as LeRobot datasets (pip install "commandagi[lerobot]").
| variable | meaning |
|---|---|
COMMANDAGI_API_KEY |
your cagi_… key. Its scopes decide what every call may do |
COMMANDAGI_BASE_URL |
API origin (default https://api.commandagi.com) |
COMMANDAGI_THREAD_ID |
default thread for self-thread methods. Set automatically when your code runs inside the platform |
COMMANDAGI_THREED |
where Kernel runs: local, cloud, both apart by , in the order to try (default local,cloud), or a running service's URL |
COMMANDAGI_THREED_URL, COMMANDAGI_THREED_TOKEN |
a 3D design service that runs already, and its bearer |
- A release is the tag
v<version>in this repository and the same version on PyPI. Pin an exact version:commandagi==0.6.2. A range (^,~=,>=) takes the next release without asking you. - The three SDKs (Python, TypeScript, Rust) carry one version number. A vocabulary at version X in one language is the vocabulary at version X in the others.
- A version promises nothing about the next one. CommandAGI keeps no compatibility layers: a release may rename or
remove a function, an element or an attribute, with no alias and no migration.
CHANGELOG.mdsays what changed. - So a document belongs with the version that reads it. Keep the pin in the project that holds the documents, and change both together. A document you open in a CommandAGI editor is read by the SDK that editor ships.
MIT (LICENSE).