# Motus Cloud Source: https://docs.motus.lithosai.com/cloud/overview The managed platform that runs your agents. Same code as motus serve, reachable over HTTPS, with observability built in. Motus Cloud runs your agents for you. You push code, the platform builds it, deploys it, and exposes it as an HTTPS endpoint. Servers, container builds, autoscaling, and rolling upgrades are all on the platform side. The code you write locally (`motus serve start myapp:agent`) is the same code that runs on the cloud (`motus deploy myapp:agent`). The REST API is the same. `motus serve chat ` works against either. What changes when you deploy is where the agent lives, not how you wrote it. ## The hierarchy ``` Project ├── Build (one per deploy) └── Session (one per conversation) └── Trace (one per agent run) └── Span (each step inside a trace) ``` ### Project A project is one agent application. It has a stable `project_id`, owns every build you have ever pushed, holds the secrets you injected, and keeps the URL your agent is reachable at. You typically create one project per agent app and keep deploying to it. ### Build A packaged snapshot of your source code. Every `motus deploy` produces a new build with a fresh `build_id` that moves through: ``` CREATED → QUEUED → BUILDING → BUILT → DEPLOYING → DEPLOYED → HEALTHY ``` `HEALTHY` is the steady state (the build is serving traffic). `FAILED` is the terminal error state and can happen at any of the earlier stops. The newest healthy build is always what serves traffic. Earlier builds remain in the dashboard as a record, with their logs available. ### Session A session is a sequence of requests that share state across turns. What that state contains depends on the agent: for a `ReActAgent` it is the conversation context that memory carries forward (prior messages, tool calls, and their results). Behaves identically to `motus serve`: create a session, post messages to it, receive responses. ### Trace A record of one agent run, from the initial invocation through every LLM call, tool call, and sub-task it triggered. Every trace belongs to the session it ran inside. ### Span Each LLM call, tool invocation, and `@agent_task` run shows up as a span inside the trace. Spans nest, so the trace tree shows exactly what the agent did and how long each step took. ## Deploy in one command ```bash theme={null} motus deploy myapp:agent --name my-agent ``` This packages your source, builds it on the platform, and makes your agent reachable at a stable HTTPS URL tied to the project. See [Deployment](/guides/deployment) for the full workflow including secrets and Git-based deploys. ## Authentication Run `motus login` once. It opens a browser for device-code OAuth and writes credentials to `~/.motus/credentials.json`. The same credentials are picked up by `motus deploy`, `motus serve chat `, and any call to the platform REST API. For CI or other non-interactive environments, set `LITHOSAI_API_KEY` instead. It overrides the credentials file. ## What the cloud handles for you **You do not configure model-provider credentials in deployed code.** At deploy time the platform injects the right API keys and base URLs into your container, so clients like `OpenAIChatClient()` or `AnthropicChatClient()` just work and the calls bill to your Motus account. A few other things you no longer have to worry about once you deploy: * **Build environment.** The platform installs your dependencies from `pyproject.toml` or `requirements.txt` (same as `uv sync` locally) and runs your agent. * **Runtime secrets.** Pass `--secret KEY=VALUE` on the `motus deploy` command and the value lands in the agent's environment, ready for `os.environ[KEY]`. * **HTTPS, DNS, scaling, health checks, and rolling deploys.** All handled by the platform. Nothing about the code in your repo changes between local and cloud runs. ## Where to go next `motus deploy` end to end: project setup, secrets, Git-based deploys. Sessions, webhooks, and the REST API that both local and cloud expose. Where shell commands in deployed agents run. One per session, managed for you. How spans are captured, what the dashboard shows, export options. Project settings, environment variables, `.env` support. # Cloud Sandbox Source: https://docs.motus.lithosai.com/cloud/sandbox Where your deployed agent's shell commands actually run. One sandbox per session, managed for you. When your agent on Motus Cloud runs a shell command, that code does not execute in the agent's own process. It runs inside a cloud sandbox: a network-isolated Linux container that the platform boots for each session and tears down when the session ends. You do not provision it, resize it, or SSH into it. You keep writing `get_sandbox()` the same way you do locally, and `get_sandbox()` returns a handle pointing at the right container. ## Mental model ### Why a sandbox at all Agent tools often shell out, install packages, and leave files on disk. A deployed agent needs its own predictable Linux environment for that work: stable toolchain, scratch filesystem, no crosstalk between conversations, no side effects on the rest of the platform. A sandbox is that environment. Code runs inside; the agent process and the rest of the platform stay outside. On Motus Cloud that environment is a per-session container. It is the only place on the cloud where `sb.sh(...)` calls actually land. ### One sandbox per session The cloud hierarchy on [Motus Cloud](/cloud/overview) is `Project → Session → Trace → Span`. A session is one ongoing conversation between a user and your agent. The sandbox hangs off the session: ``` Session ├── Sandbox (one, serves every request in the session) └── Traces (one per request, runs tool calls in the sandbox) ``` When a session is created, the platform records a sandbox for it but does not boot the container yet. The first time your agent actually runs something, the platform starts the container. Every later turn in the same session reuses it. When the session is deleted, the sandbox is deleted with it. You never start or stop a sandbox from agent code. It follows the session. ### Pause, not delete Sandboxes do not stay running indefinitely. About an hour after the container boots, the platform pauses it. The container is torn down, but the workspace directory survives on persistent storage. The next time your agent calls in, the platform boots a fresh container against the same workspace and your files are right where you left them. From your code this is invisible: `sb.sh(...)` just works. The first call after a resume may take an extra second or two to warm up. ### What persists, what does not | Where | Survives pause? | | ------------------------------- | ------------------------------------------------- | | `/home/agent/workspace` | Yes. Persists for the entire session lifetime. | | Anywhere else on the filesystem | No. Rebuilt from the base image on every resume. | | Background processes | No. Anything you left running dies at pause time. | Rule of thumb: write state you want to keep under `/home/agent/workspace`. Everything else is fair game for the platform to recycle. ## Using it from your agent The API is the same as the [local sandbox](/concepts/sandbox). You call `get_sandbox()` and you get a sandbox object back. ```python theme={null} from motus.tools import tool, get_sandbox @tool async def run_command(command: str) -> str: """Run a shell command inside the sandbox and return its output.""" with get_sandbox() as sb: return await sb.sh(command) ``` Locally, `get_sandbox()` spins up a Docker container. On Motus Cloud, it returns a `CloudSandbox` handle pointed at the session's already-provisioned container. Same code, different backend. On cloud, leaving the `with` block closes the Python-side handle but does not tear down the container. The sandbox is still there for the next tool call in the same session. ### What you can do in it | Method | What it does | | ----------------------------------- | --------------------------------------------------------------------------------------------- | | `sb.sh("command")` | Run a shell command. Returns combined stdout and stderr as a string. | | `sb.python("script")` | Shortcut for running a short Python snippet. | | `sb.exec(*cmd, input=, cwd=, env=)` | General form. Arbitrary command with optional stdin, working directory, or per-call env vars. | A non-zero exit does not raise. The output comes back as a string and the caller inspects it. Commands are capped at 300 seconds server-side. ### What comes pre-installed Alpine Linux with the usual agent-workflow tools: * **Runtimes**: Python 3, bash * **Network tools**: curl, git, openssh-client * **Build tools**: gcc, make, build headers * **Everyday utilities**: vim, tmux, jq, less, sudo You run as `agent`, a non-root user with passwordless sudo if you need it. For anything missing, `apk add` or `pip install` at runtime. Cloud uses a platform-provided image. `get_sandbox(image=..., dockerfile=..., ports=..., mounts=..., connect=..., env=...)` kwargs are accepted for code compatibility but ignored at runtime, and `sb.endpoint(port)` is not available. ### Network policy Outbound to the public internet works. `curl`, `git clone`, `pip install`, calls to third-party APIs are all fine. Outbound to private IP ranges is blocked: `10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`, and the `169.254.0.0/16` link-local range. Inbound from the public internet is blocked. The agent can only initiate connections; nothing on the outside can reach in. ## Managing sandboxes in the console The [Motus console](https://console.lithosai.cloud) exposes two entry points into sandboxes. ### The Sandboxes page A list of your account's sandboxes. For each one you see: * **Sandbox ID** * **Status**: `active`, `paused`, `starting`, or `stopping` * **Last active timestamp** You can sort and filter by ID. The page is list-only right now; to get rid of a sandbox you delete the session that owns it. ### The Files panel in the chat playground When you open a deployed agent in the console's chat playground, a **Files** button near the chat header opens a side panel that browses the session's workspace at `/home/agent/workspace`. From there you can: * Navigate the directory tree * Download individual files to your machine Handy for pulling artifacts the agent produced during a conversation. ## Where to go next The abstract `Sandbox` interface and how `DockerSandbox`, `CloudSandbox`, and `LocalShell` all fit under it. Where Project, Session, Trace, and Span fit together. Get your agent running on the cloud where the sandbox is actually used. Gate risky sandbox commands on user approval before they run. # ReActAgent Source: https://docs.motus.lithosai.com/concepts/agents The reasoning-and-acting loop: give the model tools, and let it decide what to do next. `ReActAgent` is one of the two programming models in Motus, alongside [Workflow](/concepts/workflow). It runs a ReAct loop ("reason, then act"): the agent sends your prompt to the model, runs any tools the model asks for, feeds the results back, and repeats until the model returns a final answer. Reach for `ReActAgent` when the problem is open-ended and you want the model to decide what to do next. Research, debugging, coding agents, triage, customer support: anything where you cannot write the plan down in advance. ## Minimal example ```python theme={null} import asyncio from motus.agent import ReActAgent from motus.models import OpenAIChatClient from motus.tools import tool @tool async def weather(city: str) -> str: """Get the current weather for a city.""" return f"22°C and sunny in {city}." agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", system_prompt="You are a helpful assistant.", tools=[weather], ) async def main(): response = await agent("What's the weather in Tokyo?") print(response) asyncio.run(main()) ``` Three pieces fit together here: a [model client](/concepts/models) that talks to the provider, a [tool](/concepts/tools) (`weather`) defined by decorating a plain Python function, and the agent itself wiring them into a loop. If you don't pass `name` when constructing an agent, Motus infers it from your variable name on first call. The agent above is named `"agent"` automatically. This matters for tracing and for using one agent as another's tool. The examples below show only the lines that matter for each feature. Assume they run inside an `async def main()` wrapper like the one above. ## How the loop runs A single call to `await agent(...)` runs this loop until the model has nothing more to ask for. Your prompt is appended to the agent's conversation history. The agent sends the full conversation and the tool schemas to the model and waits for a completion. The completion becomes an assistant message in the history, whether it contains tool calls, a final answer, or both. The agent calls each tool. Because every tool call goes through the runtime as a task, independent tool calls execute concurrently rather than one at a time. Each result is appended to memory as a tool message, keyed to its `tool_call_id`. If the model asked for tool calls, go back to step 2 with the updated history. If it returned a plain response, that response is the final answer and the loop ends. The loop stops early if `max_steps` or `timeout` is exceeded. ## Multi-turn conversations `ReActAgent` is stateful by default. Each call to the agent appends the user message and the assistant reply to its memory, so the next call sees the full history. ```python theme={null} await agent("My name is Alice.") response = await agent("What's my name?") print(response) # "Alice" ``` The default `memory_type="basic"` keeps every message in order. For long conversations that could run into tens of thousands of tokens, switch to `memory_type="compact"`, which summarizes older turns once the token count crosses a threshold so the context window never overflows. See [Memory](/concepts/memory) for the full picture. ### Resetting and forking Call `agent.reset()` to clear the conversation history and start fresh with the same configuration. `agent.fork()` returns an independent copy of the agent with the same configuration and a forked copy of the current conversation. Changes to the fork do not affect the original, so you can branch off a checkpoint and explore alternatives. ```python theme={null} await agent("My name is Alice.") forked = agent.fork() await forked("Call me Bob instead.") await agent("What's my name?") # "Alice" await forked("What's my name?") # "Bob" ``` This is how you run parallel exploratory conversations or A/B comparisons from the same starting point. ## Structured output Pass a Pydantic model as `response_format` and the agent returns a parsed instance instead of a string. Motus uses the provider's strict structured-output mode to guarantee the JSON matches your schema. ```python theme={null} from pydantic import BaseModel class Sentiment(BaseModel): label: str score: float agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", response_format=Sentiment, ) result = await agent("Analyze: 'I love this product'") print(result.label, result.score) # result is a Sentiment instance ``` Structured output composes with tools. The model can still call tools on intermediate steps; only the final assistant message is parsed into your schema. ## Limits Use `max_steps` to cap the number of reasoning-and-acting cycles. Use `timeout` to set a wall-clock deadline in seconds. ```python theme={null} agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", max_steps=5, timeout=30.0, ) ``` Reaching `max_steps` without a final answer raises `RuntimeError`. Exceeding `timeout` raises `TimeoutError`, checked before each new step so the current step finishes first and the execution trace is preserved. Catch these if you need graceful degradation. ## Usage and cost After any call to the agent, you can read token usage, estimated cost, and context window usage directly off the agent object. The counts accumulate across every call in the agent's lifetime, not just the most recent one. ```python theme={null} response = await agent("Explain quantum computing.") agent.usage # {"input_tokens": 1234, "output_tokens": 567, "cache_read_input_tokens": 200, ...} agent.cost # 0.0042 (USD; gateway-reported when present — e.g. OpenRouter or # LithosAI proxy — otherwise computed from tokens via the # bundled pricing table; None if neither is available) agent.context_window_usage # {"estimated_tokens": 1801, "threshold": 150000, "ratio": 0.012, "percent": "1%"} ``` The `threshold` in `context_window_usage` is whatever will trigger memory compaction (from `CompactionMemory`, or a default derived from the model's context window if basic memory is used). | Attribute | What you get | | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `agent.usage` | Accumulated token counts across every LLM call | | `agent.cost` | Total cost in USD — gateway-reported (OpenRouter / LithosAI proxy) when present, otherwise computed from tokens; `None` if neither source is available | | `agent.context_window_usage` | Current working-memory size relative to the compaction threshold | | `agent.get_execution_trace()` | The memory trace as a dict, enriched with usage, model, and cost | ### Streaming intermediate state Pass a `step_callback` to observe the agent in real time. The callback fires after every LLM step that has tool calls, before those tools run. ```python theme={null} async def on_step(content, tool_calls): if content: print(f"Thinking: {content}") for call in tool_calls: print(f"Calling {call['name']}({call['arguments']})") agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[weather], step_callback=on_step, ) ``` This is how `motus serve` streams intermediate state to connected clients. It does not fire on the final step (the one without tool calls); the caller receives the final answer as the return value. ## Using an agent as a tool `agent.as_tool()` wraps an agent so another agent can call it as a regular tool. The caller never knows it's talking to another agent. ```python theme={null} researcher = ReActAgent( client=client, model_name="gpt-4o", name="researcher", system_prompt="You research topics thoroughly.", ) supervisor = ReActAgent( client=client, model_name="gpt-4o", system_prompt="You coordinate research tasks.", tools=[researcher.as_tool(description="Research a topic in depth")], ) ``` By default, each `as_tool()` invocation starts the inner agent with a fresh conversation. Pass `stateful=True` to preserve the inner agent's memory across calls within the same parent run. Other options include overriding the inner agent's `name`, `description`, `max_steps`, and per-call guardrails. You can also pass an agent directly in `tools=[...]` without calling `as_tool()`. Motus wraps it automatically with default settings. See [Multi-agent](/guides/multi-agent) for the full composition guide, including output extractors and other advanced options. ## Guardrails Attach validation functions that run before the agent starts or after it returns. Input guardrails see the user prompt; output guardrails see the final result. A guardrail can do three things: return `None` to let the value through unchanged, return a replacement value, or raise to block the run entirely. ```python theme={null} from motus.guardrails import InputGuardrailTripped def block_profanity(value: str): if "badword" in value.lower(): raise InputGuardrailTripped("Input rejected by guardrail.") return None # pass through; return a string here to rewrite it agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", input_guardrails=[block_profanity], ) ``` When `response_format` is set, output guardrails can declare individual Pydantic fields and rewrite them. See [Guardrails](/guides/guardrails) for the full API, including tool-level guardrails. ## Reasoning The `reasoning` parameter controls extended thinking on models that support it. The default is `ReasoningConfig.auto()`, which enables adaptive thinking on Opus 4.6 and Sonnet 4.6. ```python theme={null} from motus.models import AnthropicChatClient, ReasoningConfig client = AnthropicChatClient() # Adaptive (default): the model decides how much to think agent = ReActAgent(client=client, model_name="claude-opus-4-6") # Lower effort for faster, cheaper responses on adaptive models agent = ReActAgent( client=client, model_name="claude-opus-4-6", reasoning=ReasoningConfig(effort="low"), ) # Explicit token budget (for non-adaptive models like Sonnet 4.5) agent = ReActAgent( client=client, model_name="claude-sonnet-4-5-20250929", reasoning=ReasoningConfig(budget_tokens=5000), ) # Disable thinking entirely agent = ReActAgent( client=client, model_name="claude-opus-4-6", reasoning=ReasoningConfig.disabled(), ) ``` Adaptive models accept `effort="low" | "medium" | "high" | "max"`. Non-adaptive models use `budget_tokens` to set an explicit thinking budget. `ReasoningConfig.disabled()` turns thinking off on any model. ## Prompt caching On Anthropic models, the agent places cache breakpoints on the repeating part of your prompt so that system prompt, tool definitions, and prior conversation turns are read from cache instead of billed as fresh input on every call. `cache_policy` controls how aggressive this is. | Policy | Cache breakpoints | TTL | | ------------------ | ----------------------------------------------------- | --------- | | `"none"` | None | n/a | | `"static"` | System prompt and tool definitions | 5 minutes | | `"auto"` (default) | Static plus the end of the previous conversation turn | 5 minutes | | `"auto_1h"` | Same as `"auto"` | 1 hour | Under `"auto"`, Motus tags the second-to-last user or tool-result message with a cache breakpoint on every call. The net effect is that on step N+1, the entire prompt prefix up to and including turn N is a cache read, and only the latest turn is fresh tokens. `"auto_1h"` is the same strategy with a longer TTL, useful for long-lived agents where the prefix is reused over timescales greater than five minutes. ```python theme={null} from motus.models import AnthropicChatClient, CachePolicy agent = ReActAgent( client=AnthropicChatClient(), model_name="claude-opus-4-6", cache_policy=CachePolicy.AUTO_1H, ) ``` See [Models](/concepts/models) for more on prompt caching and provider support. ## Constructor reference | Parameter | Type | Default | Purpose | | ------------------- | -------------------------------- | ------------------------ | -------------------------------------------------------- | | `client` | `BaseChatClient` | required | LLM provider client | | `model_name` | `str` | required | Model identifier (e.g. `"gpt-4o"`, `"claude-opus-4-6"`) | | `name` | `str \| None` | auto-inferred | Agent name, used in tracing and tool registration | | `system_prompt` | `str \| None` | `None` | System prompt prepended to every LLM call | | `tools` | list, dict, callable, or `Tools` | `None` | Tools available to the agent | | `response_format` | `type[BaseModel] \| None` | `None` | Structured output via a Pydantic model | | `max_steps` | `int` | `20` | Max loop cycles before the agent raises `RuntimeError` | | `timeout` | `float \| None` | `None` | Wall-clock deadline in seconds; raises `TimeoutError` | | `memory_type` | `"basic" \| "compact"` | `"basic"` | Memory strategy, ignored if `memory` is passed | | `memory` | `BaseMemory \| None` | `None` | Custom memory instance, overrides `memory_type` | | `input_guardrails` | `list[Callable]` | `[]` | Hooks on the user prompt before the agent runs | | `output_guardrails` | `list[Callable]` | `[]` | Hooks on the final result | | `reasoning` | `ReasoningConfig` | `ReasoningConfig.auto()` | Extended thinking configuration | | `cache_policy` | `CachePolicy \| str` | `"auto"` | Prompt caching strategy (Anthropic only) | | `step_callback` | `Callable \| None` | `None` | Async callback fired after each LLM step with tool calls | If `name` is not passed, Motus infers it from the variable you assigned the agent to on first call, falling back to the class name. # Memory Source: https://docs.motus.lithosai.com/concepts/memory Manage conversation context with automatic compaction for long-running agents. Memory manages the conversation context that gets sent to the LLM on each call. Without memory management, context grows unbounded until it exceeds the model's context window and causes an API error. Motus handles this automatically with two built-in strategies. ## Memory types | Strategy | Token management | Persistence | Use case | | ------------ | ------------------------------------ | -------------------------- | -------------------------------- | | `basic` | None (grows unbounded) | In-memory only | Short conversations, testing | | `compact` | Auto-compacts at threshold | Optional log-based restore | Production agents, long sessions | | `background` | Auto-compacts + agent-managed memory | Cross-session persistence | Coming soon | Both extend `BaseMemory` and share an async interface: `add_message()`, `compact()`, `get_context()`, and `get_memory_trace()`. ## Architecture ```text theme={null} BaseMemory (abstract) ├── BasicMemory : append-only, no compaction └── CompactionBase (abstract): boundary detection, compact(), set_model() └── CompactionMemory : + conversation log store, session restore ``` `CompactionBase` provides the core compaction logic shared by all compacting memory types: turn boundary detection, token threshold management, and LLM-based summarization. `CompactionMemory` adds conversation log persistence and session restore on top. ### BasicMemory `BasicMemory` is the default. Messages accumulate until the conversation ends. If the context window overflows, the model provider returns an API error. ```python theme={null} agent = ReActAgent(client=client, model_name="gpt-4o", memory_type="basic") ``` You get this when you pass no `memory_type` or `memory` argument. ### CompactionMemory `CompactionMemory` monitors token count after every message. When the estimated token count exceeds a threshold and the conversation is at a turn boundary, it summarizes older turns into a continuation message. The agent loop continues without interruption. Use `memory_type="compact"` for any agent that will handle long conversations or run in production. It prevents context window overflows without any changes to your agent logic. ```python theme={null} agent = ReActAgent(client=client, model_name="gpt-4o", memory_type="compact") ``` ## Configuring CompactionMemory For full control, instantiate `CompactionMemory` directly and pass it via the `memory` parameter: ```python theme={null} from motus.memory import CompactionMemory, CompactionMemoryConfig memory = CompactionMemory( config=CompactionMemoryConfig( compact_model_name="claude-haiku-4-5-20251001", safety_ratio=0.75, ), on_compact=lambda stats: print(f"Compacted {stats['messages_compacted']} messages"), ) agent = ReActAgent(client=client, model_name="gpt-4o", memory=memory) ``` ### CompactionMemoryConfig fields | Field | Default | Description | | ------------------------ | ------------- | --------------------------------------------------------------------------------------------------- | | `compact_model_name` | Agent's model | Model used for the compaction LLM call | | `token_threshold` | `None` | Explicit token threshold. When `None`, derived from the model's context window times `safety_ratio` | | `safety_ratio` | `0.75` | Fraction of the context window that triggers compaction | | `session_id` | Auto UUID | Identifier for the conversation session | | `log_base_path` | `None` | Directory for JSONL conversation logs. `None` disables logging | | `max_tool_result_tokens` | `50000` | Maximum tokens per tool result before truncation | Compaction only triggers at clean turn boundaries to avoid corrupting in-progress tool call sequences. A ReAct agent loop produces three types of turn units: * **Unit A**: `[user message]` * **Unit B**: `[assistant + tool_calls]` followed by `[tool_result x N]` * **Unit C**: `[assistant, no tool calls]` (final response) Compaction defers until all tool results from a parallel tool call batch have arrived. This is tracked via `_pending_tool_calls`, a counter incremented when the assistant issues tool calls and decremented as each result arrives. Compaction fires only when the counter reaches zero. ## Session save and restore When you set `log_base_path`, `CompactionMemory` writes every message and compaction event to a JSONL file. You can restore a previous session from this log: ```python theme={null} from motus.memory import CompactionMemory restored = CompactionMemory.restore_from_log( session_id="user-123", log_base_path="./conversation_logs", ) agent = ReActAgent(client=client, model_name="gpt-4o", memory=restored) # Agent continues with the previous conversation's context ``` `restore_from_log` replays all log entries (messages and compaction events) to rebuild the in-memory state. The restored instance appends to the same session log. For programmatic session persistence without log files, use `CompactionSessionState`: ```python theme={null} from motus.memory import CompactionSessionState # Snapshot current state state = memory.get_session_state() data = state.to_dict() # serialize to a JSON-compatible dict # Restore later restored_state = CompactionSessionState.from_dict(data) ``` `CompactionSessionState` captures the current context window (messages + system prompt) along with session identity and log store location for cross-session continuity. ## Custom compaction function Replace the default LLM-based compaction with your own summarization logic: ```python theme={null} def my_compaction(messages, system_prompt): """Return a summary string from the conversation.""" return f"Summary: {len(messages)} messages processed" memory = CompactionMemory(compact_fn=my_compaction) ``` The function receives the message list and system prompt, and returns a summary string. ## Custom memory Subclass `BaseMemory` and implement `compact()` and `reset()` to build your own strategy: ```python theme={null} from motus.memory import BaseMemory class MyMemory(BaseMemory): async def compact(self, **kwargs): """Implement your compaction strategy.""" ... def reset(self): """Clear all state and return counts.""" count = len(self._messages) self._messages.clear() return {"messages": count} agent = ReActAgent(client=client, model_name="gpt-4o", memory=MyMemory()) ``` The base class provides working memory management, token estimation, tool result truncation, and trace logging. For compacting memory types, extend `CompactionBase` instead. It provides boundary-aware auto-compaction, `set_model()`, and the default LLM summarization logic. ## BackgroundMemory (coming soon) A long-term memory solution that works both locally and on the cloud is under active development. `BackgroundMemory` will extend `CompactionBase` with agent-managed cross-session memory, allowing the main agent to remember facts, preferences, and context across conversations without distraction. # Model Clients Source: https://docs.motus.lithosai.com/concepts/models Connect to any LLM provider through the same `BaseChatClient` interface. A model client is the object that actually talks to an LLM provider. Motus ships four of them (OpenAI, Anthropic, Gemini, OpenRouter), all implementing the same `BaseChatClient` interface. You pick a client, pass it into `ReActAgent`, and switch providers later by changing the import and the model name; the agent code does not move. ```python theme={null} import asyncio from motus.agent import ReActAgent from motus.models import OpenAIChatClient agent = ReActAgent(client=OpenAIChatClient(), model_name="gpt-4o") async def main(): print(await agent("Hello!")) asyncio.run(main()) ``` ## Supported providers | Class | Provider | API key env var | | ---------------------- | ------------------------------------------------------------ | -------------------- | | `OpenAIChatClient` | OpenAI, and any OpenAI-compatible server (Ollama, vLLM, ...) | `OPENAI_API_KEY` | | `AnthropicChatClient` | Anthropic | `ANTHROPIC_API_KEY` | | `GeminiChatClient` | Google (Gemini Developer API or Vertex AI) | `GEMINI_API_KEY` | | `OpenRouterChatClient` | OpenRouter (multi-provider routing) | `OPENROUTER_API_KEY` | Each client reads its env var automatically if you do not pass `api_key`. They all also accept arbitrary `**kwargs` that are forwarded to the underlying provider SDK (`timeout`, `max_retries`, `default_headers`, and so on). ## Creating a client ```python theme={null} from motus.models import OpenAIChatClient client = OpenAIChatClient() client = OpenAIChatClient(api_key="sk-...") ``` ```python theme={null} from motus.models import AnthropicChatClient client = AnthropicChatClient() client = AnthropicChatClient(api_key="sk-ant-...") ``` ```python theme={null} from motus.models import GeminiChatClient client = GeminiChatClient() client = GeminiChatClient(api_key="...") # Vertex AI instead of the Gemini Developer API client = GeminiChatClient( vertexai=True, project="my-project", location="us-central1", ) ``` ```python theme={null} from motus.models import OpenRouterChatClient client = OpenRouterChatClient() client = OpenRouterChatClient(api_key="sk-or-...") ``` ## Local models `OpenAIChatClient` works with any OpenAI-compatible server. Point `base_url` at your local service: ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient # Ollama client = OpenAIChatClient(base_url="http://localhost:11434/v1") # vLLM client = OpenAIChatClient(base_url="http://localhost:8000/v1") agent = ReActAgent(client=client, model_name="llama3.1") ``` No API key is required when the server does not enforce authentication. ## Prompt caching `AnthropicChatClient` supports Anthropic's prompt caching. Set `cache_policy` on the agent; see [Prompt caching on the Agents page](/concepts/agents#prompt-caching) for the full table of options and TTLs. On providers that do not implement prompt caching (OpenAI, Gemini, OpenRouter), `cache_policy` is a no-op. ## Reasoning Models with extended thinking (Opus 4.6, Sonnet 4.6, and others) are controlled by the `reasoning` parameter on the agent. See [Reasoning on the Agents page](/concepts/agents#reasoning) for `ReasoningConfig.auto()`, `effort=`, `budget_tokens=`, and `ReasoningConfig.disabled()`. ## Message and completion types The two types every client reads and writes. `ReActAgent` handles them for you, so most of the time you only need to construct them when you write a custom agent or call a client by hand. ### `ChatMessage` The unified message format that every client reads and writes. Use the factory methods for each role: ```python theme={null} from motus.models import ChatMessage system = ChatMessage.system_message("You are a helpful assistant.") user = ChatMessage.user_message("Hello!") assist = ChatMessage.assistant_message("Hi there!") tool = ChatMessage.tool_message( content="result", tool_call_id="call_123", name="my_tool", ) ``` `user_message` and `assistant_message` accept an optional `base64_image` for vision inputs. ### `ChatCompletion` The return value of `client.create()` and `client.parse()`. The fields a caller usually reads: | Field | Type | What it is | | ------------------- | ------------------------ | --------------------------------------------------------------------------------------------------------- | | `content` | `str \| None` | Text response | | `tool_calls` | `list[ToolCall] \| None` | Tool calls the model requested | | `reasoning` | `str \| None` | Readable chain of thought (when the model emits one) | | `reasoning_details` | `list[dict] \| None` | Provider-specific reasoning blocks, passed back on follow-up calls so the model can continue its thinking | | `finish_reason` | `str` | `"stop"`, `"tool_calls"`, or `"length"` | | `usage` | `dict` | Token counts | | `parsed` | `Any \| None` | Parsed Pydantic object (populated by `parse()`) | | `id` / `model` | `str` / `str` | Response ID and model identifier | Call `completion.to_message()` to turn a completion into a `ChatMessage` you can append to conversation history. ## Calling a client directly Every client implements two async methods. `ReActAgent` calls these for you; you only reach for them when building a custom agent or running a one-off completion. ```python theme={null} import asyncio from motus.models import ChatMessage, OpenAIChatClient client = OpenAIChatClient() async def main(): completion = await client.create( model="gpt-4o", messages=[ChatMessage.user_message("What is 2 + 2?")], ) print(completion.content) asyncio.run(main()) ``` | Method | What it does | | ------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | | `create(model, messages, tools=None, reasoning=..., **kwargs)` | Standard chat completion. Returns `ChatCompletion`. | | `parse(model, messages, response_format, tools=None, reasoning=..., **kwargs)` | Structured output. The completion's `parsed` field holds an instance of `response_format`. | # Architecture Overview Source: https://docs.motus.lithosai.com/concepts/overview A map of the Motus library: the two ways you write agents, the runtime they share, and where each concept lives. Motus is an open source agent serving project. It also ships a Python library for writing the agents you serve. This page is a map of that library: the two programming models it supports, the runtime underneath them, and where to go for each concept. ## Two programming models on one runtime Motus gives you two ways to write an agent, and both run on the same underlying runtime. You pick the one that matches your problem, and the two can be combined when you need to. For exploratory, open-ended problems. The model decides what to do next. You hand `ReActAgent` a client, a set of tools, and a system prompt. It calls the model, runs any tool calls the model asks for, feeds the results back, and loops until the model returns a final answer. For stable control over a known-good procedure. You decide what to do next. Decorate plain Python functions with `@agent_task`, call them, and Motus turns the data flow between them into a parallel task graph. No DAG wiring, no YAML. Roughly, the two shapes look like this: ```python ReActAgent theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[weather, search], ) answer = await agent("What's the weather in Tokyo?") ``` ```python Workflow theme={null} from motus.runtime import agent_task, resolve @agent_task def fetch(url): ... @agent_task def summarize(pages): ... pages = [fetch(url) for url in urls] # parallel fetches result = resolve(summarize(pages)) # runs once every fetch is done ``` ### When to reach for which Reach for `ReActAgent` when the problem is open-ended and you want the agent to discover the right steps on its own. Research, debugging, triage, coding agents, customer support, anything where you cannot write down the plan in advance. You give it tools and let the model explore. Reach for `Workflow` when the procedure is already a solved problem and you want stable, repeatable control over it. ETL pipelines, evaluation harnesses, content processing, batch LLM jobs, anything where the steps are well understood and you mostly want parallelism, retries, and observability around them. The two compose. A workflow step can call a `ReActAgent` when part of an otherwise stable pipeline needs exploration. An agent can delegate to another agent via `as_tool()` when one exploratory loop should hand off to a more specialized one. ### The runtime underneath both Both programming models run on the same task scheduler. When a `ReActAgent` calls the model or runs a tool, that call is submitted as an `@agent_task` under the hood, which is the same path you are using directly when you decorate Workflow steps. Independent tasks run in parallel, each returns an `AgentFuture` that carries its result and its dependencies, and retries, timeouts, and cancellation all live at this level. This is what makes a `ReActAgent`'s parallel tool calls actually parallel, and what lets you mix both styles in one project without adopting a second execution system. ## The pieces ### ReActAgent * [Model clients](/concepts/models): OpenAI, Anthropic, Gemini, OpenRouter, and local models via `base_url` * [Tools](/concepts/tools): `@tool`, `@tools`, function tools, class tools, Docker sandboxes * [MCP tools](/guides/mcp-integration): wrap any MCP server with `get_mcp()` * [Memory](/concepts/memory): `BasicMemory` and `CompactionMemory` * [Guardrails](/guides/guardrails): input, output, and per-tool validation hooks * [Reasoning and cache policy](/concepts/models): extended thinking and prompt caching * [Multi-agent](/guides/multi-agent): `as_tool()` and `fork()` * [Human in the loop](/guides/human-in-the-loop): pause mid-turn for approval or clarification * [Skills](/concepts/skills): load extra instructions and examples on demand ### Workflow * [`@agent_task`](/concepts/workflow): decorate any sync or async Python function * [`AgentFuture`](/concepts/workflow): dependency tracking through function arguments, non-blocking operators * [`resolve()`](/concepts/workflow): block on or await a future * [Retries, timeouts, and policy overrides](/concepts/workflow): per-task and per-call * [Multi-return and per-task hooks](/concepts/workflow): `num_returns`, `on_start`, `on_end` ### Runtime, serving, and tracing * [Runtime](/concepts/workflow#how-the-runtime-runs-it): the task scheduler underneath both programming models (`GraphScheduler`, thread pool, event loop) * [Serving](/guides/serving): `motus serve` locally, `motus deploy` to Motus Cloud, session-based REST API * [Tracing](/guides/tracing): opt-in via one environment variable, lifecycle hooks, HTML viewer, OpenTelemetry export ## Where to go next Pick a starting point based on what you want to build. * **A single LLM-driven assistant.** Start with [Agents](/concepts/agents), then [Tools](/concepts/tools), then [Memory](/concepts/memory). * **A parallel pipeline or batch job.** Start with [Workflow](/concepts/workflow). Circle back to [Agents](/concepts/agents) if any step needs an LLM. * **A team of agents.** See [Multi-agent](/guides/multi-agent) for how `as_tool()` turns one agent into a tool for another. * **An agent that needs external tools.** See [MCP Integration](/guides/mcp-integration) to plug in any MCP server. * **An existing agent from another framework.** See the [Integrations](/integrations/openai-agents) tab for OpenAI Agents SDK, Anthropic SDK, and Google ADK adapters. * **Shipping any of the above.** See [Serving](/guides/serving) for the REST API and [Deployment](/guides/deployment) for the cloud workflow. # Sandbox Source: https://docs.motus.lithosai.com/concepts/sandbox A safe, disposable place for agents to run code, shell commands, and long-lived processes. Same API locally and in the cloud. Any time an agent runs a shell command or executes Python, the interesting question is *where*. Your laptop is fine for prototyping and wrong for almost everything else: a stray `rm -rf`, a runaway `pip install`, or a model that decides to `curl` a dubious URL reaches straight into your machine. A sandbox is the boundary that prevents that. `Sandbox` is an abstract execution environment. You create one, run commands in it, move files in and out, and close it when you are done. Because the interface is a clean abstract class, a sandbox can be backed by anything: a local Docker container, a remote machine, a microVM, a serverless runtime, your own in-house isolation technology. Motus ships two reference backends so you do not have to start from scratch: * **`DockerSandbox`** runs work inside a local Docker container. Great for local development and self-hosted deployments. * **`CloudSandbox`** talks to a remote sandbox over a REST API. Used automatically when your agent runs on [Motus Cloud](/cloud/overview); see [Cloud Sandbox](/cloud/sandbox) for the cloud-specific behavior. **The same `get_sandbox(...)` call works in both places**. You get a `DockerSandbox` on your laptop and a `CloudSandbox` when the deploy target is Motus Cloud. If neither fits, implement the `Sandbox` interface yourself and callers of `get_sandbox()` keep working unchanged. ## Quick start ```python theme={null} import asyncio from motus.tools import get_sandbox async def main(): with get_sandbox(image="python:3.12") as sb: print(await sb.sh("echo hello from the sandbox")) print(await sb.sh("ls /tmp")) asyncio.run(main()) ``` That's the whole loop. `get_sandbox()` picks the right backend, the `with` block tears it down on exit, and each `exec`/`sh` call returns the command's output as a string. ## Local and cloud, one call Your code makes the same `get_sandbox(...)` call in both environments. Locally it returns a `DockerSandbox`. When the same agent runs on Motus Cloud, it returns a `CloudSandbox` that talks to a sandbox the cloud side manages for you. The switch happens automatically. Motus Cloud currently runs your agent inside a fixed, Motus-provided sandbox image. See [Cloud Sandbox](/cloud/sandbox) for the cloud-specific lifecycle, limits, and console UI. ## Creating a sandbox `get_sandbox()` is the recommended entry point. It manages a global provider behind the scenes, so repeated calls in the same process do not spin up redundant infrastructure. ```python from an image theme={null} with get_sandbox(image="python:3.12") as sb: ... ``` ```python from a Dockerfile theme={null} with get_sandbox(dockerfile="./sandbox") as sb: ... ``` ```python with mounts and env theme={null} with get_sandbox( image="node:20", mounts={"/local/project": "/workspace"}, env={"NODE_ENV": "production"}, ) as sb: ... ``` ```python with port mapping theme={null} # {8080: None} maps container port 8080 to a random host port. with get_sandbox(image="node:20", ports={8080: None}) as sb: url = sb.endpoint(8080) # "http://localhost:" ``` ```python attach to existing container theme={null} with get_sandbox(connect="my-dev-container") as sb: # Closing the sandbox will NOT stop or remove a connected container. ... ``` For fine-grained control, the `DockerSandbox` class is a direct factory with the same options plus an async variant: ```python theme={null} from motus.tools import DockerSandbox async with await DockerSandbox.acreate("python:3.12", mounts={"/tmp/data": "/data"}) as sb: await sb.sh("ls /data") ``` ### Parameter reference | Parameter | Type | Description | | ------------ | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `image` | `str` | Docker image to run. Default `"python:3.12"`. | | `dockerfile` | `str \| None` | Path to a directory with a Dockerfile. Built on creation. | | `name` | `str \| None` | Container name. If provided, must be unique. | | `env` | `dict[str, str] \| None` | Environment variables inside the container. | | `mounts` | `dict[str, str] \| None` | Host-to-container bind mounts (`{"/local": "/inside"}`). | | `ports` | `dict[int, int \| None] \| None` | Container-to-host port mapping. `None` value picks a random host port. | | `connect` | `str \| None` | Docker backend only. Attach to an existing container by name or id. When set, `image` and `dockerfile` are ignored. | ## What you can do in a sandbox Every sandbox exposes the same small surface, built on `exec()`. | Method | Purpose | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `exec(*cmd, input=, cwd=, env=)` | Run an arbitrary command. Returns the command output as a string (Docker and Cloud interleave stdout + stderr; `LocalShell` returns stdout on success and both streams on non-zero exit). | | `python(script)` | Shortcut for `exec("python3", "-c", script)`. | | `sh(command)` | Shortcut for `exec("sh", "-c", command)`. | | `put(local_path, sandbox_path)` | Copy a file in. | | `get(sandbox_path, local_path)` | Copy a file out. | | `endpoint(port)` | URL to reach a service listening on `port` inside the sandbox. | ```python theme={null} async def pip_install_and_verify(): with get_sandbox(image="python:3.12") as sb: await sb.put("./requirements.txt", "/app/requirements.txt") await sb.sh("cd /app && pip install -r requirements.txt") version = await sb.sh("python3 -c 'import requests; print(requests.__version__)'") # get() writes to disk and returns the resolved local path. local_path = await sb.get("/app/output.json", "./output.json") ``` A non-zero exit code does not raise; the command output is returned as a string so the caller can inspect it. This matches the way a terminal behaves and lets the agent handle a failure the same way it handles any other tool output. ## Handing a sandbox to an agent A `Sandbox` is both a Python object you can drive yourself and a tool collection an agent can call. Two common patterns: ### Pass the sandbox directly ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient from motus.tools import get_sandbox with get_sandbox(image="python:3.12") as sb: agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[sb], ) await agent("Use python to compute the 50th Fibonacci number.") ``` Motus extracts the sandbox's `python` and `sh` methods and exposes them as tools (`Sandbox` is declared with `@tools(allowlist={"python", "sh"})`). ### Use the full `builtin_tools` suite `builtin_tools(sandbox=sb)` wraps the sandbox in a richer developer toolkit: `bash`, `read_file`, `write_file`, `edit_file`, `glob_search`, `grep_search`, a todo list, and (when `skills_dir=` is passed) `load_skill`. See [Tools](/concepts/tools) for how these fit together. ```python theme={null} from motus.tools import builtin_tools, get_sandbox with get_sandbox(image="python:3.12") as sb: agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[*builtin_tools(sandbox=sb)], ) await agent("Find every TODO in /workspace and open an issue for each.") ``` Call `builtin_tools()` with no argument and it binds to a `LocalShell`, which runs commands directly on the host. Convenient for prototyping; not suitable once the model is running code you did not write yourself. ## Lifecycle Context managers are the recommended path. Motus supports both sync and async: ```python theme={null} # Sync with get_sandbox(image="python:3.12") as sb: ... # sb.close() runs on exit # Async async with await DockerSandbox.acreate("python:3.12") as sb: ... # sb.aclose() runs on exit ``` For long-lived sandboxes driven from tests or a custom orchestrator, you can also close manually: ```python theme={null} sb = DockerSandbox.create("python:3.12") try: ... finally: sb.close() ``` ### Ownership Sandboxes created with `create()` / `acreate()` own the underlying container: closing them attempts to stop and remove it. Sandboxes obtained with `connect(name)` do not own the container and will leave it running when closed. This is what you want when your sandbox is a shared dev environment rather than a disposable workspace. ## Backends The default on your laptop and on self-hosted deploys. Requires a running Docker daemon. Supports images, Dockerfiles, bind mounts, port mapping, and attach-to-existing. The first time Motus brings up the `DockerToolProvider` in a process, it checks for a `ghcr.io/lithos-ai/sandbox` image (a Python base with common utilities) and builds it locally from the bundled Dockerfile if missing. This check only runs once per provider. Your `get_sandbox(image=...)` call is separate: it spins up whatever image you name. Used automatically when your agent runs on Motus Cloud. It does not create or destroy containers itself; it is an HTTP client for a sandbox managed by the cloud side. `exec`, `put`, and `get` all round-trip over the network, so you do not need Docker on the host. See [Cloud Sandbox](/cloud/sandbox) for the lifecycle, limits, and console UI. A zero-setup backend that runs commands directly on the host via `subprocess`. Not safe for untrusted code. Reach for it in dev, demos, or tests where the cost of booting a container is higher than the risk. ```python theme={null} from motus.tools import LocalShell async def main(): with LocalShell(cwd="/tmp/work") as sh: print(await sh.sh("ls")) ``` `LocalShell` implements the same `Sandbox` interface as Docker and cloud, so swapping to a real sandbox later is a one-line change. ## Where to go next How the `@tool` decorator, `builtin_tools`, and sandbox fit together. Per-session containers on Motus Cloud: lifecycle, limits, and console UI. Running MCP servers inside a sandbox when you want their side effects contained. Require approval before the agent runs commands that write to disk or reach the network. # Skills Source: https://docs.motus.lithosai.com/concepts/skills Self-contained instruction packs an agent loads on demand, instead of bloating the system prompt with every procedure it might ever need. A useful agent often needs many different procedures at its fingertips: how to review code, how to deploy a service, how to onboard a new user, how to file a support ticket. Put all of that into the system prompt and the prompt swells: the model's attention gets diffuse and you burn tokens on every call for instructions the current request does not need. That is what skills are for. Each skill is a folder on disk with a `SKILL.md` file that carries a short description and a longer instruction body. The agent only sees the descriptions up front. When a user request matches one, the agent calls `load_skill(...)` to pull in the full instructions (and any companion files) just for that turn. ## Anatomy of a skill A skill is a directory that contains at least a `SKILL.md` file. Everything else in the folder (reference docs, checklists, templates) is optional and lives alongside it. ``` skills/ code_review/ SKILL.md review_checklist.md deploy/ SKILL.md deploy-reference.md research/ SKILL.md ``` `SKILL.md` is markdown with YAML frontmatter: ```markdown theme={null} --- name: code_review description: Review code for bugs, style issues, and improvement opportunities --- # Code Review Skill When asked to review code, follow this checklist: 1. **Correctness** - Identify bugs, edge cases, off-by-one errors. 2. **Security** - Check for injection, auth issues, secret exposure. 3. **Performance** - Flag unnecessary allocations, N+1 queries, missing indexes. 4. **Readability** - Suggest clearer naming, simpler control flow. See `review_checklist.md` in this directory for the full checklist. ``` Both frontmatter fields are optional but you should fill them in: | Field | Purpose | Default | | ------------- | --------------------------------------------------------------- | -------------- | | `name` | Identifier the agent uses to load this skill. | Directory name | | `description` | One-line summary the agent reads when deciding whether to load. | Empty string | Everything after the frontmatter is the instruction body, in plain markdown. The agent sees that text verbatim once the skill is loaded, so write it the way you would write instructions for a careful but junior teammate. ## Wiring skills into an agent Pass `skills_dir` to `builtin_tools()` and the agent picks up a `load_skill` tool automatically: ```python theme={null} import asyncio from motus.agent import ReActAgent from motus.models import AnthropicChatClient from motus.tools import builtin_tools async def main(): agent = ReActAgent( client=AnthropicChatClient(), model_name="claude-haiku-4-5-20251001", system_prompt="You are a helpful coding assistant. When a user request matches an available skill, load it first to get detailed instructions.", tools=builtin_tools(skills_dir="path/to/skills/"), ) print(await agent("Review this code for bugs:\ndef divide(a, b):\n return a / b")) asyncio.run(main()) ``` The `load_skill` tool shows up next to the other built-in tools. Without `skills_dir`, `load_skill` is simply absent and the rest still work. Directories that do not exist log a warning and yield zero skills. Subdirectories without a `SKILL.md` are silently skipped. A subdirectory with a malformed `SKILL.md` logs a warning and is skipped. YAML keys beyond `name` and `description` (like `version:`) are parsed but ignored. Calling `load_skill("bogus_name")` returns a helpful error string listing the available skills, rather than raising. ## What the agent actually sees When you wire skills in, the `load_skill` tool's description is auto-generated and lists every skill by name and description: ```text theme={null} Load detailed instructions for a skill. Skills are self-contained units of knowledge and instructions. Load a skill when the user's request matches one of the available skills. Available skills: - code_review: Review code for bugs, style issues, and improvement opportunities - deploy: Deploy an agent to the Motus cloud - research: Conduct thorough research on a topic using search and file tools Returns the skill's instructions as markdown text, along with the skill directory path for accessing companion files via file tools. ``` That listing is what drives the model's routing decision, which is why the `description` field in your `SKILL.md` is load-bearing. A vague description like `"Deployment stuff"` usually loses to a specific one like `"Deploy an agent to the Motus cloud"`. ## How a turn with skills unfolds `builtin_tools(skills_dir=...)` scans the directory once, parses every `SKILL.md` it finds, and builds the `load_skill` tool. The agent never re-scans at runtime. The model reads the tool descriptions (including the skill listing) and decides whether the user's request matches a skill. If yes, it calls `load_skill("skill_name")`. The tool returns a string that starts with `Skill directory: /abs/path/to/skills/code_review` and then the full instructions. The directory path is right there, so the agent can reach for its file tools (`read_file`, `glob_search`) to pull in companion files from the same folder as it needs them. A skill that the agent never loads costs nothing beyond the one-line description in the `load_skill` tool definition. Skills are instruction-shaped, not action-shaped. Loading a skill only adds text to the conversation; it does not fire other tools or mutate state. If a skill tells the agent to run a command, the agent still has to call the relevant tool itself. Skills are scoped per agent (whatever `skills_dir` you passed to that one `builtin_tools` call) and are not dynamic: new `SKILL.md` files dropped into the folder after construction do not show up until you rebuild the agent. ## A concrete example Here is what a request looks like end to end with the [bundled example](https://github.com/lithos-ai/motus/tree/main/examples/skills): ```text theme={null} User: Review this code for bugs: def divide(a, b): return a/b # The request matches the code_review skill, so the agent loads it first. Agent calls: load_skill(skill_name="code_review") Tool returns: Skill directory: /abs/path/to/skills/code_review # Code Review Skill When asked to review code, follow this checklist: 1. Correctness - ... 2. Security - ... ... See `review_checklist.md` in this directory for the full checklist. Agent calls: read_file(path="/abs/path/to/skills/code_review/review_checklist.md") Tool returns: (full checklist contents) Agent: (produces the review, following the loaded checklist) ``` The agent only paid for the full checklist on turns that actually needed it. ## Writing skills that work The `description` field is load-bearing. It is the only thing the model sees before deciding whether to load the skill, so write it like a good commit subject: specific, verb-forward, scoped. A few habits that pay off: * **One skill per task.** Separate `deploy` and `rollback` skills are easier to match and safer to follow than a single `deployment` skill that tries to do both. A big skill tends to get loaded for everything adjacent, and the agent wanders. * **Push heavy reference material into companion files.** `SKILL.md` should cover the workflow and link out; a 400-line API table belongs in a companion file the agent reads only when it needs it. * **Write instructions like you would for a careful junior.** Numbered steps, bold labels, explicit output formats. The agent follows these as directives, not as suggestions. ## Skills vs. system prompt vs. tools They all add behavior, but they answer different questions. | Mechanism | When to reach for it | | ----------------- | ----------------------------------------------------------------------------------------------------- | | **System prompt** | Identity, global rules, the agent's persona. Always in context. | | **Tools** | Actions with side effects (run a command, read a file, call an API). | | **Skills** | Procedures the agent occasionally needs to follow, not always. Instruction-shaped, not action-shaped. | If a piece of guidance fits on one line and applies to every turn, put it in the system prompt. If it is a procedure that only some requests need, make it a skill. ## Where to go next How built-in tools, custom tools, and guardrails fit together. Wiring agents, system prompts, and memory. What stays in context across turns, and how to keep it bounded. Runnable `research` and `code_review` skills with an agent that uses them. # Tools Source: https://docs.motus.lithosai.com/concepts/tools Give your agent the ability to do things: call functions, run shell commands, query an MCP server, or delegate to another agent. A tool is any Python callable a `ReActAgent` can invoke to do something outside the LLM. In practice that means: * A plain Python function * A method on a class * Another agent (wrapped as a callable) * A method exposed by an MCP server Pass any of these into `tools=[...]` and the agent sees them as named, schema-typed functions it can call by name. ## Functions are tools The fastest way to give an agent a tool is to pass a plain Python function. ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient async def search(query: str) -> str: """Search the web for a query.""" return await web_search(query) agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[search], ) ``` Motus reads the name, the docstring, and the type hints and builds the JSON Schema the LLM sees. Sync and async functions both work. Parameters without type annotations must have a default value. Adding docstrings to functions and descriptions to parameters is strongly recommended so the model understands when and how to call your tool. ## The `@tool` decorator Use `@tool` to customize how a function is exposed to the model: its name, description, schema, guardrails, an approval gate, or lifecycle hooks. ```python theme={null} from motus.tools import tool @tool(name="web_search", description="Search the web and return the top result.") async def search(query: str) -> str: return await web_search(query) ``` `tool()` also works as a post-hoc patcher on a callable you don't own: ```python theme={null} configured = tool(third_party_fn, name="fetch", input_guardrails=[validate_url]) ``` | Option | What it does | | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | | `name` | Tool name the model sees (default: function name) | | `description` | Tool description (default: docstring) | | `schema` | Input schema override. Accepts a Pydantic model, an `InputSchema` subclass, or a raw JSON Schema dict. | | `input_guardrails` / `output_guardrails` | Functions that validate or rewrite input and output. See [Guardrails](/guides/guardrails). | | `requires_approval` | When `True`, the agent pauses before running the tool and waits for human confirmation. See [Human in the Loop](/guides/human-in-the-loop). | | `on_start`, `on_end`, `on_error` | Per-tool lifecycle hook callbacks. See [Tracing](/guides/tracing). | ## Describing parameters You have three ways to describe what a tool's arguments mean. Pick whichever is most convenient. ### Pydantic model (recommended) Define a `BaseModel` subclass with `Field` descriptors. This gives you validation constraints, nested objects, enums, and rich descriptions all in one place. ```python theme={null} from pydantic import BaseModel, Field class Filter(BaseModel): field: str = Field(description="Column to filter on") value: str = Field(description="Value to match") class SearchInput(BaseModel): query: str = Field(description="The search query") filters: list[Filter] = Field(default=[], description="Optional filters") max_results: int = Field(ge=1, le=50, default=10, description="Max results") @tool(schema=SearchInput) async def search(query: str, filters: list, max_results: int = 10) -> str: ... ``` Nested models like `Filter` are expanded into the JSON Schema the model sees. The function signature is what Motus calls when the tool runs, so make sure the field names match the parameter names. ### `Annotated` for inline descriptions A lighter alternative when you only need to add descriptions and do not need validation. Add a string after the type in an `Annotated` wrapper. ```python theme={null} from typing import Annotated async def search( query: Annotated[str, "The search query"], max_results: Annotated[int, "Max results to return"] = 10, ) -> str: ... ``` ### Raw JSON Schema For exact control over the schema the model sees, pass a dict directly. ```python theme={null} @tool(schema={ "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }) async def search(query: str) -> str: ... ``` ### Type mapping When Motus infers the schema from your type hints, this is the mapping it uses. | Python | JSON Schema | | ----------------------------------- | ---------------------------------------- | | `str`, `int`, `float`, `bool` | `string`, `integer`, `number`, `boolean` | | `list[T]` | `array` with `items` | | `dict[str, T]` | `object` with `additionalProperties` | | `T \| None` | `anyOf [T, null]` | | `BaseModel`, `TypedDict`, dataclass | `object` with `properties` | | `Annotated[T, "desc"]` | schema of `T` plus `description` | ## Tool collections from a class Group related tools into a class with the `@tools` decorator. Public methods become tools automatically, and `self` is stripped from the schema. ```python theme={null} from motus.tools import tools @tools(prefix="db_") class DatabaseTools: def __init__(self, conn_string: str): self.conn = connect(conn_string) async def query(self, sql: str) -> str: """Execute a SQL query.""" return str(self.conn.execute(sql)) async def insert(self, table: str, data: dict) -> str: """Insert a row.""" ... agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[DatabaseTools("postgres://...")], ) # Exposes "db_query" and "db_insert" to the model. ``` `tools()` also works as a function call on an instance you don't own, for cases where you want to pick specific methods: ```python theme={null} tools(some_instance, allowlist={"get", "list"}, prefix="api_") ``` | Option | What it does | | ---------------------------------------- | -------------------------------------------------- | | `prefix` | Prepended to every tool name | | `include_private` | Expose methods starting with `_` (default `False`) | | `allowlist` / `blocklist` | Filter methods by name | | `method_schemas` | Per-method schema overrides | | `method_aliases` | Rename methods | | `input_guardrails` / `output_guardrails` | Default guardrails applied to every method | A per-method `@tool` overrides the class-level `@tools` options for that one method: ```python theme={null} @tools(prefix="db_", input_guardrails=[log_all]) class DatabaseTools: @tool(name="raw_query", input_guardrails=[validate_sql]) async def query(self, sql: str) -> str: ... # Exposed as "db_raw_query" with [validate_sql] async def insert(self, table: str, data: dict) -> str: ... # Exposed as "db_insert" with [log_all] ``` ## Built-in tools Motus ships a ready-made set of tools that cover the common needs of a code-writing agent: running shell commands, reading and writing files, searching the filesystem, and keeping a checklist of its own work. Most agents that do anything with code can drop these in without writing their own. Get them from `builtin_tools()`, which works out of the box against your local machine. ```python theme={null} from motus.tools import builtin_tools agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[*builtin_tools()], ) ``` | Tool | What it does | | ------------- | ------------------------------------------------------------------------------------------ | | `bash` | Run a shell command. Default 120s timeout, max 600s, output truncated to 30000 characters. | | `read_file` | Read a file with line numbers. Accepts `offset` and `limit` (default: first 2000 lines). | | `write_file` | Write a file, creating parent directories if needed. | | `edit_file` | Exact string replacement. Fails on ambiguous matches unless `replace_all=True`. | | `glob_search` | Find files by glob pattern. | | `grep_search` | Regex search with context, type filters, and output modes. | | `to_do` | A checklist the agent maintains to track its own progress across a long task. | Pass `skills_dir="..."` to add a `load_skill` tool that lets the agent load self-contained instructions from disk on demand. See [Skills](/concepts/skills). With no sandbox, `builtin_tools()` runs `bash`, `write_file`, and `edit_file` directly against your machine. That means the agent can delete files, install packages, or run any shell command, just like a person with terminal access. Use a sandbox (see below) for untrusted prompts, and use [approval gates](#approval-gates) on tools you do not want the agent running without permission. You can customize an individual built-in tool by re-decorating it. The object returned by `builtin_tools()` has attribute access to every tool, so you can patch just one: ```python theme={null} bt = builtin_tools() tool(bt.bash, description="Only read-only commands", input_guardrails=[no_rm]) agent = ReActAgent(..., tools=[*bt]) ``` ## Sandboxed execution A `Sandbox` in Motus is an execution environment that the built-in tools run inside: you create it, the tools bound to it execute commands there, and you close it when you're done. It is not a tool itself; it is the place tools run. Two implementations ship with Motus: * `LocalShell`, the default, runs commands directly on your host machine. * `DockerSandbox` runs everything inside a container, so a rogue command cannot touch your host. `builtin_tools()` uses `LocalShell` when you do not pass anything. To isolate execution, create a `DockerSandbox` and hand it in: ```python theme={null} from motus.tools import builtin_tools, DockerSandbox with DockerSandbox.create(image="python:3.12") as sandbox: agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[*builtin_tools(sandbox=sandbox)], ) await agent("...") ``` For a managed sandbox with mounts, ports, or a pre-built image, use the `get_sandbox()` factory. It reuses a single global provider under the hood, so repeated calls do not spin up new containers. ## MCP tools Any [Model Context Protocol](https://modelcontextprotocol.io/) server can be used as a tool source via `get_mcp()`. ```python theme={null} from motus.tools import get_mcp # Local stdio server async with get_mcp( command="npx", args=["-y", "@anthropic/mcp-server-filesystem", "/workspace"], ) as session: agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[session], ) await agent("List the Python files under /workspace.") # Remote HTTP server async with get_mcp(url="http://localhost:3000/mcp") as session: ... # Stdio server running inside a Docker sandbox async with get_mcp( image="node:20", command="npx", args=["@playwright/mcp", "--port", "8080"], port=8080, ) as session: ... ``` The session exposes every tool the MCP server advertises. Both patterns for managing the session work. If you pass an unconnected `get_mcp(...)` session to the agent without `async with`, the agent connects it lazily on its first run. Using `async with` gives you deterministic cleanup when the block exits. See [MCP Integration](/guides/mcp-integration) for connection options, filtering, and renaming tools. ## Agents as tools An agent can be another agent's tool. The caller treats it like any other entry in `tools=[...]`. ```python theme={null} researcher = ReActAgent( client=client, model_name="gpt-4o", name="researcher", system_prompt="You research topics in depth.", ) orchestrator = ReActAgent( client=client, model_name="gpt-4o", tools=[researcher], ) ``` For a custom name, description, or stateful memory across calls, use `researcher.as_tool(name="do_research", stateful=True)`. See [Multi-agent](/guides/multi-agent) for the full composition guide. ## Approval gates Mark a dangerous tool with `requires_approval=True` and the agent will pause before running it, emit an approval request to the caller, and resume once the caller approves. ```python theme={null} import os @tool(requires_approval=True) async def delete_file(path: str) -> str: """Delete a file from disk.""" os.remove(path) return f"Deleted {path}" ``` `motus serve` surfaces these pauses through its REST API so a client can prompt the user and respond. See [Human in the Loop](/guides/human-in-the-loop) for the full protocol. ## Registration cheat sheet A quick reference for how to pass each kind of tool to an agent. | What you have | How to pass it | | ------------------------- | --------------------------------------------------- | | A function you own | `tools=[my_func]` | | A function you don't own | `tools=[tool(other_func, name="...")]` | | A class with methods | `tools=[MyClass()]` (class decorated with `@tools`) | | An instance you don't own | `tools=[tools(instance, allowlist={"get"})]` | | An MCP server | `tools=[session]` (from `get_mcp(...)`) | | Another agent | `tools=[other_agent]` or `other_agent.as_tool(...)` | # Workflow Source: https://docs.motus.lithosai.com/concepts/workflow Turn Python functions into a parallel task graph with `@agent_task` and `AgentFuture`. A Workflow is a graph of `@agent_task`-decorated Python functions. You write ordinary Python; Motus infers the data-flow dependencies between your functions and runs the graph in parallel underneath. No DAG definitions, no edge declarations, no YAML. Reach for Workflow when the steps are already known and you want stable, repeatable control over them: ETL pipelines, evaluation harnesses, content processing, batch LLM jobs. For open-ended problems where you want the model to decide what to do next, use a [ReActAgent](/concepts/agents) instead. Both programming models run on the same scheduler. A `ReActAgent`'s model calls and tool calls are themselves `@agent_task`s, which is how the two models share retries, timeouts, cancellation, and tracing, and how a workflow step can freely call a `ReActAgent` (and vice versa). ## Basic example ```python theme={null} from motus.runtime import agent_task, resolve @agent_task def add(a, b): return a + b @agent_task def multiply(x, y): return x * y a = add(3, 4) # returns AgentFuture, not 7 b = multiply(a, 10) # depends on a, runs after a completes print(resolve(b)) # 70 ``` When you call `add(3, 4)`, the runtime registers a task and hands you back an `AgentFuture`, not a concrete value. When you pass that future into `multiply(a, 10)`, the runtime sees the dependency and schedules `multiply` to run after `a` resolves. You write straight-line Python; Motus figures out what can run in parallel and what has to wait. ## Getting a result Three ways to pull the value out of a future, depending on where you are. ```python theme={null} from motus.runtime import resolve # Sync code: block until resolved value = resolve(future) # Block on several futures at once and unpack a, b, c = resolve([future_a, future_b, future_c]) # Block with a timeout value = future.af_result(timeout=5) # raises TimeoutError if exceeded ``` Inside an async context, `await` the future directly instead of blocking: ```python theme={null} @agent_task async def fetch_and_process(url): raw = await download(url) return await parse(raw) ``` Inside an async `@agent_task`, use `await` rather than `resolve()`. Async tasks run on the runtime's own event loop, and calling `resolve()` there raises `RuntimeError` to protect you from a deadlock. Sync `@agent_task`s run in the thread pool off the loop, so `resolve()` works fine inside them. ## Retries and timeouts Pass retry and timeout policy into the decorator. A task that raises (including `TimeoutError`) is re-queued with the same arguments until retries run out. ```python theme={null} @agent_task(retries=3, timeout=10.0, retry_delay=1.0) async def download(url): async with aiohttp.ClientSession() as session: async with session.get(url) as resp: return await resp.json() ``` | Parameter | Type | Default | Purpose | | ------------- | --------------- | ------- | ------------------------------------------------------------------------------------------------ | | `retries` | `int` | `0` | Number of retries after the first failure | | `timeout` | `float \| None` | `None` | Per-execution timeout in seconds. Exceeding it raises `TimeoutError`, which counts as a failure. | | `retry_delay` | `float` | `0.0` | Seconds to wait between attempts | If a task exhausts its retries, the final exception propagates out of the future. `resolve()` or `await` will re-raise it. ## Multi-return When a task produces multiple independent outputs, use `num_returns` to split them into separate futures. ```python theme={null} @agent_task(num_returns=2) def split(data): mid = len(data) // 2 return data[:mid], data[mid:] @agent_task def sum_part(part): return sum(part) left, right = split([1, 2, 3, 4]) # two separate AgentFutures left_total = sum_part(left) # scheduled as soon as `left` is ready right_total = sum_part(right) # scheduled as soon as `right` is ready print(resolve(left_total + right_total)) # 10 ``` ## Per-call policy overrides Override the default policy for one invocation without touching the decorator. ```python theme={null} result = download.policy(retries=5, timeout=30)("https://api.example.com") ``` `.policy()` gives you a one-off variant of the task with different settings. The original task is unchanged, so other call sites keep using its defaults. You can override `retries`, `timeout`, `retry_delay`, and `num_returns` this way. ## Sync and async tasks Both sync and async functions work with `@agent_task`. Sync tasks run in the runtime's `ThreadPoolExecutor`; async tasks run directly on the runtime's event loop. You can wrap any callable this way, whether or not you wrote it and whether or not it is `async`. ```python theme={null} @agent_task def cpu_bound(data): """Runs in a thread pool executor.""" return heavy_computation(data) @agent_task async def io_bound(url): """Runs as a coroutine on the runtime event loop.""" return await fetch(url) ``` ## Class methods `@agent_task` implements the descriptor protocol, so it works on class methods: `self` is bound automatically when the method is accessed through an instance. ```python theme={null} class DataProcessor: def __init__(self, multiplier): self.multiplier = multiplier @agent_task def process(self, value): return value * self.multiplier proc = DataProcessor(3) future = proc.process(10) # AgentFuture, resolves to 30 ``` ## Per-task hooks Attach lifecycle callbacks directly in the decorator. They fire only for this task. For cross-cutting hooks (global, per task type, or per task name), see [Tracing](/guides/tracing). ```python theme={null} def on_start(event): print(f"Starting {event.name}") def on_end(event): print(f"Finished {event.name}: {event.result}") def on_error(event): print(f"Failed {event.name}: {event.error}") @agent_task(on_start=on_start, on_end=on_end, on_error=on_error) def important_task(data): return transform(data) ``` ## Cancellation Cancel a future to stop its task and every task that depends on it downstream. ```python theme={null} from motus.runtime import cancel, cancelled future = long_running_task() cancel(future) # or future.af_cancel() if cancelled(future): # or future.af_cancelled() print("Cancelled.") ``` Cancellation is thread-safe and can be called from any thread, including from inside another `@agent_task`. If the future is already resolved, `cancel()` is a no-op and returns `False`. ## Lifecycle You rarely need to manage the runtime yourself. It auto-initializes on the first `@agent_task` call and cleans up at interpreter exit. The following entry points exist for the cases where you need them: ```python theme={null} from motus.runtime import init, shutdown, is_initialized init() # optional; pre-start the runtime thread and event loop shutdown() # force-stop: cancels in-flight tasks, poisons their futures is_initialized() # True if the runtime is currently running ``` `shutdown()` is a hard stop: it signals the event loop to exit, cancels any in-flight tasks, and poisons their futures with `RuntimeError("Motus runtime is shutting down")`. If you need tasks to finish, `resolve()` or `await` them before calling `shutdown()`. ## Building the graph without blocking Most Python operators on an `AgentFuture` return a new future that extends the graph, so you can keep composing without pulling values out. ```python theme={null} x = add(10, 5) total = x + 100 # AgentFuture, not blocking doubled = x * 2 # AgentFuture, another graph node data = make_data() first = data["scores"][0] # chained getitem, two new futures top_score = data.scores # attribute access is deferred too get_scorer = find_scorer() score = get_scorer(team, year) # calling a future is deferred too ``` `total = x + 100` does not wait for `x` to resolve. It creates a node that executes when `x` is ready. Arithmetic, `__getitem__`, `__getattr__`, `__call__`, and ordering comparisons (`>`, `<`, `>=`, `<=`) all return futures. Some operators force a blocking wait to return a concrete value. See [Sync barriers](#sync-barriers) below. ## Sync barriers A handful of Python operators have to return a concrete value rather than another future. On an `AgentFuture` these trigger a blocking wait to resolve the underlying value. | Operator | Triggered by | | ----------------------- | ---------------------------------------------- | | `__bool__` | `if future:`, `bool(future)`, `not future` | | `__str__` | `str(future)`, `f"{future}"` | | `__len__` | `len(future)` | | `__iter__` | `for x in future:`, unpacking `a, b = future` | | `__int__` / `__float__` | `int(future)`, `float(future)` | | `__eq__` / `__ne__` | `future == x`, `future != x` | | `__hash__` | putting a future in `set()` or as a `dict` key | | `__contains__` | `x in future` | Motus warns every time a sync barrier fires so you can catch accidents. Set `MOTUS_QUIET_SYNC=1` to silence the warnings once you have audited your code. ## How the runtime runs it Under the hood, every `@agent_task` call is submitted to `GraphScheduler`, a small task scheduler that lives in its own thread and drives an async event loop. The scheduler tracks dependencies through `AgentFuture` objects and dispatches each task as soon as its prerequisites are ready. The same scheduler backs `ReActAgent`. Every model call and tool call inside a reasoning loop is wrapped as an `@agent_task` and submitted to `GraphScheduler`, which is why both programming models share retries, timeouts, cancellation, and tracing uniformly, and why `ReActAgent`'s independent tool calls run in parallel. You normally never touch `GraphScheduler` or `AgentRuntime` directly. The decorator is the API. # Code Style Source: https://docs.motus.lithosai.com/contributing/code-style Code conventions for contributing to Motus. Motus enforces code style automatically via ruff. This page documents the conventions that ruff does not catch. ## Automated enforcement ```bash theme={null} uv run ruff check . # Lint uv run ruff format --check . # Format check ``` Ruff handles formatting, import sorting, and pycodestyle. The active rule sets are: | Rule set | What it covers | | -------- | ------------------------------------------ | | **E** | pycodestyle errors | | **F** | pyflakes (unused imports, undefined names) | | **I** | isort (import ordering) | | **W** | pycodestyle warnings | **Ignored rules:** `E501` (line length) and `E402` (module-level import position). The full configuration is in `pyproject.toml` under `[tool.ruff]`. ## Import ordering Organize imports into four groups, separated by blank lines: ```python theme={null} from __future__ import annotations # 1. Future import asyncio # 2. Standard library import logging from pydantic import BaseModel # 3. Third-party from motus.runtime import agent_task # 4. Local/relative if TYPE_CHECKING: # 5. TYPE_CHECKING block from motus.runtime.agent_future import AgentFuture ``` Ruff sorts imports within each group automatically. The `TYPE_CHECKING` block goes last and contains imports used only in type annotations. ## Type annotations Use modern Python 3.12+ syntax throughout: ```python theme={null} # Correct name: str | None items: list[Tool] mapping: dict[str, int] # Incorrect — do not use name: Optional[str] items: List[Tool] mapping: Dict[str, int] ``` Use `TYPE_CHECKING` guards for forward references that cause circular imports: ```python theme={null} from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from motus.runtime.agent_future import AgentFuture ``` ## Naming conventions | Element | Convention | Example | | ------------------- | ------------------ | ------------------------------- | | Classes | PascalCase | `TaskPolicy`, `AgentFuture` | | Functions / methods | snake\_case | `register_hook`, `_deep_unwrap` | | Constants | UPPER\_SNAKE\_CASE | `DEFAULT_POLICY`, `MODEL_CALL` | | Private internals | `_` prefix | `_prerequisites`, `_scan_deps` | | Type aliases | PascalCase | `HookCallback`, `TaskType` | ## Docstrings Use Google style. Required for all public classes and functions. ```python theme={null} def register_hook(event: str, callback: HookCallback) -> None: """Register a lifecycle hook for the given event. Args: event: The event name to listen for (e.g., "task_start"). callback: A callable invoked when the event fires. Raises: ValueError: If the event name is not recognized. """ ``` Docstrings are **not** required for private helpers (prefixed with `_`) or test functions. ## Comments Add comments only where the logic is not self-evident from the code. Do not add comments to code you did not change. Prefer renaming variables or extracting functions over writing explanatory comments. ```python theme={null} # Good — explains a non-obvious constraint # AgentFuture overrides __eq__/__hash__ as sync barriers, # so never use set() or dict keys with AgentFuture instances. pending: dict[int, AgentFuture] = {} # Bad — restates the code # Increment the counter counter += 1 ``` ## String formatting Use f-strings for interpolation. Use explicit `str.format()` only when the template is defined separately from its arguments. ```python theme={null} # Correct msg = f"Task {task.name} failed after {elapsed:.2f}s" # Correct — template defined elsewhere TEMPLATE = "Task {name} completed in {duration}s" msg = TEMPLATE.format(name=task.name, duration=elapsed) ``` # Development Setup Source: https://docs.motus.lithosai.com/contributing/development-setup Get a local Motus checkout running in a few minutes. ## What you need * **[uv](https://docs.astral.sh/uv/getting-started/installation/)** handles packaging, virtual envs, and installs the right Python for you. If you have it, you have everything the project needs on the Python side. * **git** to clone. * **Docker** only if you plan to run or modify the sandbox tests. ## Clone, install, verify ```bash theme={null} git clone https://github.com/lithos-ai/motus.git cd motus ``` ```bash theme={null} uv sync --all-extras ``` `uv` reads `pyproject.toml`, provisions the right Python version, creates a `.venv`, and installs the project with every optional group (`dev`, `docs`, `test`). Re-run any time you pull new dependencies. ```bash theme={null} uv run pre-commit install ``` This registers a git hook that runs **ruff** (linting and formatting) on every commit. To run the checks manually: ```bash theme={null} uv run pre-commit run --all-files ``` ```bash theme={null} uv run pytest tests/unit/ -x -q ``` Unit tests run without API keys or Docker. If something fails here, re-check that `uv sync --all-extras` completed cleanly. The ruff config lives in `pyproject.toml` under `[tool.ruff]`. Any editor ruff integration that respects `pyproject.toml` will pick it up automatically, which is most of them. ## Previewing the docs If you touch anything under `docs/`, preview the change before you open a PR: ```bash theme={null} cd docs npx mintlify dev ``` Opens at [http://localhost:3000](http://localhost:3000) and rebuilds on file changes. ## Next steps * [Code Style](/contributing/code-style) for conventions beyond what ruff enforces. * [Testing](/contributing/testing) before writing your first test. * [Pull Requests](/contributing/pull-requests) when you are ready to submit. # Pull Requests Source: https://docs.motus.lithosai.com/contributing/pull-requests How to submit and review pull requests. Submit changes through pull requests on GitHub. ## Branch and commit ```bash theme={null} git checkout -b your-name/short-description # Make changes, then: git add git commit -m "Brief description of the change" ``` Use the `your-name/short-description` branch naming convention. Keep commit messages concise and focused on *what changed and why*. ## Before submitting ```bash theme={null} uv run ruff check . ``` ```bash theme={null} uv run ruff format --check . ``` ```bash theme={null} uv run pytest tests/unit/ -x -q ``` All three commands must pass. Fix any ruff violations before creating the PR — CI will block merge on lint failures. ## Create the PR ```bash theme={null} gh pr create --title "Brief title" --body "What and why" ``` Or push your branch and create the PR through the GitHub UI: ```bash theme={null} git push -u origin your-name/short-description ``` ## PR guidelines * **Keep PRs focused** — one logical change per PR. Split unrelated changes into separate PRs. * **Include tests** for new functionality. Reviewers will ask for them if they are missing. * **Update documentation** if you change public APIs, add new agent types, or modify configuration. * **New examples** auto-appear in the docs on the next build. No extra docs work needed. * **New VCR cassettes** — if you record cassettes for integration tests, include them in the PR. * **PR title** — use imperative mood: "Add retry logic to task executor", not "Added retry logic". * **PR body** — explain *what* changed and *why*. Link related issues with `Fixes #123` or `Closes #123`. ## Review process 1. Address feedback by pushing **new commits** (do not force-push during review — it erases comment context). 2. Mark conversations as resolved after addressing them. 3. Re-request review after pushing fixes if the reviewer has not re-reviewed. Expect at least one approval before merge. For changes touching runtime internals (`agent_future.py`, `task_instance.py`, `agent_runtime.py`), expect closer scrutiny and possibly multiple reviewers. ## Code owners | Area | Owner | | ------------------------------ | ------------------------- | | Runtime (`src/motus/runtime/`) | @NorthmanPKU | | Agents (`src/motus/agent/`) | @NorthmanPKU, @JackFram | | Tools (`src/motus/tools/`) | @eliotsolomon18, @coppock | | Models (`src/motus/models/`) | @yzhou442 | | Memory (`src/motus/memory/`) | @JackFram, @vasiliskyp | PRs touching a code-owned area will automatically request review from the listed owners. ## CI Tests run automatically on every push to an open PR. The CI pipeline runs: 1. **Ruff** — lint and format checks 2. **Unit tests** — `tests/unit/` 3. **Integration tests** — `tests/integration/` with VCR replay If CI fails: ```bash theme={null} # Check which step failed in the GitHub Actions log, then reproduce locally: uv run ruff check . uv run pytest tests/unit/ -x -v ``` Fix failures before requesting review. Reviewers will not review PRs with failing CI. ## Merging Maintainers merge PRs using **squash-and-merge** by default. Your commit messages become the squash commit body, so write them clearly. After merge, delete your remote branch: ```bash theme={null} git branch -d your-name/short-description git push origin --delete your-name/short-description ``` # Testing Source: https://docs.motus.lithosai.com/contributing/testing Test tiers, VCR cassettes, and async testing patterns. Motus uses pytest with three test tiers: unit, integration (VCR replay), and slow (live API). ## Running tests ```bash theme={null} # Unit tests only (fast, no API keys needed) uv run pytest tests/unit/ -x -q # Integration tests (VCR replay, no API keys needed) uv run pytest tests/integration/ -x -q # All tests uv run pytest ``` ## Test markers | Marker | What it means | API keys needed | | --------------- | ------------------------------------ | ------------------- | | *(none / unit)* | Fast unit tests | No | | `integration` | Uses VCR cassettes for HTTP replay | No (uses fake keys) | | `slow` | Real API calls against live services | Yes | Run a specific marker: ```bash theme={null} uv run pytest -m slow -x -q ``` ## VCR cassette system VCR cassettes record and replay HTTP interactions so integration tests run without live API access. This is the primary mechanism for testing agent behavior end-to-end. ### Where cassettes live ``` tests/integration/examples/cassettes_vcrpy/ ``` Each cassette is a YAML file containing the recorded request/response pairs. ### Replay mode (default, CI) No API keys needed. The `_fake_api_keys` fixture injects placeholder keys so the HTTP client constructs valid-looking requests, and VCR intercepts them before they reach the network. ```bash theme={null} uv run pytest tests/integration/examples/ -x -q ``` ### Record mode To record new cassettes or re-record existing ones, run with real API keys set in your environment: ```bash theme={null} uv run pytest tests/integration/examples/ -v --vcr-record=all ``` Cassettes are automatically scrubbed of: * API keys and authorization headers * Base64-encoded binary blobs * OpenAI reasoning content A custom JSON body matcher normalizes whitespace for stable matching across recording sessions. ### When to record * You add a new integration test that makes HTTP calls * An upstream API changes its response format * You modify agent behavior that changes the request sequence ## Async tests `asyncio_mode = "auto"` is set in `pyproject.toml`. All `async def test_*` functions run automatically without the `@pytest.mark.asyncio` decorator. ```python theme={null} async def test_memory_compaction(): memory = CompactionMemory(model="gpt-4o-mini") await memory.add_message({"role": "user", "content": "hello"}) assert memory.message_count() == 1 ``` For class-based async tests, inherit from `unittest.IsolatedAsyncioTestCase`: ```python theme={null} class TestCompactionMemory(unittest.IsolatedAsyncioTestCase): async def test_auto_compact(self): memory = CompactionMemory(model="gpt-4o-mini") await memory.add_message({"role": "user", "content": "hello"}) await memory._auto_compact() ``` ## Writing a new test Follow this checklist: * **Place unit tests** in `tests/unit//` mirroring the `src/motus/` structure. * **Place integration tests** in `tests/integration/`. * **If your test makes HTTP calls**, record a VCR cassette and commit it with your PR. * **If your test uses the runtime**, call `shutdown()` in teardown to avoid leaked tasks. * **Name test files** with the `test_` prefix (e.g., `test_agent_task.py`). * **Name test functions** with the `test_` prefix describing the behavior under test. ```bash theme={null} # Run your new test in isolation to verify uv run pytest tests/unit/runtime/test_agent_task.py -x -v ``` ## Coverage You can generate a coverage report locally: ```bash theme={null} uv run pytest tests/unit/ --cov=motus --cov-report=term-missing ``` Focus coverage on the code you changed. Full-repo coverage targets are not enforced, but reviewers may ask for tests if you add untested code paths. # Configuration Source: https://docs.motus.lithosai.com/getting-started/configuration Configure Motus using environment variables and an optional motus.toml project file. Motus reads configuration from environment variables at runtime. For project-level settings, you can also create a `motus.toml` file in your project root. ## API keys Set your provider API keys as environment variables before running an agent. You only need the key for the provider you are using. | Variable | Provider | Example value | | -------------------- | ------------- | ------------- | | `OPENAI_API_KEY` | OpenAI | `sk-...` | | `ANTHROPIC_API_KEY` | Anthropic | `sk-ant-...` | | `OPENROUTER_API_KEY` | OpenRouter | `sk-or-...` | | `BRAVE_API_KEY` | Brave Search | | | `JINA_API_KEY` | Jina AI (MCP) | | `OpenAIChatClient` also reads `OPENAI_BASE_URL` if you want to point it at a custom endpoint, such as a local model server: ```bash theme={null} export OPENAI_BASE_URL="http://localhost:11434/v1" ``` `OpenRouterChatClient` reads `OPENROUTER_BASE_URL` to override the default endpoint (`https://openrouter.ai/api/v1`). ## Runtime variables These variables control Motus's logging and tracing behavior. | Variable | Purpose | Default | | ------------------------ | --------------------------------------------------------- | --------------------------- | | `MOTUS_LOG_LEVEL` | Log verbosity: `DEBUG`, `INFO`, `WARNING`, `ERROR` | `DEBUG` | | `MOTUS_QUIET_SYNC` | Suppress sync barrier warnings (`1` to enable) | off | | `MOTUS_TRACING` | Enable trace collection and file export (`1` to enable) | off | | `MOTUS_COLLECTION_LEVEL` | Tracing detail: `disabled`, `basic`, `detailed` | `basic` | | `MOTUS_TRACING_ONLINE` | Enable tracing and the live trace viewer (`1` to enable) | off | | `MOTUS_TRACING_EXPORT` | Write trace files without the live viewer (`1` to enable) | off | | `MOTUS_TRACING_DIR` | Directory to write trace output | `traces/trace_/` | ### Tracing When tracing is enabled, Motus records task execution, tool calls, and model interactions: * **`disabled`** — no trace data is collected. * **`basic`** — task-level events only (start, end, errors). * **`detailed`** — includes model request/response payloads and tool arguments. Set `MOTUS_TRACING=1` to enable collection and file export. To also open the live trace viewer in your browser, use `MOTUS_TRACING_ONLINE=1` instead. If you want file export without the viewer, set `MOTUS_TRACING_EXPORT=1`. ```bash theme={null} # Enable basic tracing with file export export MOTUS_TRACING=1 # Enable detailed tracing with live viewer export MOTUS_TRACING_ONLINE=1 export MOTUS_COLLECTION_LEVEL=detailed ``` ## `motus.toml` Create a `motus.toml` file in your project root to store project-level settings. Motus searches upward from the current working directory until it finds this file. ```toml theme={null} project_id = "my-project" import_path = "myapp:agent" ``` | Field | Description | | ------------- | ------------------------------------------------------------------------ | | `project_id` | Unique identifier for your project, used in deployment and tracing. | | `import_path` | Python import path to your agent instance, in `module:attribute` format. | With `motus.toml` in place, you can omit these values from CLI commands: ```bash theme={null} # Without motus.toml motus serve start --import-path myapp:agent # With motus.toml (reads import_path automatically) motus serve start ``` `motus deploy` creates a `motus.toml` automatically when you first deploy a project. You can also create it by hand. ## `.env` file support Motus does not auto-load `.env` files. If you prefer to manage secrets in a `.env` file, load it yourself at the top of your entry point using `python-dotenv`: ```bash theme={null} pip install python-dotenv ``` Create a `.env` file in your project root: ```bash theme={null} # .env OPENAI_API_KEY=sk-... MOTUS_LOG_LEVEL=INFO ``` Then load it before any Motus imports: ```python theme={null} from dotenv import load_dotenv load_dotenv() # loads .env into os.environ from motus.agent import ReActAgent ``` Call `load_dotenv()` before importing any Motus modules. Environment variables are read when modules initialize, so loading them afterward has no effect. ## Verifying your configuration Run this snippet to confirm your API key is set and the client can initialize: ```python theme={null} import os from motus.models import OpenAIChatClient assert os.environ.get("OPENAI_API_KEY"), "OPENAI_API_KEY is not set" client = OpenAIChatClient() print("Client initialized successfully") ``` If the key is missing or invalid, the client raises an error at request time with a descriptive message. # Installation Source: https://docs.motus.lithosai.com/getting-started/installation Install the Motus library, CLI, and optional coding agent plugin. Motus ships as a single Python package, `lithosai-motus`, that includes both the Python library and the `motus` CLI. You can install it directly with `uv` or `pip`, or let a one line installer set up the library, the CLI, and plugins for Claude Code, Codex, and Cursor all at once. ## Prerequisites You need a Python package manager. Either works: * **uv** (recommended). Installs with `curl -LsSf https://astral.sh/uv/install.sh | sh`. uv manages Python for you, so you do not need to install Python separately. * **pip** with **Python 3.12 or later** already installed (check with `python --version`). That is all you need to install Motus. Provider API keys are not required to install or to deploy. See [Set your API keys](#set-your-api-keys-optional) below if you plan to run agents against your own provider account. ## Install One command installs the Motus CLI, the Python library, and plugins for Claude Code, Codex, and Cursor. ```bash theme={null} curl -fsSL https://www.lithosai.com/motus/install.sh | sh ``` After it finishes, your coding agent understands `/motus serve`, `/motus deploy`, and the rest of the plugin commands. See the [Plugin guide](/guides/plugin) for details. ```bash theme={null} uv add lithosai-motus ``` This installs the Python library and the `motus` CLI into your current uv project. ```bash theme={null} pip install lithosai-motus ``` This installs the Python library and the `motus` CLI into your current Python environment. If you are not already inside a virtual environment, create one first to avoid polluting your global Python install: `python -m venv .venv && source .venv/bin/activate`. ### Bring your existing agent framework Motus works hand in hand with the agent frameworks you already use. If you have agents written with the OpenAI Agents SDK or the Google ADK, keep them. Install the matching extra and Motus serves, deploys, and traces them alongside its own agents with no code changes. ```bash uv theme={null} uv add "lithosai-motus[openai-agents]" ``` ```bash pip theme={null} pip install "lithosai-motus[openai-agents]" ``` ```bash uv theme={null} uv add "lithosai-motus[google-adk]" ``` ```bash pip theme={null} pip install "lithosai-motus[google-adk]" ``` ```bash uv theme={null} uv add "lithosai-motus[openai-agents,google-adk]" ``` ```bash pip theme={null} pip install "lithosai-motus[openai-agents,google-adk]" ``` The Anthropic SDK and Google's `google-genai` client are part of the core install. You do not need an extra to use `motus.anthropic` or the `GeminiChatClient`. ## Verify the installation Confirm the library imports cleanly: ```bash uv theme={null} uv run python -c "from motus.agent import ReActAgent; print('Motus is ready')" ``` ```bash pip theme={null} python -c "from motus.agent import ReActAgent; print('Motus is ready')" ``` You should see `Motus is ready` printed to your terminal. Confirm the CLI is on your PATH: ```bash theme={null} motus --help ``` If `motus` is not found after a pip install, make sure the Python `Scripts/bin` directory is on your PATH, or use `python -m motus` instead. uv users can run the CLI with `uv run motus`. ## Set your API keys (optional) This step is only needed if you want to run agents against your own provider account. Motus Cloud deployments and agents using the Motus model proxy do not need your own keys. Pick a provider and export its key. You only need one. ```bash theme={null} export OPENAI_API_KEY=sk-... # or export ANTHROPIC_API_KEY=sk-ant-... # or export GEMINI_API_KEY=... # or export OPENROUTER_API_KEY=sk-or-... ``` See [Configuration](/getting-started/configuration) for the full list of environment variables, `motus.toml` project settings, and `.env` file support. ## Next steps Create a tool, wire up a model client, and run your first ReAct agent in minutes. API keys, runtime environment variables, and `motus.toml` project settings. # Quickstart Source: https://docs.motus.lithosai.com/getting-started/quickstart Write an agent, serve it locally, and deploy it to Motus Cloud in under 3 minutes. This guide walks through the full Motus loop: write an agent in a few lines of Python, serve it locally as an HTTP API, chat with it from your terminal, then deploy it to Motus Cloud with one command. The code is the same for local and cloud. If you have not installed Motus yet, see [Installation](/getting-started/installation) first. ## Fastest path: let a coding agent drive If you already work in Claude Code, Codex, Cursor, or Gemini CLI, skip the manual loop. One command installs the Motus CLI alongside a `/motus` skill that your coding agent picks up automatically: ```bash theme={null} curl -fsSL https://www.lithosai.com/motus/install.sh | sh ``` After it finishes, describe what you want to your coding agent: > /motus build me a weather agent and deploy it to Motus Cloud. It writes the code, runs `motus serve` to sanity check it, then `motus deploy` to ship, prompting you only when it actually needs a decision. See the [Plugin guide](/guides/plugin) for the full list of skill commands. Prefer to drive the CLI yourself? The rest of this page walks through the same loop by hand. ## 1. Write the agent Create a file called `myapp.py` in an empty directory: ```python myapp.py theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient from motus.tools import tool @tool async def weather(city: str) -> str: """Get the current weather for a city.""" # In a real app, call a weather API here. return f"It is 22°C and sunny in {city}." agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", system_prompt="You are a helpful weather assistant.", tools=[weather], ) ``` That is the whole agent. A few things to notice: * **The `@tool` decorator** turns any Python function into a tool the LLM can call. Type annotations on parameters are required. Motus uses them to generate the JSON schema the model sees, and the docstring becomes the tool description. * **`OpenAIChatClient()`** with no arguments reads `OPENAI_API_KEY` from your environment. You can also pass `api_key="sk-..."` explicitly. Motus ships with clients for OpenAI, Anthropic, Gemini, and OpenRouter. See [Models](/concepts/models) for the full list. * **`ReActAgent`** combines a model client, a model name, and a list of tools into a reasoning loop. Pass a `system_prompt` to give it a persona or standing instructions. * **The `agent` variable** at module level is what Motus will expose when you serve or deploy. The CLI looks it up by the `module:attribute` syntax, as you will see in a moment. Export your provider key before running locally: ```bash theme={null} export OPENAI_API_KEY=sk-... ``` Cloud deployments use the Motus model proxy, so this is only needed for local runs. Want to use Anthropic or Gemini instead? Swap `OpenAIChatClient` for `AnthropicChatClient` or `GeminiChatClient`, and change `model_name` on the `ReActAgent` accordingly (for example `"claude-sonnet-4-5-20250929"` or `"gemini-2.0-flash"`). The rest of the code stays the same. ## 2. Serve it locally Start the agent as an HTTP API with one command: ```bash theme={null} motus serve start myapp:agent --port 8000 ``` This spins up a FastAPI server at `http://localhost:8000` that manages sessions and routes messages to your agent. The `myapp:agent` argument tells Motus to import the `myapp` module and use its `agent` attribute. `myapp:agent` is a Python import path, not a file path. `myapp.py` needs to be in your current directory (or installed as a package) so Python can import it. Nested paths like `mypackage.mymodule:agent` also work. If you see `ModuleNotFoundError: No module named 'myapp'`, make sure you are running the command from the directory that contains `myapp.py`. ## 3. Chat with your local agent Open a second terminal and run: ```bash theme={null} motus serve chat http://localhost:8000 "What's the weather in Tokyo?" ``` You should see something like: ``` It is 22°C and sunny in Tokyo. ``` The agent received your question, called the `weather` tool with `city="Tokyo"`, and used the result to answer in natural language. Drop the quoted message to enter interactive mode: ```bash theme={null} motus serve chat http://localhost:8000 ``` This opens a REPL where you can keep chatting until you hit `Ctrl+C`. Sessions are created automatically and the session ID is printed so you can resume later with `--session ` (sessions are kept on exit, so traces remain viewable). The chat client handles human in the loop pauses for you. If a tool is marked `requires_approval=True`, it prompts you in the terminal before running. See [Human in the Loop](/guides/human-in-the-loop). ## 4. Deploy to Motus Cloud Stop the local server with `Ctrl+C`. Now deploy the exact same code to Motus Cloud. ```bash theme={null} motus deploy --name weather-bot myapp:agent ``` On the first run, `motus deploy` checks whether you are logged in. If not, it opens a browser so you can sign up or sign in and waits for the OAuth flow to finish, then resumes the deploy automatically. Motus then packages your project, uploads it, and streams the build status: ``` queued → building → built → deploying → deployed ``` Once it finishes, your agent is live. The deploy command prints its URL when the build succeeds. The first deploy requires `--name` (or `--project-id`). Motus writes the assigned project ID to `motus.toml` in your project root. On subsequent deploys, just run `motus deploy` with no arguments and it picks up the project from `motus.toml`. The same `motus serve chat` command works against cloud URLs: ```bash theme={null} motus serve chat "What's the weather in Tokyo?" ``` Auth headers are injected automatically from your stored credentials. You do not need to set any provider API keys for cloud deployments: Motus proxies model calls on your behalf. You can also manage the deployment from the [Motus console](https://console.lithosai.cloud): browse builds and sessions, inspect traces, and open the built-in chat UI to smoke test the agent from the browser. For CI or other non-interactive environments, set `LITHOSAI_API_KEY` instead of going through `motus login`. It overrides the credentials file. ## The full loop, recap You just went from an empty directory to a cloud hosted agent: ```bash theme={null} # Write myapp.py (~15 lines) motus serve start myapp:agent --port 8000 # run it locally motus serve chat http://localhost:8000 # chat locally motus deploy --name weather-bot myapp:agent # ship to the cloud (prompts login on first run) motus serve chat # chat with the live agent ``` The same code runs in both places. The same command talks to both URLs. No Dockerfiles, no Kubernetes, no infra changes when you move from laptop to production. ## Next steps Dig into ReActAgent's reasoning loop, memory, guardrails, structured output, and usage tracking. Write tools with `@tool`, wrap class methods with `@tools`, connect MCP servers, or run untrusted code in Docker sandboxes. Session management, worker pools, TTL, webhooks, and the full REST API. Secrets, Git based deploys, ignore rules, and the `motus.toml` project file. # Coding Agent Source: https://docs.motus.lithosai.com/guides/coding-agent A pre-configured Motus agent for software-engineering tasks. Multi-provider, sandboxed, and customizable. `CodingAgent` is a `ReActAgent` subclass preconfigured for software-engineering tasks. It bundles a curated tool set, system prompt, and harness behavior — file I/O, search, shell, web, todos, plan mode, and subagent dispatch — so you can spin up a working coding assistant in a few lines. ```python theme={null} from motus.agent import CodingAgent from motus.models import AnthropicChatClient agent = CodingAgent( client=AnthropicChatClient(), model_name="claude-sonnet-4-6", ) result = await agent("Find the bug in src/parser.py and fix it.") ``` The agent runs against your local shell by default. Pass an explicit `sandbox=` (e.g. a `DockerSandbox`) to run in isolation. The same template works with any chat client Motus supports — `AnthropicChatClient`, `OpenAIChatClient`, `OpenRouterChatClient`, `GeminiChatClient`. Swap the client; the agent code stays the same. ## What's included When you construct `CodingAgent` with no special flags, the agent gets the following tools: | Tool | Purpose | | ------------------------------------ | ---------------------------------------------------------------------------------------------- | | `bash` | Run shell commands (with timeout + output truncation). | | `read_file` | Read a file with line numbers. | | `write_file` | Create or overwrite a file. | | `edit_file` | Exact-string replacement in an existing file. | | `glob_search` | Find files by glob pattern. | | `grep_search` | Search file contents with ripgrep-style options. | | `to_do` | Track a structured task list. | | `web_fetch` | Fetch a URL and extract relevant content via a small LLM. | | `web_search` | Search the web with Brave (requires `BRAVE_API_KEY`). | | `task` | Dispatch self-contained work to a specialized subagent (`general-purpose`, `Explore`, `Plan`). | | `enter_plan_mode` / `exit_plan_mode` | Switch to a read-only investigation phase before making changes. | The system prompt encodes a clear working philosophy: prefer dedicated tools over `bash`, parallel tool calls when independent, file references as `path:line`, anti-overengineering, careful handling of destructive actions, and so on. ## Customizing the system prompt There are five ways to customize the prompt, ordered from least to most invasive. **The first is the recommended default for project-scoped instructions.** ### 1. `AGENTS.md` (recommended) Drop a markdown file at the project root and `CodingAgent` picks it up automatically, wrapping it in a `` block at the end of the system prompt. Use this for **project-specific rules that should travel with the codebase**. ```markdown theme={null} # AGENTS.md - This codebase uses pytest, not unittest. Run tests with `pytest -x`. - All public APIs must have type hints. - Don't import from `internal/` outside its own module. - We use Polars, not Pandas, in new code. ``` `CodingAgent` walks the file at `project_root/AGENTS.md` (defaults to current working directory) plus a fallback `CLAUDE.md`. Both are included if both exist; `AGENTS.md` is rendered first. ```python theme={null} agent = CodingAgent( client=client, model_name="...", project_root="/path/to/repo", # optional; defaults to cwd ) ``` The convention matches the [`AGENTS.md` standard](https://agents.md/), so the same file is portable across any agent that follows it. Prefer `AGENTS.md` over inline customization whenever the rules are about the project rather than your personal preferences. The file lives in `git`, so the rules apply to teammates and CI agents automatically. ### 2. `system_prompt_extra` — append your own block For per-agent rules that don't belong in `AGENTS.md` (personal preferences, agent-specific roles), append text to the end of the default prompt: ```python theme={null} agent = CodingAgent( client=client, model_name="...", system_prompt_extra=""" ## Extra rules - Always run `make lint` before reporting work as complete. - When you finish a task, paste a one-line summary in the format "DONE: ". """, ) ``` The extra block lands after the default prompt and after any `AGENTS.md` injection. ### 3. Render then patch When you want to keep most of the default but rewrite a specific section, render the prompt explicitly and edit the string before passing it back: ```python theme={null} from motus.agent.templates import build_system_prompt default = build_system_prompt(model_name="claude-sonnet-4-6") patched = default.replace( "## Tone and style", "## Tone and style\n- Always answer in formal British English.\n", ) agent = CodingAgent( client=client, model_name="claude-sonnet-4-6", system_prompt=patched, ) ``` ### 4. `system_prompt` — full replacement Pass any string to replace the default entirely. Use when you want a completely different agent personality but still want the `CodingAgent` tool wiring. **Note**: `AGENTS.md` injection is skipped when `system_prompt` is passed explicitly — you'd need to call `build_system_prompt()` and incorporate the project context yourself if you want both. ```python theme={null} agent = CodingAgent( client=client, model_name="...", system_prompt="You are a security-focused code reviewer. Read carefully. Flag issues; don't fix.", ) ``` ### 5. Subclass `CodingAgent` For fundamental shape changes — different file conventions, additional auto-injected sections, custom prompt-build pipeline — subclass and override. Reach for this only when the four above don't fit. **Rule of thumb**: project rules → `AGENTS.md`. Personal/agent-specific rules → `system_prompt_extra`. Section rewrites → render+patch. Full personality change → `system_prompt`. Structural change → subclass. ## Toggling features All capability sets are keyword flags. Pass `False` to remove a tool group. | Flag | Default | What it adds | | ------------------ | ------- | --------------------------------------------------------------------------------------------------- | | `enable_web` | `True` | `web_fetch` + `web_search`. `web_search` no-ops with a friendly error if `BRAVE_API_KEY` isn't set. | | `enable_subagents` | `True` | `task` tool with `general-purpose` / `Explore` / `Plan` types. | | `enable_plan_mode` | `True` | `enter_plan_mode` / `exit_plan_mode` and the read-only tool subset toggle. | ```python theme={null} # Minimal: just bash + file + search + todo, no web / subagents / plan mode agent = CodingAgent( client=client, model_name="...", enable_web=False, enable_subagents=False, enable_plan_mode=False, ) ``` ## Adding extra tools Pass `extra_tools=[...]` to add tools alongside the builtins: ```python theme={null} from motus.tools import tool, InputSchema from pydantic import Field class DeployInput(InputSchema): target: str = Field(description="The deployment target.") @tool(schema=DeployInput) async def deploy(target: str) -> str: """Deploy the project to a target environment.""" ... agent = CodingAgent( client=client, model_name="...", extra_tools=[deploy], ) ``` To **replace** the default tools entirely, pass `tools=[...]` instead. (`extra_tools`, `enable_web`, `enable_subagents`, etc. are ignored when `tools=` is set.) ## Sandboxing By default `CodingAgent` uses a `LocalShell` sandbox — actions affect your real filesystem. For isolated runs: ```python theme={null} from motus.tools import get_sandbox sandbox = get_sandbox("docker", image="python:3.12") agent = CodingAgent( client=client, model_name="...", sandbox=sandbox, ) ``` The same agent code works against `LocalShell`, `DockerSandbox`, or `CloudSandbox` — only the sandbox arg changes. ## Constructor reference All keyword arguments after `model_name`: | Argument | Type | Default | Description | | ---------------------------- | --------------------------------- | ------------------------ | ---------------------------------------------------------------------------------------------------------- | | `sandbox` | `Sandbox \| None` | `LocalShell()` | Sandbox the builtin tools execute in. | | `project_root` | `str \| Path \| None` | cwd | Directory whose `AGENTS.md` / `CLAUDE.md` to inject. | | `skills_dir` | `str \| Path \| None` | `None` | If set, adds a `load_skill` tool over this directory. | | `enable_web` | `bool` | `True` | Adds `web_fetch` + `web_search`. | | `web_search_api_key` | `str \| None` | `BRAVE_API_KEY` env | Brave key for `web_search`. | | `web_fetch_extraction_model` | `str \| None` | parent's `model_name` | Model used for `web_fetch` extraction. Use a small fast model (e.g. `claude-haiku-4-5`) to keep costs low. | | `enable_subagents` | `bool` | `True` | Adds the `task` tool. | | `subagent_specs` | `dict[str, SubAgentSpec] \| None` | `DEFAULT_SUBAGENTS` | Override or extend available subagent types. | | `enable_plan_mode` | `bool` | `True` | Adds `enter_plan_mode` / `exit_plan_mode`. | | `plan_mode_allowed_tools` | `frozenset[str] \| None` | `PLAN_MODE_TOOLS` | Tool name allowlist active during plan mode. | | `extra_tools` | `list \| None` | `None` | Tools added alongside the builtins. | | `system_prompt_extra` | `str \| None` | `None` | Text appended to the default system prompt. | | `system_prompt` | `str \| None` | rendered default | Replaces the default prompt entirely. | | `tools` | `list \| None` | rendered default | Replaces the default tool set entirely. | | `memory_type` | `"basic" \| "compact"` | `"compact"` | Memory implementation. Coding sessions usually benefit from compaction. | | `reasoning` | `ReasoningConfig` | `ReasoningConfig.auto()` | Reasoning effort. | | `cache_policy` | `CachePolicy` | `CachePolicy.AUTO` | Prompt caching strategy (Anthropic providers). | | `max_steps` | `int \| None` | `None` | Max reasoning steps. `None` means no limit. | All other `ReActAgent` keyword arguments pass through unchanged. # Cloud Deployment Source: https://docs.motus.lithosai.com/guides/deployment Deploy agents to Motus Cloud with a single command. Deploy your agent to Motus Cloud with `motus deploy`. On the first run, you provide a project name and an import path. Motus packages your code, uploads it, and streams build progress to your terminal. Every subsequent deploy reads configuration from `motus.toml`, so you only need to run `motus deploy`. ## Quick start ```bash theme={null} motus login ``` Provide a project name and the import path to your agent: ```bash theme={null} motus deploy --name my-project myapp:agent ``` Motus validates the import path locally, creates a project, packages your code, and streams build status until the deployment is healthy. A `motus.toml` file is written to your project directory. Subsequent deploys read everything from `motus.toml` — no flags needed: ```bash theme={null} motus deploy ``` ## Authentication Before deploying, authenticate with your Motus Cloud account: ```bash theme={null} motus login ``` This opens a browser window for OAuth. Credentials are stored in `~/.motus/credentials.json`. In CI or other non-interactive environments, set the `LITHOSAI_API_KEY` environment variable instead it overrides the credential file. Other auth commands: | Command | Description | | -------------- | -------------------------------------------- | | `motus whoami` | Check your current identity | | `motus logout` | Revoke your key and clear stored credentials | ## How it works A deployment proceeds through these stages: Validate the import path (`module:variable` format) by importing it locally. Look up an existing project by `--project-id`, or create one with `--name`. Create a build via the cloud API with the project ID, import path, optional Git source, and optional secrets. Persist `project_id`, `build_id`, and `import_path` to `motus.toml` so subsequent deploys can reuse them. (`git_url` and `git_ref` are also saved when provided.) Collect project files, pack them into a `.tar.zst` archive, and upload via a presigned URL. Git deploys skip this step — the build service pulls the repository directly. Stream build status via SSE. The build progresses through: `queued` → `building` → `built` → `deploying` → `deployed` After deployment, health checks continue in the background until the build transitions to `healthy` (or `failed`). ## Deploy from Git Instead of uploading local files, you can build directly from a Git repository: ```bash theme={null} motus deploy --name my-project --git-url https://github.com/org/repo --git-ref main server:app ``` The build service clones the repository at the specified ref. No local file archiving happens. `--git-ref` accepts a branch name, tag, or commit SHA. After the first deploy, `git_url` and `git_ref` are saved to `motus.toml` so subsequent `motus deploy` calls reuse them. ## Secrets Pass secrets to your deployed agent with `--secret`. Provide a value inline or let Motus read it from your local environment: ```bash theme={null} # Inline value motus deploy --secret API_KEY=sk-123 --secret DATABASE_URL=postgres://... # Read from local environment (the value of DATABASE_URL in your shell) motus deploy --secret DATABASE_URL ``` The `--secret` flag is repeatable. Secrets are encrypted at rest and injected as environment variables in the build environment. ## motus.toml The `motus.toml` file is created automatically on your first deploy and updated on every subsequent one. Motus walks up the directory tree to find it, so you can run `motus deploy` from any subdirectory. ```toml theme={null} project_id = "proj_abc123" build_id = "build_def456" import_path = "server:app" ``` When deploying from Git, these fields are added as well: ```toml theme={null} git_url = "https://github.com/org/repo" git_ref = "main" ``` You can commit `motus.toml` to version control so your team shares the same project ID. Do not commit secrets. ## Deploy flags Full reference for `motus deploy`: ```text theme={null} motus deploy [OPTIONS] [import-path] ``` | Flag | Default | Description | | -------------- | ----------------- | --------------------------------------------------------------------- | | `import-path` | from `motus.toml` | Python import path in `module:variable` format | | `--name` | — | Project name — creates a new project if one does not exist | | `--project-id` | from `motus.toml` | Target an existing project by ID | | `--git-url` | — | Git repository URL — builds from Git instead of uploading local files | | `--git-ref` | — | Branch, tag, or commit SHA to check out (requires `--git-url`) | | `--secret` | — | `KEY=VALUE` or `KEY` (reads from environment). Repeatable. | `--name` and `--project-id` are mutually exclusive. On the first deploy you must provide one of them; on subsequent deploys the project ID is read from `motus.toml`. ## Ignore rules When packaging local files, Motus applies a three-layer ignore strategy: 1. **Dotfiles** are always excluded — `.env`, `.git/`, `.vscode/`, and any other file or directory whose name begins with `.`. 2. **Default patterns** exclude common build artifacts: ```text theme={null} __pycache__/ *.pyc *venv*/ *.egg-info/ dist/ build/ htmlcov/ ``` 3. **`.gitignore` files** in your project tree are respected. Nested `.gitignore` files are scoped to their own directory. Git-based deploys bypass archiving entirely — the build service clones the repository directly. # Guardrails Source: https://docs.motus.lithosai.com/guides/guardrails Validate, transform, or block the inputs and outputs of agents and tools with plain Python functions. A guardrail is just a Python function you hand to an agent or a tool. Motus calls it with the relevant value and interprets what the function returns. Reach for guardrails when you need to: * **Block risky actions** before they run (a SQL tool rejecting `DROP`, a shell tool refusing `rm -rf`). * **Redact or mask sensitive data** in arguments, tool outputs, or the agent's final response (API keys, SSNs, PII). * **Normalize inputs** the model is sloppy about (trim whitespace, coerce enums, canonicalize paths). * **Enforce policy** on prompts or answers (refuse off-topic requests, require a score to fall in range, strip forbidden words). * **Gate with human approval** for high-stakes tool calls before they execute. Every guardrail has the same three-outcome rule: * **Return `None`** (or nothing): let the value through unchanged. * **Return a value**: rewrite what the guardrail is guarding. A `str` replaces a string input or output; a `dict` patches specific keys of a tool's arguments or a structured output. * **Raise an exception**: block execution. Sync and async functions both work. Guardrails declare only the parameters they care about. Motus inspects the function signature and passes the matching values automatically, so you never need to accept the full set of arguments. ## Tool guardrails Tool input guardrails run before a tool function executes. They declare only the parameters they want to inspect, in exactly the names and types the tool uses. Motus reads the function's signature and passes just the matching arguments through. ```python theme={null} from motus.guardrails import ToolInputGuardrailTripped from motus.tools import tool @tool async def execute_sql(query: str, timeout: int = 30, database: str = "main") -> str: """Run a SQL query.""" ... def block_drop(query: str): # only declares `query` if "DROP" in query.upper(): raise ToolInputGuardrailTripped("DROP statements are forbidden.") safe_sql = tool(execute_sql, input_guardrails=[block_drop]) ``` `block_drop` does not mention `timeout` or `database`, so Motus does not pass them in. The guardrail sees only `query`. This lets you write focused checks instead of accepting a long signature just to look at one field. To **modify** an argument instead of blocking, return a `dict` with the keys you want to change. Motus merges it into the tool's kwargs; omitted keys stay unchanged. ```python theme={null} import re def redact_token(query: str) -> dict: return {"query": re.sub(r"sk-\w+", "[REDACTED]", query)} ``` Tool output guardrails run after the tool returns, before the result gets serialized back to the model. They receive the raw return value. ```python theme={null} def redact_passwords(result: str) -> str: return re.sub(r"password=\S+", "password=***", result) safe_query = tool(execute_sql, output_guardrails=[redact_passwords]) ``` When a tool guardrail raises, Motus catches the exception and returns the message to the model as a `{"error": ...}` tool result. The model sees the failure the same way it would see any other tool error, reads your exception message as feedback, and can reconsider what to try next. This is how an agent naturally learns to avoid a blocked action and route around it. ### Multiple guardrails chain sequentially Passing several guardrails builds a pipeline where each one sees the previous one's output. Order matters. ```python theme={null} from motus.guardrails import ToolInputGuardrailTripped from motus.tools import tool def normalize_whitespace(text: str) -> dict: return {"text": " ".join(text.split())} def lowercase(text: str) -> dict: return {"text": text.lower()} def reject_profanity(text: str): if {"damn", "crap"} & set(text.split()): raise ToolInputGuardrailTripped("Profanity detected.") @tool(input_guardrails=[normalize_whitespace, lowercase, reject_profanity]) async def post_comment(text: str) -> str: """Post a comment.""" return f"posted: {text}" ``` Calling `post_comment(" Hello WORLD ")` flows through normalize → lowercase → profanity check. The tool function itself receives `text="hello world"`. ## Agent guardrails Attach guardrails to a `ReActAgent` with `input_guardrails` (run on the user's prompt before the agent starts) and `output_guardrails` (run on the final response before it returns to the caller). ```python theme={null} import re from motus.agent import ReActAgent from motus.guardrails import InputGuardrailTripped from motus.models import OpenAIChatClient def no_homework(value: str, agent): if "homework" in value.lower(): raise InputGuardrailTripped("No homework help.") def redact_ssn(value: str) -> str: return re.sub(r"\b\d{3}-\d{2}-\d{4}\b", "[SSN]", value) agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", input_guardrails=[no_homework], output_guardrails=[redact_ssn], ) ``` Input guardrails receive the user's prompt as a string. If the function also declares a second parameter named `agent`, Motus passes in the running `ReActAgent` instance so the guardrail can read its configuration, inspect memory, or call helpers on it. Return a string to rewrite the prompt; raise `InputGuardrailTripped` to block the run. Output guardrails receive the final response string. Return a string to replace it; raise `OutputGuardrailTripped` to block. Agent guardrail exceptions propagate out of `agent(...)` to the caller. Your application code catches them, not the agent loop. ## Structured output guardrails When an agent uses `response_format` with a Pydantic model, the final result is a model instance rather than a string. Output guardrails in this mode declare the fields they want to inspect; Motus looks up each parameter name on the model and passes the value through. Declare `score` on your guardrail, and Motus passes the parsed result's `score` field. ```python theme={null} from pydantic import BaseModel from motus.agent import ReActAgent from motus.guardrails import OutputGuardrailTripped from motus.models import OpenAIChatClient class AnalysisResult(BaseModel): score: float summary: str def validate_score(score: float): if score < 0 or score > 1: raise OutputGuardrailTripped("Score must be between 0 and 1.") agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", response_format=AnalysisResult, output_guardrails=[validate_score], ) ``` `validate_score` only declares `score`; other fields of `AnalysisResult` pass through untouched. Return a `dict` for a partial update, for example `{"summary": "[redacted]"}`. ## Where to attach guardrails | Level | How to attach | Parameters | | --------------- | ------------------------------------------------------------------------------------- | --------------------------------------- | | Single tool | `@tool(...)` or `tool(fn, ...)` | `input_guardrails`, `output_guardrails` | | Tool collection | `@tools(...)` on a class (see [Tools](/concepts/tools#tool-collections-from-a-class)) | `input_guardrails`, `output_guardrails` | | Agent | `ReActAgent(...)` | `input_guardrails`, `output_guardrails` | For tool collections, a method-level `@tool` with its own guardrails overrides the class-level `@tools` defaults for that one method. The lists do not merge. ## Exceptions All guardrail exceptions inherit from `GuardrailTripped`. Import the one that matches what you are guarding: ```python theme={null} from motus.guardrails import ( InputGuardrailTripped, OutputGuardrailTripped, ToolInputGuardrailTripped, ToolOutputGuardrailTripped, ) ``` | Exception | Where it applies | | ---------------------------- | ----------------------- | | `InputGuardrailTripped` | Agent input guardrails | | `OutputGuardrailTripped` | Agent output guardrails | | `ToolInputGuardrailTripped` | Tool input guardrails | | `ToolOutputGuardrailTripped` | Tool output guardrails | # Human in the Loop Source: https://docs.motus.lithosai.com/guides/human-in-the-loop Pause an agent mid-turn, ask the user for approval or clarification, then resume from exactly where you left off. Some agent actions should not happen without a human saying yes. Deleting files, sending money, posting on someone's behalf, or making an irreversible API call are all moments when you want a real person in the loop. Other times the agent simply does not have enough information to proceed and needs to ask a clarifying question before going further. Motus has first-class support for both. An agent running inside `motus serve` can pause itself, send a payload to the parent server, wait for a reply, and then continue from exactly where it stopped. The session API exposes this as a state called `interrupted`, and clients drive it back to `running` with `POST /sessions/{id}/resume`. ## Pick your approach Require user approval before specific tools run. One decorator flag does it. Let the model present 1 to 4 questions with predefined options. Drop down to the `interrupt()` primitive for any custom payload shape. ## Try it in 30 seconds The fastest way to see HITL in action is the bundled example agent and the reference CLI client. ```bash theme={null} # Terminal 1: serve the example agent motus serve start examples.serving.hitl_agent:agent --port 8000 # Terminal 2: chat with it motus serve chat http://localhost:8000 ``` Try saying *delete the old logs file* (triggers an approval gate) or *help me organize my downloads* (triggers a clarifying question). The chat client polls for status, prints any interrupts as they arrive, prompts you in the terminal, posts the resume, and keeps going until the turn finishes. Look at `src/motus/serve/cli.py` for the full implementation if you want to use it as a template for your own UI. ## How it works Every message you send to a session spawns a fresh worker subprocess. The worker runs your agent, and your agent can call `interrupt()` from anywhere inside its execution. When that happens: Your agent calls `await interrupt(payload)`. The worker sends the payload to the parent server over a pipe and the agent's coroutine blocks on a future, waiting for a reply. The server stores the payload under a freshly generated `interrupt_id`, adds it to the session's pending interrupts, and flips the session status from `running` to `interrupted`. Any client doing a long poll on `GET /sessions/{id}` wakes up immediately and sees the new state. The `interrupt_id` is what the client will echo back when it posts the resume. Your frontend (or CLI, or Slack bot) reads the payload, presents whatever UI makes sense, and gathers the user's response. A `POST /sessions/{id}/resume` with the `interrupt_id` and a `value` ships the user's reply back through the server, into the worker, and into the agent's awaiting future. The agent picks up exactly where it left off. Once all pending interrupts are resolved, the status flips back to `running` and the agent finishes its turn normally. If the agent triggers another interrupt later in the same turn, the cycle repeats. Your local setup and Motus Cloud behave identically. The same `AgentServer` runs in both environments, so the REST API, session lifecycle, and wire protocol are the same. ## Three ways to pause an agent ### Tool approval gates The simplest case: you have a tool that should never run without explicit user approval. Add `requires_approval=True` to the `@tool` decorator and Motus does the rest. ```python theme={null} from motus.tools import tool @tool(requires_approval=True) async def delete_file(path: str) -> str: """Delete a file at the given path.""" import os os.remove(path) return f"Deleted {path}" ``` When the agent decides to call `delete_file`, Motus pauses the worker and emits an interrupt with this shape: ```json theme={null} { "type": "tool_approval", "tool_name": "delete_file", "tool_args": { "path": "/tmp/old_logs.txt" } } ``` Your client shows the user what is about to happen, collects a yes or no, and posts back: ```json theme={null} { "interrupt_id": "", "value": { "approved": true } } ``` If `approved` is `true`, the tool runs normally. If it is `false` or missing, Motus raises `ToolRejected`; the agent sees it as a regular tool error (`{"error": "User rejected delete_file"}`) and can try a different approach, ask the user what to do instead, or give up gracefully. The model stays in control of recovery. Under the hood, `requires_approval=True` prepends an auto-generated input guardrail to the tool's guardrail chain. The approval check runs before any guardrails you defined yourself, and you never write interrupt logic by hand. ### Structured questions with `ask_user_question` Sometimes the agent does not need approval. It needs information. Maybe it does not know which file to edit, or which date range to pull data for, or whether the user wants the long answer or the short one. The `ask_user_question` builtin tool lets the model ask structured questions with predefined options. ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenRouterChatClient from motus.tools import tool from motus.tools.builtins.ask_user import ask_user_question @tool async def organize_files(directory: str, strategy: str) -> str: """Organize files in a directory using the chosen strategy.""" return f"Organized {directory} by {strategy}" agent = ReActAgent( client=OpenRouterChatClient(), model_name="anthropic/claude-sonnet-4", tools=[organize_files, ask_user_question], ) ``` Serve the agent, then send a vague prompt like *help me clean up my downloads folder*. The model decides it does not have enough to run `organize_files`, calls `ask_user_question` instead, and the worker emits this interrupt payload: ```json theme={null} { "type": "user_input", "questions": [ { "question": "How would you like to organize the files?", "header": "Strategy", "multiSelect": false, "options": [ { "label": "By date", "description": "Group files by year and month" }, { "label": "By type", "description": "Group files by extension" }, { "label": "By size", "description": "Move large files into a separate folder" } ] } ] } ``` Your frontend renders the question, shows the options as buttons or a dropdown, and (by convention) appends a free-text "Other" input so the user can type something the model did not anticipate. The reply has this shape: ```json theme={null} { "interrupt_id": "", "value": { "answers": { "How would you like to organize the files?": "By date" } } } ``` The whole `{"answers": {...}}` dict is what the `ask_user_question` tool returns back to the model (serialized as JSON, like any other tool result), so the agent can continue reasoning with the user's choice in hand. #### Schema reference The `ask_user_question` tool validates inputs against this Pydantic schema: | Field | Type | Validation | Notes | | ----------------------------------- | -------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `questions` | list | required, 1 to 4 items (enforced) | Top-level array. | | `questions[].question` | string | required | Full question text. End with `?` by convention (not enforced). | | `questions[].header` | string | required | Short chip label for the UI. Keep it under \~12 chars (not enforced). | | `questions[].multiSelect` | bool | optional, default `false` | Set to `true` for non-mutually-exclusive choices. | | `questions[].options` | list | required, 2 to 4 items (enforced) | The list of choices. | | `questions[].options[].label` | string | required | Display text, 1 to 5 words by convention. | | `questions[].options[].description` | string | required | Explanation of this option. | | `questions[].options[].markdown` | string \| null | optional | A code snippet or technical preview the frontend can render in a monospace box when the user hovers or focuses this option. Use it when an option is best explained by showing the actual text or code it would produce. | The reference CLI client (`motus serve chat`) automatically appends an "Other" free-text input. If you build your own frontend, do the same so users are not boxed in by the model's options. ### Custom interrupts with the `interrupt()` primitive If neither pattern fits, drop into the primitive directly. `interrupt()` accepts any dict and returns whatever value the client posts back. This is what you reach for when you want to ask for free-form text input, present a custom UI, or build your own elicitation pattern. The example below uses the plain `(message, state) -> (response, new_state)` callable shape that `motus serve` accepts. See the [Serving guide](/guides/serving) for the full set of agent shapes. ```python theme={null} from motus.models import ChatMessage from motus.serve.interrupt import interrupt async def my_agent(message: ChatMessage, state: list[ChatMessage]): user_choice = await interrupt({ "type": "color_picker", "prompt": "Pick a brand color for the export", "presets": ["#FF6B6B", "#4ECDC4", "#FFE66D"] }) color = user_choice.get("hex", "#000000") response = ChatMessage.assistant_message(content=f"Using {color} for the export.") return response, state + [message, response] ``` Then serve it like any other agent: ```bash theme={null} motus serve start myapp:my_agent --port 8000 ``` The `type` field is a convention, not enforced. Pick any string your client knows how to handle. The framework's built-in interrupts use `tool_approval` and `user_input`. If you want the reference CLI client (`motus serve chat`) to render your custom interrupts, reuse one of those strings. Otherwise, the CLI prints `[warn] unknown interrupt type` on every poll and never resumes the interrupt, so the session stays wedged until you build your own client. `interrupt()` only works inside a `motus serve` worker subprocess. Calling it from a unit test or standalone script raises `RuntimeError("interrupt() called outside motus serve worker subprocess")`. To test agents that use HITL, spin up a real serve process in your test fixture (see `tests/integration/serve/test_hitl.py` for the pattern). ## Session state machine The session status tells your client what to do next. | Status | What it means | What you can do | | ------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | `idle` | Waiting for input. Initial state after creation. | Send a message with `POST /messages`. | | `running` | The worker is executing the agent. | Long poll `GET /sessions/{id}?wait=true`. | | `interrupted` | The agent paused and is waiting for one or more resumes. | Read `interrupts`, present to user, post `POST /resume` for each. | | `error` | The worker failed (exception, timeout, cancellation, or crash). The `error` field has the message. | Read the error, optionally send a new message to retry. | Transitions: ``` idle ──POST /messages── running ──┬── idle (turn completed) ├── interrupted (agent called interrupt) └── error (worker failed) interrupted ──POST /resume──▶ running (only after ALL pending interrupts are resolved) ``` You cannot send a new message while the session is `running` or `interrupted`. The server returns `409 Conflict`. Either wait for the turn to finish, post a resume, or `DELETE` the session to start over. For the exact request and response shapes on every endpoint, see the [Sessions API reference](/reference/api/sessions). ## Common errors You called `interrupt()` from a unit test, REPL, or some other context that is not a serve worker. The primitive only works inside a process spawned by `AgentServer`. To test, use a real serve process in your fixture (see `tests/integration/serve/test_hitl.py`). Each interrupt payload is pickled before being sent across the worker pipe, and Motus enforces a hard limit of 16 KiB on outbound interrupts. The `ValueError` is raised inside `interrupt()` itself, so it propagates up through your agent code like any other exception. If the agent does not catch it, the worker returns an error traceback and the session transitions to `error`. If you need to ship something bigger (a screenshot, a large file), upload it to object storage first and pass a URL through the interrupt instead. The limit only applies to interrupts going out from the worker; resume values posted in by the client are not size checked. The `interrupt_id` you sent does not map to a live pending interrupt. Common causes: the interrupt was already resumed (resumes are not idempotent, the second call gets a 404), the session was deleted or timed out, you raced a resume against the worker tearing down (the server replies with `"Session not actively waiting for resume"`), or there is a typo in the id. Use the exact id from the most recent poll response and avoid double-resuming. Your client is probably ignoring an interrupt. Every interrupt must be resumed before the worker can continue. Make sure your polling loop walks every entry in the `interrupts` array and posts a resume for each one. The CLI client (`motus serve chat`) only handles `tool_approval` and `user_input`. Custom interrupt types make it print `[warn] unknown interrupt type` on every poll without ever resuming, so the session stays wedged. If you use custom types, build your own client. ## Things to know A `motus serve` process holds all sessions in a dict that does not survive restarts. In Motus Cloud, each deployment runs as a single process, so HITL just works. If you ever scale a serve deployment horizontally yourself, you need sticky session routing so resume requests land on the same process that holds the interrupted session. The server's `--ttl` flag auto-sweeps idle and errored sessions, but **not** running or interrupted ones. A session that pauses on an interrupt and then gets abandoned will stay in memory until you explicitly delete it or restart the server. Build a client-side timeout if you expect users to walk away mid-approval. If the turn times out or you `DELETE` the session while it is interrupted, the worker is killed and any pending `await interrupt(...)` inside the agent raises `EOFError("Worker pipe closed")`. The session transitions to `error` with a clean message: `"Turn cancelled"` on delete, `"Agent timed out"` on timeout. Full Python tracebacks only appear when the agent itself raises an unhandled exception. An agent can fire multiple interrupts at once (for example with `asyncio.gather`). The session's `pending_interrupts` is a dict and each must be resumed individually. The status only flips back to `running` once every pending interrupt has been resolved. The order does not matter. If you configure a webhook in your `POST /messages` request, it fires when the session reaches `idle` or `error`, **not** on each interrupt. If your orchestration depends on knowing the exact moment an interrupt arrives, use long polling instead. ## Where to go next The full serving guide covers session lifecycle, agent types, and the Python API. REST reference for creating, polling, and managing sessions. Learn how the `@tool` decorator and guardrails work under the hood. Validate and transform agent inputs and outputs without touching your agent logic. # MCP Integration Source: https://docs.motus.lithosai.com/guides/mcp-integration Connect any MCP-compatible tool server to a Motus agent with get_mcp(). The [Model Context Protocol](https://modelcontextprotocol.io/) is an open standard for exposing tools and data sources to AI agents. Filesystem access, browser automation, search APIs, database clients, and most SaaS integrations already have an MCP server you can plug in. In Motus, `get_mcp()` is the single entry point: give it a command or a URL, pass the returned session to your agent's `tools=[...]`, and every tool the server publishes becomes a regular agent tool with the same schema, guardrail support, and tracing as tools you write by hand. ## Stdio (local process) The server runs as a child process and Motus talks to it over stdin/stdout. Good for servers distributed as a CLI (`npx`, `uvx`, a local binary). ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient from motus.tools import get_mcp session = get_mcp( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], ) agent = ReActAgent( client=OpenAIChatClient(), model_name="gpt-4o", tools=[session], ) response = await agent("List files in /workspace") ``` `npx`-based servers need [Node.js](https://nodejs.org/) on the host. `uvx` servers need [uv](https://github.com/astral-sh/uv). ## HTTP (remote server) Point to a running MCP endpoint. Pass `headers` for authentication. The agent wiring is the same as stdio; only the session constructor changes: ```python theme={null} from motus.tools import get_mcp session = get_mcp( url="https://mcp.jina.ai/v1", headers={"Authorization": "Bearer "}, ) agent = ReActAgent(client=OpenAIChatClient(), model_name="gpt-4o", tools=[session]) ``` Motus connects via the streamable HTTP transport. If you need a custom `httpx.AsyncClient` (to configure timeouts, proxies, or mTLS), pass it as `http_client=` instead of `headers=`. ## Docker sandbox Launch an MCP server inside a container when you want its file access, network, or dependencies isolated from the host. Motus starts the container, maps `port`, and connects to it over HTTP: ```python theme={null} from motus.tools import get_mcp session = get_mcp( image="node:20", command="npx", args=["@playwright/mcp", "--port", "8080"], port=8080, ) ``` `port=` only maps the container port to the host; it does **not** tell the server what to listen on. If the server binds a different port, the first connection attempt fails with a `TimeoutError` after about 30 seconds. You have to configure the server explicitly, either via a CLI flag (`--port 8080` above) or an env var: ```python theme={null} get_mcp( image="node:20", command="npx", args=["@modelcontextprotocol/server-everything", "streamableHttp"], env={"PORT": "3000"}, port=3000, ) ``` Pass a pre-built `sandbox=` object instead of `image=` when you want to share a container with other tools or configure mounts and network policies. See [Sandboxed execution](/concepts/tools#sandboxed-execution). ## Session lifecycle `get_mcp()` returns an `MCPSession`. The constructor only stores connection parameters. The real connection opens later, either lazily on the agent's first tool call or eagerly when you enter an `async with` block. **Lazy connect** is the shortest path. Hand the session to the agent and Motus takes over: the connection opens on the first tool call that needs it and is reused for every subsequent call. Pick this when you do not need to control exactly when the connection opens or closes. ```python theme={null} session = get_mcp(command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]) agent = ReActAgent(client=client, model_name="gpt-4o", tools=[session]) response = await agent("Read /workspace/README.md") ``` **Explicit `async with`** is for when you need control. You need to inspect the server's tools before building the agent, you want a deterministic close point (between tests, between requests, when switching MCP servers), or you want graceful shutdown rather than relying on garbage collection. ```python theme={null} async with get_mcp(command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"]) as session: print(list(session)) # ['read_file', 'list_directory', 'write_file', ...] agent = ReActAgent(client=client, model_name="gpt-4o", tools=[session]) response = await agent("Read /workspace/README.md") # Session closes cleanly on exit ``` Once connected, `MCPSession` behaves like a mapping of tool name to tool: `list(session)`, `len(session)`, and `session["read_file"]` all work. Each tool is also exposed as an attribute (`session.read_file`), which the `tool()` wrapper below picks up. If a server-side tool name collides with an `MCPSession` attribute (`close`, `aclose`, and similar), the attribute is exposed as `mcptool_` instead. `session["close"]` still works regardless. ## Filtering, renaming, and guarding tools MCP servers often publish more tools than you want the model to see. Use `tools()` to wrap the whole session, or `tool()` to pick out a single method. ```python theme={null} from motus.tools import get_mcp, tools async def validate_path(path: str): if not path.startswith("/workspace"): raise ValueError(f"Path {path!r} is outside the allowed root") async with get_mcp( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], ) as session: wrapped = tools( session, prefix="fs_", # "read_file" becomes "fs_read_file" blocklist={"write_file", "create_directory"}, # hide destructive ops input_guardrails=[validate_path], # default for every tool in the session ) agent = ReActAgent(client=client, model_name="gpt-4o", tools=wrapped) response = await agent("List files in /workspace") ``` Order of operations: Motus filters by the original tool name, then applies `prefix`, then attaches session-wide guardrails to any tool that does not already have its own. ### `tools()` options for MCP | Parameter | Description | | ------------------- | ------------------------------------------------------------------- | | `prefix` | Prepend to every tool name the agent sees. | | `allowlist` | Only expose these names (original, unprefixed). | | `blocklist` | Exclude these names (original, unprefixed). | | `input_guardrails` | Session-wide input guardrails, applied to tools without their own. | | `output_guardrails` | Session-wide output guardrails, applied to tools without their own. | ### Configuring a single tool If you only want to tweak one tool, grab it by attribute (or by key) and wrap it with `tool()`: ```python theme={null} from motus.tools import get_mcp, tool async with get_mcp( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], ) as session: agent = ReActAgent( client=client, model_name="gpt-4o", tools=[tool(session.read_file, input_guardrails=[validate_path])], ) response = await agent("Read /workspace/config.yaml") ``` `tool()` on an MCP tool accepts `name`, `description`, `schema`, `input_guardrails`, `output_guardrails`, `requires_approval`, and the `on_start`/`on_end`/`on_error` hooks. Use `requires_approval=True` to gate a destructive MCP tool behind a user approval prompt; see [Human-in-the-Loop](/guides/human-in-the-loop) for how the approval flow works end to end. ## Mixing MCP with other tools An agent can hold MCP sessions, plain Python functions, class-based tool collections, and other agents in the same `tools=[...]` list. Motus normalizes them all during agent startup. ```python theme={null} from motus.tools import get_mcp async def summarize(text: str) -> str: """Summarize a block of text.""" ... fs_session = get_mcp( command="npx", args=["-y", "@modelcontextprotocol/server-filesystem", "/workspace"], ) search_session = get_mcp( url="https://mcp.jina.ai/v1", headers={"Authorization": "Bearer "}, ) agent = ReActAgent( client=client, model_name="gpt-4o", tools=[ fs_session, # every tool from the filesystem server search_session, # every tool from the remote search server summarize, # a plain Python function ], ) ``` ## Where to go next How the `@tool` and `@tools` decorators, guardrails, and sandboxes fit together. Validate and transform tool inputs and outputs with plain Python functions. A catalog of community-maintained MCP servers you can plug into your agent. Gate destructive MCP tools behind user approval with `requires_approval=True`. # Multi-Agent Source: https://docs.motus.lithosai.com/guides/multi-agent Compose agents with as_tool() and fork() so a supervisor can delegate to specialists and explore independent conversation branches. A single agent with many tools quickly becomes a monolith. Motus gives you two primitives for breaking it apart: * `agent.as_tool()` turns a specialist agent into a tool that a supervisor can call. * `agent.fork()` makes an independent copy of an agent at its current conversation state so you can explore branches without touching the original. They compose freely. A supervisor can hold several specialists as tools, and you can fork any of them whenever you want an isolated branch. ## Agent as tool `as_tool()` is the core building block for supervisor and specialist patterns. Wrap any agent and hand the result to another agent's `tools=[...]` list. ```python theme={null} from motus.agent import ReActAgent from motus.models import OpenAIChatClient client = OpenAIChatClient() researcher = ReActAgent( client=client, model_name="gpt-4o", name="researcher", system_prompt="You research topics thoroughly and return detailed findings.", ) supervisor = ReActAgent( client=client, model_name="gpt-4o", system_prompt="You coordinate research tasks and synthesize results.", tools=[ researcher.as_tool( name="research", description="Delegate a research question. Input is a single string prompt; output is the researcher's findings.", ), ], ) async def main(): return await supervisor("What are the latest advances in fusion energy?") # asyncio.run(main()) ``` The supervisor sees a normal tool with one parameter, `request: str`. When the LLM decides to call it, Motus forwards the string to the specialist, runs the specialist's full agent loop, and returns the result to the supervisor as if it were any other tool output. Always pass a real `description`. The default is `"Delegate to sub-agent: "`, which tells the supervisor's LLM almost nothing about when to use the tool. A sentence or two about what the specialist is for, what input it expects, and what it returns makes a big difference in tool selection quality. ### Parameter reference All parameters are keyword-only. | Parameter | Type | Default | Description | | ------------------- | ------------------ | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `name` | `str \| None` | the agent's `name` | Tool name exposed to the supervisor. Override when the supervisor needs a clearer verb (e.g. `"research"` instead of `"researcher"`). | | `description` | `str \| None` | `"Delegate to sub-agent: "` | Tool description the supervisor's LLM reads when deciding whether to call it. | | `output_extractor` | `Callable \| None` | `None` | Post-process the specialist's return value before it goes back to the supervisor. See [Output extractors](#output-extractors). | | `stateful` | `bool` | `False` | See [Stateful vs. stateless](#stateful-vs-stateless). | | `max_steps` | `int \| None` | the agent's own `max_steps` | Override the specialist's reasoning step cap for this tool. | | `input_guardrails` | `list \| None` | `None` | Guardrails run on the `request` string before the specialist sees it. | | `output_guardrails` | `list \| None` | `None` | Guardrails run on the specialist's output before it returns to the supervisor. | ### Stateful vs. stateless The `stateful` flag controls whether the specialist's conversation history accumulates across tool calls within a single supervisor run. Each call forks the specialist, so every invocation starts from the specialist's original state (system prompt plus any pre-loaded messages). The fork is a lightweight memory copy, not a model call. Concurrent invocations never share memory, which makes this safe for fan-out. ```python theme={null} tools=[researcher.as_tool(description="...")] # stateful=False ``` The tool calls the same specialist instance every time, so its memory grows across calls in the supervisor run. Use this when the specialist benefits from remembering earlier context, for example a researcher that builds on its own previous findings. ```python theme={null} tools=[researcher.as_tool(description="...", stateful=True)] ``` Stateful specialists are not safe to call concurrently within one supervisor run. All calls write to one shared memory buffer, so parallel invocations (like a model emitting two simultaneous tool calls that both hit the stateful specialist) will race and corrupt the conversation. Stick to sequential calls, or keep `stateful=False` for parallel paths. ### Output extractors By default the specialist's result is passed back as-is; non-string results are JSON-encoded before reaching the supervisor. `output_extractor` runs on the specialist's return value first. Two common uses: * **Pull a field out of a structured result.** If the specialist returns a Pydantic model, return just the field the supervisor actually needs. * **Shrink a long answer.** The specialist may produce pages of analysis; the supervisor may only want the headline. ```python theme={null} from pydantic import BaseModel class AnalysisResult(BaseModel): summary: str evidence: list[str] # analyst is a ReActAgent configured with response_format=AnalysisResult tools=[ analyst.as_tool( description="Analyze a dataset and return a one-line summary.", output_extractor=lambda r: r.summary, ), ] ``` The extractor runs after the specialist finishes and before the supervisor sees the result. Raising inside it aborts the tool call like any other exception. ### Errors When the specialist raises, the exception is caught at the tool boundary and surfaced to the supervisor as a regular tool error (`{"error": "..."}`). The supervisor's model sees the failure as feedback and can retry with a different input or give up, the same way it handles any other tool exception. ## Forking `agent.fork()` makes an independent copy of the agent with the same configuration and a forked copy of the conversation. Changes to the fork's memory never affect the original. ```python theme={null} agent = ReActAgent(client=client, model_name="gpt-4o") async def main(): await agent("Summarize the pros and cons of microservices.") # Two independent branches from the same starting point branch_a = agent.fork() branch_b = agent.fork() pro = await branch_a("Now argue strongly in favor.") con = await branch_b("Now argue strongly against.") # The original is unchanged recap = await agent("What did you just summarize?") return pro, con, recap ``` The fork carries over every `__init__` argument (client, model, system prompt, tools, guardrails, `response_format`, `reasoning`, `timeout`, `cache_policy`, `step_callback`, and so on) and gets its own independent copy of the conversation memory. Forking is useful for A/B comparisons, self-consistency sampling, and any case where you want to branch from a known conversation state without mutating the original. If you just want a fresh empty agent, instantiate a new `ReActAgent` rather than forking; fork's job is preserving the accumulated conversation. ## Where to go next Everything a `ReActAgent` can do on its own: tools, memory, guardrails, reasoning. Deep dive on `@agent_task`, the task graph, and parallel execution. How the `@tool` decorator and guardrails work under the hood. Debugging multi-agent flows is much easier with tracing on. # Plugin Source: https://docs.motus.lithosai.com/guides/plugin The Motus system is easy for your coding agent to use, whether you use Claude Code, Codex, Cursor, or Gemini CLI. The /motus skill is automatically installed with the CLI. ```sh theme={null} curl -fsSL https://www.lithosai.com/motus/install.sh | sh ``` /motus enables your agent to deploy existing agents or even to create new ones which exploit Motus-unique features. ## Usage ```text theme={null} /motus # guided agent creation /motus I need a customer support agent with PII redaction /motus deploy # auto-detect and guided deploy /motus deploy agent:my_agent # specify entry point /motus deploy my-project agent:my_agent # direct cloud deploy ``` ## Alternative Installation Methods ### Claude Code Plugin Marketplace ```sh theme={null} claude plugin marketplace add lithos-ai/motus && claude plugin install motus ``` Or from within Claude Code: ```text theme={null} /plugin marketplace add lithos-ai/motus /plugin install motus ``` #### Enable Automatic Updates The `curl | sh` installation above enables automatic plugin updates. If you used the later alternative installation method, you'll need to enable automatic updates as follows: 1. Open Claude Code and run `/plugin` 2. Go to **Marketplaces** tab 3. Select **motus-marketplace** 4. Choose **Enable auto-update** ### npx skills `npx skills` installs the skill for wider range of coding agents. ```sh theme={null} npx skills add lithos-ai/motus ``` ## Team Setup for Claude Code Add to your project's `.claude/settings.json` for automatic availability: ```json theme={null} { "extraKnownMarketplaces": { "motus-marketplace": { "source": { "source": "github", "repo": "lithos-ai/motus" } } }, "enabledPlugins": { "motus@motus-marketplace": true } } ``` # Self-Managed Source: https://docs.motus.lithosai.com/guides/serving Serve agents over HTTP with session-based conversations and per-request process isolation with a single command. The self-managed motus exposes any agent as an HTTP server with session-based conversations using `motus serve`. Each message spawns a fresh worker subprocess, so your agent runs in complete isolation. No shared state between requests, and a crash in one turn never affects another. ## Quick start Define your agent in a Python file, then start the server: ```python myapp.py theme={null} from motus.agent import ReActAgent from motus.models import AnthropicChatClient client = AnthropicChatClient() agent = ReActAgent( client=client, model_name="claude-opus-4-6", system_prompt="You are a helpful assistant.", ) ``` ```bash theme={null} # Start the server motus serve start myapp:agent --port 8000 # Chat interactively in a new terminal motus serve chat http://localhost:8000 ``` ## Agent types Every agent type follows the same turn contract: receive a `ChatMessage` and the session's prior state, return a response `ChatMessage` and updated state. All agent types run in worker subprocesses and must be importable from the module level. | Parameter | Type | Description | | --------- | ------------------- | ----------------------------------------------------------------------- | | `message` | `ChatMessage` | The new user message (constructed by the server from the HTTP request). | | `state` | `list[ChatMessage]` | The session's state from the previous turn (empty list on first turn). | **Return value**: `tuple[ChatMessage, list[ChatMessage]]` the response message (surfaced to the HTTP client) and the updated state (stored in the session). The agent owns the state and can append, compact, or restructure it freely. Any object with a conforming `run_turn` method can be served directly. This is a runtime checkable. `Protocol`inheritance is not required. ```python theme={null} from motus.serve import ServableAgent from motus.models.base import ChatMessage class MyAgent(ServableAgent): async def run_turn( self, message: ChatMessage, state: list[ChatMessage], ) -> tuple[ChatMessage, list[ChatMessage]]: response = ChatMessage.assistant_message(content="hello") return response, state + [message, response] ``` Built-in implementations include `AgentBase` and all of its subclasses (such as `ReActAgent`). Google ADK agents are supported via `motus.google_adk.agents.Agent`, a subclass of the ADK `Agent` that implements `ServableAgent`. Session history is replayed automatically each turn. ```python theme={null} from motus.google_adk.agents.llm_agent import Agent agent = Agent( model="gemini-2.0-flash", name="my_agent", instruction="You are a helpful assistant.", ) ``` ```bash theme={null} motus serve start myapp:agent ``` Requires the optional `google-adk` dependency. Anthropic SDK tool runners are supported via `motus.anthropic.ToolRunner`. Define tools with the `@beta_async_tool` decorator and pass the runner directly. A fresh runner is created per turn. ```python theme={null} from motus.anthropic import ToolRunner, beta_async_tool @beta_async_tool async def get_weather(city: str) -> str: """Get the weather for a city.""" return f"Sunny in {city}" runner = ToolRunner( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[get_weather], system="You are a helpful assistant.", ) ``` ```bash theme={null} motus serve start myapp:runner ``` Requires `anthropic>=0.49.0`. Pass `max_iterations` to limit the tool-use loop. OpenAI Agents SDK agents are supported via auto-detection — no adapter import needed. Guardrail tripwire exceptions are caught and returned as refusal messages. Structured output is serialized to JSON. ```python theme={null} from agents import Agent agent = Agent( name="my_agent", instructions="You are a helpful assistant.", ) ``` ```bash theme={null} motus serve start myapp:agent ``` Requires the optional `openai-agents` dependency. Plain functions with the signature `(message, state) -> (response, state)` are supported. Both sync and async functions work: ```python theme={null} from motus.models.base import ChatMessage # Sync def my_agent(message, state): response = ChatMessage.assistant_message(content="hello") return response, state + [message, response] # Async async def my_agent(message, state): result = await some_api_call(message.content) response = ChatMessage.assistant_message(content=result) return response, state + [message, response] ``` ## Session lifecycle Each conversation is a session. A session moves through the following states: | Status | Description | | --------- | ------------------------------------------------------------------ | | `idle` | Waiting for input. Initial state after creation. | | `running` | Processing a message. Concurrent sends are rejected with `409`. | | `error` | Agent raised an exception. The `error` field contains the message. | ```text theme={null} idle ──POST /messages──▶ running ──success──▶ idle └──failure──▶ error error ──POST /messages──▶ running ``` A session in `error` state can receive new messages and transitions back to `running`. Sessions are held in memory and do not persist across server restarts. When `--ttl` is set, idle and errored sessions whose last activity exceeds the TTL are swept by a background task. When `--timeout` is set, agent turns that exceed the limit are killed and the session transitions to `error` with an `"Agent timed out"` message. ## Architecture ```mermaid theme={null} graph TD A["HTTP Requests"] --> B["AgentServer"] B --> C["WorkerExecutor"] C --> D1["Process 1 (one-shot worker)"] C --> D2["Process 2 (one-shot worker)"] C --> DN["Process N (one-shot worker)"] ``` Each message spawns a fresh subprocess via `multiprocessing.Process` with pipe-based IPC. An `asyncio.Semaphore` limits concurrency to `max_workers`. Processes are not reused: each one starts, runs the agent function, sends the result over the pipe, and exits. On timeout or cancellation, the process is killed immediately. This subprocess isolation model means: * A crash in one agent turn never affects other sessions or the server itself. * No shared state leaks between requests. * Resource cleanup is automatic — when the process exits, all memory is reclaimed. ```text theme={null} serve/ ├── __init__.py # Public exports: AgentServer, ServableAgent ├── protocol.py # ServableAgent runtime-checkable protocol ├── server.py # AgentServer class (FastAPI routes, background tasks) ├── worker.py # WorkerExecutor, subprocess execution, agent type dispatch ├── schemas.py # Pydantic models (SessionStatus, request/response types) ├── session.py # Session dataclass and in-memory SessionStore └── cli.py # CLI (start, chat, health, create, sessions, get, delete, messages, send) ``` For detailed CLI usage and all available flags, see the [CLI reference for `motus serve`](/reference/cli/serve/overview). ## Server options Start options for `motus serve start`: | Flag | Default | Description | | -------------------- | --------- | ------------------------------------------------------------------------------------------- | | `--host` | `0.0.0.0` | Bind address | | `--port` | `8000` | Port | | `--workers` | CPU count | Max concurrent worker processes | | `--ttl` | `0` | TTL for idle/error sessions in seconds (`0` = disabled) | | `--timeout` | `0` | Max seconds per agent turn before the worker is killed (`0` = no limit) | | `--max-sessions` | `0` | Maximum concurrent sessions (`0` = unlimited) | | `--shutdown-timeout` | `0` | Seconds to wait for in-flight tasks on shutdown before cancelling (`0` = wait indefinitely) | | `--allow-custom-ids` | `false` | Enable `PUT /sessions/{id}` for client-specified session IDs | | `--log-level` | `info` | Log verbosity: `debug`, `info`, `warning`, `error` | ```bash theme={null} motus serve start myapp:agent --port 8080 --workers 8 --ttl 3600 --timeout 60 ``` ## CLI reference ### `motus serve chat` Send a message or enter an interactive REPL: ```bash theme={null} # Interactive REPL (creates a new session, prints session ID, kept on exit) motus serve chat http://localhost:8000 # Single message motus serve chat http://localhost:8000 "hello" # Resume an existing session motus serve chat http://localhost:8000 --session 550e8400-e29b-41d4-a716-446655440000 ``` Sessions are always kept on exit so traces remain viewable. The session ID is printed when a new session is created — copy it to resume later or to inspect traces in the cloud console. Use `motus serve delete ` to delete manually. | Flag | Default | Description | | ----------- | ------- | ---------------------------------------------------------------------- | | `--session` | — | Resume an existing session instead of creating a new one | | `--param` | — | `KEY=VALUE` per-request parameter passed as `user_params` (repeatable) | ### Other commands | Command | Description | | -------------------------------------------- | ------------------------------------------------------ | | `motus serve health ` | Check server status and worker counts | | `motus serve create ` | Create a new session | | `motus serve sessions ` | List all active sessions | | `motus serve get [--wait]` | Get session details; `--wait` blocks until not running | | `motus serve delete ` | Delete a session | | `motus serve messages ` | Get the full conversation history | | `motus serve send [--wait]` | Send a message; `--wait` blocks for completion | ## Python API Use `AgentServer` to embed the server in your own Python application: ```python theme={null} from motus.serve import AgentServer server = AgentServer(my_agent, ttl=3600, timeout=30) server.run(host="0.0.0.0", port=8000) ``` **Constructor parameters** (all except `agent_fn` are keyword-only): | Parameter | Type | Default | Description | | | ------------------ | ---------- | ------- | ------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------- | | `agent_fn` | \`Callable | str\` | required | A `ServableAgent`, OpenAI Agent, callable, or import path string (e.g., `"myapp:my_agent"`) | | `max_workers` | \`int | None\` | `None` | Max concurrent worker processes. Defaults to `os.cpu_count()`, fallback `4`. | | `ttl` | `float` | `0` | TTL for idle/error sessions in seconds. `0` disables expiry. | | | `timeout` | `float` | `0` | Max seconds per turn before the worker is killed. `0` disables. | | | `max_sessions` | `int` | `0` | Maximum concurrent sessions. `0` means unlimited. | | | `shutdown_timeout` | `float` | `0` | Seconds to wait for in-flight tasks on shutdown. `0` waits indefinitely. | | | `allow_custom_ids` | `bool` | `False` | Enable `PUT /sessions/{id}` for client-specified IDs. | | **`run(host, port, log_level) -> None`** — starts the server (blocking). | Parameter | Type | Default | Description | | ----------- | ----- | ----------- | ----------------- | | `host` | `str` | `"0.0.0.0"` | Bind address | | `port` | `int` | `8000` | Port | | `log_level` | `str` | `"info"` | Uvicorn log level | The `server.app` property exposes the underlying `FastAPI` application for testing or mounting in a larger application. # Tracing Source: https://docs.motus.lithosai.com/guides/tracing Automatically record every LLM call and tool invocation as a structured span no manual instrumentation required. Every `@agent_task` LLM calls, tool invocations, and task dependencies is recorded as a span automatically. No manual instrumentation required. Traces capture timing, inputs, outputs, and parent relationships so you can understand exactly what your agent did and why. Run `MOTUS_TRACING=1 python my_agent.py` to enable detailed tracing with file export in a single environment variable. ## Collection levels | Level | Captures | Overhead | | ----------------- | ---------------------------------------------- | -------- | | `disabled` | Nothing | None | | `basic` (default) | Task names, timing, parent relationships | Minimal | | `detailed` | + full messages, tool arguments, model outputs | Higher | `MOTUS_TRACING=1` sets collection to `detailed` and enables file export. The `basic` level is always on by default it adds negligible overhead and gives you timing data for every run. ## Environment variables | Variable | Purpose | Default | | ------------------------ | --------------------------------------------------------------------------------------------------------------------------- | --------------------------- | | `MOTUS_TRACING` | `1` enables `detailed` collection + file export | off | | `MOTUS_COLLECTION_LEVEL` | Explicit level: `disabled`, `basic`, or `detailed`. Overrides the level set by `MOTUS_TRACING` but not its export behavior. | `basic` | | `MOTUS_TRACING_EXPORT` | `1` enables file export only (without changing the collection level) | off | | `MOTUS_TRACING_ONLINE` | `1` enables detailed collection + file export + live SSE viewer | off | | `MOTUS_TRACING_DIR` | Custom output directory | `traces/trace_/` | ## Export formats After a run, `TraceManager.export_trace()` writes the following files to the output directory: | File | Description | | -------------------- | ------------------------------------------------------------------------------------------------------ | | `tracer_state.json` | Raw span metadata — timing, parent relationships, and extracted fields | | `trace_viewer.html` | Interactive span tree with timing bars and search. Opens automatically on exit when `MOTUS_TRACING=1`. | | `jaeger_traces.json` | OpenTelemetry-format spans for Jaeger, Zipkin, or any OTLP backend | ## Lifecycle hooks Tracing is built on `HookManager`. You can register callbacks at three levels of specificity global (every task), per-name (a specific function or tool), and per-type (all tool calls or all model calls). ### Registering hooks ```python theme={null} from motus.runtime.hooks import ( register_hook, # global — fires for every task register_task_hook, # per-name — fires for a specific function or tool register_tool_hook, # per-type — fires for all tool calls register_model_hook, # per-type — fires for all model calls ) register_hook("task_end", my_callback) register_task_hook("web_search", "task_end", my_callback) register_tool_hook("task_end", my_callback) ``` ### Decorator equivalents ```python theme={null} from motus.runtime.hooks import global_hook, task_hook, tool_task_hook @global_hook("task_error") def on_error(event): logging.error(f"{event.name} failed: {event.error}") @task_hook("fetch_data", "task_end") def on_fetch(event): logging.info(f"Fetched: {event.result}") @tool_task_hook("task_end") def on_tool(event): logging.info(f"Tool {event.name}: {event.result}") ``` Execution order within each event: global hooks, then name hooks, then type hooks. Pass `prepend=True` to run a callback first within its group. Both sync and async callbacks are supported. Exceptions in callbacks are logged and never propagated. ### HookEvent fields | Field | Type | Description | | ------------ | ------------- | --------------------------------------------------------------------------------- | | `event_type` | `str` | `"task_start"`, `"task_end"`, `"task_error"`, or `"task_cancelled"` | | `name` | `str` | Function or tool name | | `task_type` | `str` | `"normal_task"`, `"tool_call"`, `"model_call"`, `"agent_call"`, or `"magic_task"` | | `args` | `tuple` | Positional arguments passed to the task | | `kwargs` | `dict` | Keyword arguments passed to the task | | `result` | `Any` | Return value (populated on `task_end` only) | | `error` | `Exception` | Exception instance (populated on `task_error` and `task_cancelled` only) | | `task_id` | `AgentTaskId` | Unique identifier for this task instance | | `metadata` | `dict` | Additional context, such as `parent_stack` | ## Programmatic access ```python theme={null} from motus.runtime import get_runtime tracer = get_runtime().scheduler.tracer # auto-inits runtime if needed tracer.export_trace() tracer.get_trace_id() # UUID for this session for task_id, meta in tracer.task_meta.items(): print(f"{meta['func']}: {meta.get('ended_at', 'running')}") ``` ## Advanced configuration For fine-grained control, construct a `TraceConfig` directly instead of relying on environment variables: ```python theme={null} from pathlib import Path from motus.runtime.tracing import TraceConfig, CollectionLevel config = TraceConfig( collection_level=CollectionLevel.DETAILED, export_enabled=True, log_dir=Path("my_traces/run_001"), ) ``` | Field | Type | Default | Description | | | ------------------ | ----------------- | --------------------- | ------------------------------ | ---------------------------- | | `collection_level` | `CollectionLevel` | from env | What data to collect | | | `export_enabled` | `bool` | from env | Write trace files on export | | | `online_tracing` | `bool` | from env | Start a local SSE viewer | | | `log_dir` | `Path` | `traces/trace_/` | Output directory | | | `json_path` | `str` | `"tracer_state.json"` | JSON state filename | | | `cloud_api_url` | \`str | None\` | from credentials | Cloud API endpoint | | `cloud_api_key` | \`str | None\` | from credentials | Cloud API key | | `project` | \`str | None\` | `MOTUS_PROJECT` / `motus.toml` | Project tag for cloud traces | | `build` | \`str | None\` | `MOTUS_BUILD` / `motus.toml` | Build tag for cloud traces | ## Cloud Tracing When your agent is deployed to Motus Cloud via `motus deploy`, traces are automatically streamed to the Motus dashboard — no additional configuration needed. The cloud infrastructure sets `MOTUS_ON_CLOUD=1`, which enables cloud trace upload alongside your existing credentials. Local runs never send traces to the cloud, even if you are logged in. This prevents accidental uploads during development. # Anthropic SDK Source: https://docs.motus.lithosai.com/integrations/anthropic-sdk Use your existing Anthropic SDK code with Motus serving and tracing. Define tools, create a ToolRunner, and deploy. Use the Anthropic SDK's Beta Tool Runner through Motus to get full tracing and HTTP serving with a single import. Define your tools, create a `ToolRunner`, and serve it. ## Installation ```bash theme={null} uv pip install "anthropic>=0.49.0" ``` ```bash theme={null} pip install "anthropic>=0.49.0" ``` The Anthropic SDK is a core dependency of Motus. You need `anthropic>=0.49.0` for tool runner support. ## Basic usage Import `ToolRunner` and `beta_async_tool` from `motus.anthropic`. Use `@beta_async_tool` to decorate your tool functions, then pass them to `ToolRunner`: ```python theme={null} from motus.anthropic import ToolRunner, beta_async_tool @beta_async_tool async def get_weather(city: str) -> str: """Get the weather for a city.""" return f"Sunny in {city}" runner = ToolRunner( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[get_weather], system="You are a helpful assistant.", ) ``` `ToolRunner` holds your model configuration and tool list. It creates a fresh `BetaAsyncToolRunner` on each turn. Tool runners are single-use generators that cannot be re-iterated, so `ToolRunner` handles that lifecycle for you. ### Tool types You can pass several tool types to `ToolRunner.tools`: * Functions decorated with `@beta_async_tool` or `@beta_tool` * Plain async or sync Python functions (auto-wrapped on each turn) * Motus `@tool`-decorated functions (unwrapped and re-wrapped automatically) ```python theme={null} from motus.anthropic import ToolRunner, beta_async_tool, beta_tool @beta_async_tool async def search(query: str) -> str: """Search the web.""" return f"Results for: {query}" @beta_tool def calculate(expression: str) -> str: """Evaluate a math expression.""" return str(eval(expression)) # noqa: S307 runner = ToolRunner( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[search, calculate], ) ``` ### Limiting the tool-use loop Pass `max_iterations` to stop after a fixed number of tool-use rounds: ```python theme={null} runner = ToolRunner( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[get_weather], max_iterations=5, ) ``` ## Deployment ### Local serving Pass the `runner` object directly to `motus serve start`: ```bash theme={null} motus serve start myapp:runner --port 8000 ``` Where `runner` is a `ToolRunner` instance defined at module level in `myapp.py`. ### Cloud deployment ```bash theme={null} cd my_project motus deploy --name my-agent tools_runner:runner ``` When deploying to Motus cloud, include `requirements.txt` with `anthropic>=0.49.0` (the SDK is not in the base image). No API key secrets are needed - the platform routes Anthropic API calls through the model proxy. Session state (conversation history) is persisted in DynamoDB and survives backend restarts, failovers, and scaling events. ### State management Motus manages conversation state across turns. Each turn receives the full prior conversation as a list of `ChatMessage` objects. `ToolRunner` converts that state into Anthropic message format and prepends it to every request, so the model always sees the full conversation context. You do not need to manage history yourself. Motus passes prior state in and stores the updated state after each turn automatically. ```python theme={null} # myapp.py from motus.anthropic import ToolRunner, beta_async_tool @beta_async_tool async def get_weather(city: str) -> str: """Get the weather for a city.""" return f"Sunny in {city}" runner = ToolRunner( model="claude-sonnet-4-20250514", max_tokens=1024, tools=[get_weather], system="You are a helpful assistant.", ) ``` ```bash theme={null} motus serve start myapp:runner --port 8000 ``` ## Tracing Tracing is automatic when the Motus runtime is active (as it is inside `motus serve`). Each turn produces three span types in `TraceManager`: | Span type | Source | Contents | | ------------ | ------------------------------ | --------------------------------------------------------- | | `agent_call` | Root span for the turn | Model name, start/end timestamps | | `model_call` | Each request to the Claude API | Model name, input messages, token usage, response content | | `tool_call` | Each tool invocation | Tool name, input arguments, output, error status | All `model_call` and `tool_call` spans are parented to the root `agent_call` span for the turn. Traces are auto-exported on process exit. On the Motus cloud platform, the `AsyncAnthropic()` client picks up platform-injected environment variables that route requests through the model proxy. You do not need to set `ANTHROPIC_API_KEY` at deploy time. ## Exports `motus.anthropic` re-exports the following from the Anthropic SDK, plus Motus-specific additions: | Export | Description | | ----------------------------------- | -------------------------------------------------------------------------- | | `ToolRunner` | Motus serve adapter which holds config and creates a fresh runner per turn | | `beta_async_tool` | Decorator for async tool functions | | `beta_tool` | Decorator for sync tool functions | | `BetaAsyncFunctionTool` | Anthropic SDK async function tool type | | `BetaFunctionTool` | Anthropic SDK sync function tool type | | `BetaAsyncBuiltinFunctionTool` | Anthropic SDK async built-in tool type | | `BetaBuiltinFunctionTool` | Anthropic SDK sync built-in tool type | | `MotusBetaToolRunner` | Instrumented sync tool runner | | `MotusBetaAsyncToolRunner` | Instrumented async tool runner | | `MotusBetaStreamingToolRunner` | Instrumented sync streaming tool runner | | `MotusBetaAsyncStreamingToolRunner` | Instrumented async streaming tool runner | | `get_tracer()` | Returns the `TraceManager` instance | # Google ADK Source: https://docs.motus.lithosai.com/integrations/google-adk Serve Google ADK agents with Motus full session history replay, automatic tracing, and cloud deployment. Serve Google ADK agents through Motus for full session history replay, automatic tracing, and cloud deployment. Import `Agent` from `motus.google_adk.agents.llm_agent` instead of the ADK directly. Your model, tools, and instructions stay the same. ## Installation ```bash theme={null} uv sync --extra google-adk ``` ```bash theme={null} pip install "lithosai-motus[google-adk]" ``` Requires `google-adk>=1.27.2`. ## Basic usage Import `Agent` from `motus.google_adk.agents.llm_agent` instead of the ADK package: ```python theme={null} from motus.google_adk.agents.llm_agent import Agent agent = Agent( model="gemini-2.0-flash", name="my_agent", instruction="You are a helpful assistant.", ) ``` The `Agent` class is a direct subclass of the Google ADK `Agent`. It accepts the same constructor arguments (`model`, `name`, `instruction`, `tools`, and any other ADK parameters) and adds a `run_turn()` method that integrates with Motus serving. ### Adding tools Pass standard ADK tools or plain Python functions directly: ```python theme={null} from motus.google_adk.agents.llm_agent import Agent def get_weather(city: str) -> str: """Get the weather for a city.""" return f"Sunny in {city}" agent = Agent( model="gemini-2.0-flash", name="weather_agent", instruction="You are a helpful weather assistant.", tools=[get_weather], ) ``` ## Deployment ### Local serving Pass the `agent` object directly to `motus serve start`: ```bash theme={null} motus serve start myapp:agent --port 8000 ``` Where `agent` is an `Agent` instance defined at module level in `myapp.py`. ```python theme={null} # myapp.py from motus.google_adk.agents.llm_agent import Agent def get_weather(city: str) -> str: """Get the weather for a city.""" return f"Sunny in {city}" agent = Agent( model="gemini-2.0-flash", name="weather_agent", instruction="You are a helpful weather assistant.", tools=[get_weather], ) ``` ```bash theme={null} motus serve start myapp:agent --port 8000 ``` ### Cloud deployment ```bash theme={null} cd my_project motus deploy --name my-adk-agent agent:root_agent ``` When deploying to Motus cloud, include `requirements.txt` with `google-adk>=1.27.2`. No API key secrets are needed - the platform routes Gemini API calls through the model proxy. Session state (conversation history) is persisted in DynamoDB and survives backend restarts, failovers, and scaling events. ### Session history replay Each turn, Motus passes the full prior conversation as a list of `ChatMessage` objects to `run_turn()`. The agent replays that history into an ADK `InMemoryRunner` session before executing the new turn, so the model sees full conversation context on every request. You do not need to manage history yourself. Motus stores the updated state after each turn and provides it to the next one automatically. A fresh `InMemoryRunner` and session are created for each turn. History is replayed by appending prior messages as ADK `Event` objects before the new user message is sent. ## Tracing Tracing is automatic when the Motus runtime is active (as it is inside `motus serve`). The `MotusSpanProcessor` implements the OpenTelemetry `SpanProcessor` interface and is registered with Google ADK's OTEL provider once per worker process. Google ADK emits OTEL spans for: | ADK span | Motus span type | Contents | | ------------------ | --------------- | ---------------------------------------------- | | `invoke_agent` | `agent_call` | Agent name, invocation duration | | `generate_content` | `model_call` | Model name, token usage, response content | | `execute_tool` | `tool_call` | Tool name, input arguments, output, error type | The processor converts each completed ADK span into Motus `task_meta` format. It extracts model name, token usage, tool arguments and responses, and error types from ADK's semantic convention attributes and ingests them into `TraceManager`. Traces are automatically exported on process exit. On the Motus cloud platform, the Google ADK client picks up platform-injected environment variables that route requests through the model proxy. You do not need to set `GOOGLE_API_KEY` at deploy time. # OpenAI Agents SDK Source: https://docs.motus.lithosai.com/integrations/openai-agents Run OpenAI Agents SDK code with Motus tracing and deployment. Let Motus run your existing OpenAI Agents SDK code with automatic tracing and cloud deployment. Import from `motus.openai_agents` instead of `agents`. Your agent definitions, tool functions, and run logic stay exactly the same. ## Installation ```bash theme={null} uv sync --extra openai-agents ``` ```bash theme={null} pip install 'lithosai-motus[openai-agents]' ``` ## Basic usage Replace your `agents` import with `motus.openai_agents`: ```python theme={null} from motus.openai_agents import Agent, Runner agent = Agent(name="assistant", instructions="You are helpful.") result = await Runner.run(agent, "Hello!") print(result.final_output) ``` The `Runner` wraps every call with tracing and model interception. You do not need to change your agent definitions, tool functions, or run logic. ## What Motus adds ### Tracing Every agent turn, tool call, and model generation is captured by `TraceManager`. The `MotusTracingProcessor` replaces the SDK's default `BackendSpanExporter` (which posts traces to `api.openai.com`) on import. Traces flow into the Motus trace viewer, Jaeger export, and analytics pipeline. Tracing is auto-registered when you import `motus.openai_agents`. You can also register it explicitly: ```python theme={null} from motus.openai_agents import register_tracing register_tracing() ``` To export traces manually before process exit: ```python theme={null} from motus.openai_agents import get_tracer tracer = get_tracer() if tracer: tracer.export_trace() ``` Traces are auto-exported on process exit when `TraceManager.config.export_enabled` is `True`. Manual export is only needed when you want to flush mid-run. ### Model proxy When deployed to Motus cloud, the platform automatically routes OpenAI Responses API calls through the model proxy. No `OPENAI_API_KEY` is needed in the deployed environment - the proxy handles authentication, rate limiting, and cost tracking transparently. ### Model wrapping `MotusOpenAIProvider` and `MotusMultiProvider` sit in the model call path as transparent pass-throughs. Future releases will add hooks for caching, routing, and cost control at this layer. ### Tool wrapping Tool invocations are intercepted before execution. Each `function_tool` call produces a traced span with input arguments and output. Future releases will add tool-level optimization and caching. ## Runner methods `Runner` exposes the same three methods as the SDK's original `Runner`: ```python async theme={null} result = await Runner.run(agent, "Hello!") ``` ```python sync theme={null} result = Runner.run_sync(agent, "Hello!") ``` ```python streaming theme={null} stream = Runner.run_streamed(agent, "Hello!") ``` Each method registers tracing, wraps tools, and injects a `MotusOpenAIProvider` into the `RunConfig` before delegating to the original SDK runner. ## Run configuration You can pass a custom `RunConfig`. Motus upgrades the default `OpenAIProvider` or `MultiProvider` to their Motus counterparts. If you supply your own custom provider, Motus preserves it: ```python theme={null} from motus.openai_agents import Runner, RunConfig, MotusOpenAIProvider config = RunConfig(model_provider=MotusOpenAIProvider()) result = await Runner.run(agent, "Hello!", run_config=config) ``` ## Deployment ### Local serving ```bash theme={null} motus serve start myapp:agent --port 8000 ``` Where `agent` is an OpenAI `Agent` instance. No adapter import is needed. ### Cloud deployment ```bash theme={null} cd my_project motus deploy --name my-agent tools:agent ``` When deploying to Motus cloud, include `requirements.txt` with `openai-agents>=0.13.4` (the SDK is not in the base image). No API key secrets are needed. The platform routes Responses API calls through the model proxy. Session state (conversation history) is persisted in DynamoDB and survives backend restarts, failovers, and scaling events. Guardrail tripwire exceptions are caught and returned as refusal messages. Structured output (Pydantic models, dataclasses) is serialized to JSON automatically. ## What works All OpenAI Agents SDK features are supported: * `function_tool` definitions * `Agent` with instructions, tools, and handoffs * `Runner.run()`, `Runner.run_sync()`, `Runner.run_streamed()` * Handoffs between agents * Guardrails (input and output) * Custom tools and MCP tools * Multi-provider routing (OpenAI, LiteLLM) ## Motus-specific exports In addition to re-exporting the full `agents` package, `motus.openai_agents` provides these additional names: | Export | Description | | --------------------------- | ---------------------------------------------------------------- | | `MotusModel` | Base model wrapper | | `MotusResponsesModel` | Responses API model wrapper | | `MotusChatCompletionsModel` | Chat Completions API model wrapper | | `MotusLitellmModel` | LiteLLM model wrapper | | `MotusOpenAIProvider` | Provider that returns Motus model wrappers | | `MotusMultiProvider` | Multi-provider with Motus interception | | `MotusLitellmProvider` | LiteLLM provider with Motus interception | | `MotusTracingProcessor` | Bridges OpenAI Agents SDK spans into `TraceManager` | | `register_tracing()` | Registers the tracing processor (called automatically on import) | | `get_tracer()` | Returns the `TraceManager` instance | `from motus.openai_agents import X` re-exports everything from the `agents` package. Motus overrides `Runner`, `OpenAIProvider`, `MultiProvider`, and model classes with its own wrappers at import time. ## Traced span types The integration produces span types in `TraceManager` via the `MotusTracingProcessor`, which bridges OpenAI Agents SDK span events: | Span type | Description | | ------------ | ------------------------------------------------------------------------------------ | | `agent` | Agent invocation spans. Contains agent name, instructions, and handoff information. | | `model_call` | LLM generation spans. Contains model name, token usage, and request/response data. | | `tool_call` | Tool execution spans. Contains tool name, input arguments, output, and error status. | | `guardrail` | Guardrail evaluation spans. Contains guardrail name and pass/fail result. | # Motus Overview Source: https://docs.motus.lithosai.com/introduction Higher capability, lower cost, faster agents. Deploy locally or to the cloud in one command. You bring the agent. Motus runs it, serves it, and deploys it. Agents can come from any framework you already use, and Motus also ships with its own agent toolkit for writing production ready agents in clean Python. ## Set up One command installs everything and teaches your coding agent how to use Motus. ```bash theme={null} curl -fsSL https://www.lithosai.com/motus/install.sh | sh ``` This installs the Motus CLI, the Python library, and adds Motus plugins to Claude Code, Codex, and Cursor. ``` /motus # activate Motus skills /motus serve # serve locally /motus deploy # ship to the cloud ``` Your coding agent now handles scaffolding, serving, and deploying for you. See the [Plugin guide](/guides/plugin) for the full list of commands. Install the Python library and CLI directly. ```bash uv theme={null} uv add lithosai-motus ``` ```bash pip theme={null} pip install lithosai-motus ``` ```bash uv theme={null} uv run python -c "from motus.agent import ReActAgent; print('Motus is ready')" ``` ```bash pip theme={null} python -c "from motus.agent import ReActAgent; print('Motus is ready')" ``` See [Installation](/getting-started/installation) for optional SDK extras and the full setup. If you also use Claude Code, Codex, or Cursor, install the Motus plugin so your coding agent can serve and deploy on your behalf: ```bash theme={null} curl -fsSL https://www.lithosai.com/motus/install.sh | sh ``` See the [Plugin guide](/guides/plugin) for details. ## Serve and deploy any agent Motus serves agents from any of these. Bring what you already have. * **Motus** native `ReActAgent` and workflows * **OpenAI Agents SDK** * **Anthropic SDK** * **Google ADK** * **Plain Python** See the [Integrations](/integrations/openai-agents) section for how each framework plugs in, what Motus adds on top, and the minimal code change needed to switch over. Once you have an agent, one command exposes it as an HTTP API or ships it to production. The code is the same either way. ```bash theme={null} # Serve locally on your own machine motus serve start myapp:agent --port 8000 # Chat with it motus serve chat http://localhost:8000 "Hello!" ``` ```bash theme={null} # Ship to Motus Cloud in one command motus deploy --name myapp myapp:agent # Chat with the deployed agent motus serve chat https://myapp.lithosai.com "Hello!" ``` See [Serving](/guides/serving) for session management, worker pools, and webhooks, and [Deployment](/guides/deployment) for the cloud workflow. ## The Motus library `lithosai-motus` is the Python package. Alongside the serving layer, it ships with an agent toolkit you can use to write agents in clean Python. Here is what you get. ### Start simple `ReActAgent` runs the reasoning loop and tool dispatch with multi turn memory, structured output, guardrails, and usage tracking baked in. A working agent in under 10 lines. Write a function, get a tool. Expose class methods with `@tools`, wrap an MCP server with `get_mcp()`, nest another agent with `as_tool()`. Built-in utilities: skills, `bash`, file ops, `glob` / `grep`, todo tracking. `@agent_task` turns plain Python functions into a parallel, resilient workflow. Motus infers the dependency graph from data flow, so you write normal Python and skip the DAG wiring entirely. Unified client for OpenAI, Anthropic, Gemini, and OpenRouter. Switch providers by changing one line. Local models (Ollama, vLLM) work through `base_url`. Every LLM call, tool invocation, and task dependency traced automatically. Interactive HTML viewer, Jaeger export, or cloud dashboard. Enabled with one env var. `motus serve` exposes any agent as a session based HTTP API locally. Test the full serving stack before deploying to the cloud. ### Go deeper Basic append only memory or compaction memory that auto summarizes when the token budget runs thin. Session save and restore built in. Input and output validation on both agents and individual tools. Return a dict to modify, raise to block. Structured output guardrails match Pydantic fields. `agent.as_tool()` wraps any agent as a tool. The supervisor does not know it is calling another agent. `fork()` creates independent conversation branches. Connect any MCP-compatible server with `get_mcp()`. Local via stdio, remote via HTTP, or inside a Docker container. Filter and rename tools with `prefix`, `blocklist`, and guardrails. Run untrusted code in isolated containers. Mount volumes, expose ports, execute shell and Python. Attach to any agent as a tool provider. Prompt caching via `CachePolicy`. `STATIC` covers system and tools, `AUTO` adds the conversation prefix. Cut latency and cost on long conversations. Pause an agent mid turn, ask the user for approval or clarification, then resume from exactly where you left off. Three level hook system (global, per task name, per task type). Tap into `task_start`, `task_end`, `task_error` for logging, metrics, or custom logic. `motus deploy` ships your agent to Motus Cloud with one command. No Dockerfiles, no Kubernetes, no infra code. Drop in for OpenAI Agents SDK, Anthropic SDK, and Google ADK. Change the import, keep your code. This is a slice of what ships with Motus. Browse the rest of the docs to find what fits your use case. ## Learn more Your first agent running in under 5 minutes. How agents, tools, models, memory, runtime, and serving fit together. Runnable demos covering runtime patterns, MCP, multi-agent bots, and more. Dev environment, tests, and how to send your first PR. # Messages Source: https://docs.motus.lithosai.com/reference/api/messages REST API reference for sending messages to a session and retrieving conversation history. Sending a message is asynchronous. `POST /sessions/\{id}/messages` returns `202 Accepted` immediately — the agent runs in the background. To get the result, either poll `GET /sessions/\{id}` (with optional long-polling via `wait=true`) or include a `webhook` field in your request to receive a callback when the turn completes. See [Webhooks](/reference/api/webhooks) for details. *** ## POST /sessions/\{session\_id}/messages Send a message to a session. The agent processes it in the background and the endpoint returns immediately. ### Path parameters The session UUID. ### Request body The text content of the message. Message role. One of `"system"`, `"user"`, `"assistant"`, or `"tool"`. Arbitrary key-value parameters passed through to the agent on the `ChatMessage` object. Useful for per-request context such as user identity or feature flags. Webhook configuration to receive the turn result via HTTP callback instead of polling. See [Webhooks](/reference/api/webhooks) for the full spec. `MessageRequest` inherits from `ChatMessage`, so additional fields are also accepted: `tool_calls`, `tool_call_id`, `name`, and `base64_image`. The minimal request body to send a user message: ```json theme={null} { "content": "hello" } ``` ### Response **`202 Accepted`** — returns a `MessageResponse` and a `Location` header pointing to the session. **Headers**: `Location: /sessions/\{session_id}` ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "running" } ``` The session UUID. Always `"running"` immediately after a message is accepted. ### Errors | Code | Condition | | ----- | -------------------------------------------- | | `404` | Session not found. | | `409` | The session is already processing a message. | *** ## GET /sessions/\{session\_id}/messages Retrieve the full conversation history managed by the agent for this session. ### Path parameters The session UUID. ### Response **`200 OK`** — returns a list of `ChatMessage` objects in chronological order. ```json theme={null} [ { "role": "user", "content": "hello" }, { "role": "assistant", "content": "hi there" } ] ``` Message role: `"user"`, `"assistant"`, `"system"`, or `"tool"`. Text content of the message. ### Errors | Code | Condition | | ----- | ------------------ | | `404` | Session not found. | # Overview Source: https://docs.motus.lithosai.com/reference/api/overview Reference for the Motus agent server REST API. Manage sessions, send messages, and receive results via webhooks. The Motus agent server exposes a REST API for managing conversations with your agent. Start a server with [`motus serve start`](/reference/cli/serve/start), then interact with it over HTTP. All endpoints are served from the base URL of your running server (e.g., `http://localhost:8000`). ## Endpoints | Endpoint | Method | Description | | ------------------------------------------------------------------------------------ | -------- | -------------------------------------------------- | | [`/health`](/reference/api/sessions#get-health) | `GET` | Server health check with worker and session counts | | [`/sessions`](/reference/api/sessions#post-sessions) | `POST` | Create a new conversation session | | [`/sessions/{id}`](/reference/api/sessions#put-sessionsid) | `PUT` | Create a session with a client-specified ID | | [`/sessions`](/reference/api/sessions#get-sessions) | `GET` | List all active sessions | | [`/sessions/{id}`](/reference/api/sessions#get-sessionsid) | `GET` | Get session details (supports long-polling) | | [`/sessions/{id}`](/reference/api/sessions#delete-sessionsid) | `DELETE` | Delete a session and free resources | | [`/sessions/{id}/messages`](/reference/api/messages#post-sessionssession_idmessages) | `POST` | Send a message to a session | | [`/sessions/{id}/messages`](/reference/api/messages#get-sessionssession_idmessages) | `GET` | Retrieve conversation history | ## Guides | Topic | Description | | ----------------------------------- | --------------------------------------------------------- | | [Sessions](/reference/api/sessions) | Session lifecycle — create, inspect, poll, and delete | | [Messages](/reference/api/messages) | Send messages and retrieve conversation history | | [Webhooks](/reference/api/webhooks) | Receive turn results via HTTP callback instead of polling | # Sessions Source: https://docs.motus.lithosai.com/reference/api/sessions REST API reference for session management. Sessions represent individual conversations with your agent — each one holds its own state and message history. ## GET /health Server health check. ### Response **`200 OK`** — returns a `HealthResponse`. ```json theme={null} { "status": "ok", "max_workers": 4, "running_workers": 2, "total_sessions": 2 } ``` Server status. Always `"ok"` when the server is reachable. Maximum number of concurrent worker processes configured on the server. Number of worker processes currently executing agent turns. Total number of sessions currently held in memory. *** A session is created before you send any messages. The server keeps all sessions in memory; they do not persist across server restarts. When `--ttl` is configured on the server, idle and errored sessions are automatically swept after the TTL period elapses. ### Session status | Status | Description | | --------- | --------------------------------------------------------------------------------------------------------------------------------- | | `idle` | Waiting for input. Initial state after creation. | | `running` | Currently processing a message. Concurrent sends are rejected with `409`. | | `error` | The agent raised an exception. The `error` field contains the message. A session in `error` state can still receive new messages. | *** ## POST /sessions Create a new conversation session. Returns a `Location` header pointing to the new session URL. ### Request body The request body is optional. Send `{}` or omit the body entirely to start with an empty session. Preload the session with an existing conversation history. Each message must include a `role` (`"user"` or `"assistant"`) and `content` string. Omit this field to start with an empty session. ### Response **`201 Created`** — returns a `SessionResponse` and a `Location` header. ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle", "response": null, "error": null } ``` Unique identifier for the session (UUID). Current session status: `"idle"`, `"running"`, or `"error"`. The agent's most recent response message. `null` until at least one turn has completed successfully. Error message from the most recent failed turn. `null` when status is not `"error"`. ### Errors | Code | Condition | | ----- | -------------------------------------------------- | | `503` | The server has reached its `--max-sessions` limit. | *** ## PUT /sessions/ Create a session with a client-specified ID. This endpoint requires the server to be started with `--allow-custom-ids`. Without it, all `PUT /sessions/{id}` requests return `405`. The request body and response are identical to `POST /sessions`. ### Path parameters The UUID you want to assign to this session. ### Response **`201 Created`** — returns a `SessionResponse` and a `Location` header. ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle", "response": null, "error": null } ``` ### Errors | Code | Condition | | ----- | -------------------------------------------------- | | `400` | The provided session ID is not a valid UUID. | | `405` | Custom session IDs are not enabled on this server. | | `409` | A session with this ID already exists. | | `503` | The server has reached its `--max-sessions` limit. | *** ## GET /sessions List all active sessions. ### Response **`200 OK`** — returns a list of `SessionSummary` objects. ```json theme={null} [ { "session_id": "550e8400-e29b-41d4-a716-446655440000", "total_messages": 4, "status": "idle" } ] ``` Unique session identifier. Total number of messages in the session's conversation history. Current session status: `"idle"`, `"running"`, or `"error"`. *** ## GET /sessions/ Get session details and the agent's most recent response. Supports optional long-polling to block until a running turn finishes. ### Path parameters The session UUID. ### Query parameters When `true`, the request blocks until the session is no longer `"running"`. Maximum seconds to wait when `wait=true`. If the timeout elapses before the turn finishes, the response is returned with `status: "running"`. Omit for an unlimited wait. ### Response **`200 OK`** — returns a `SessionResponse`. ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle", "response": { "role": "assistant", "content": "hi there" }, "error": null } ``` While `status` is `"running"`, both `response` and `error` are `null`. They are populated only after the turn completes. ### Long-poll behavior | Scenario | HTTP status | `status` field | | ------------------------------------------ | ----------- | -------------- | | Agent finished successfully | `200` | `"idle"` | | Agent raised an exception | `200` | `"error"` | | Timeout elapsed before completion | `200` | `"running"` | | Session not found or deleted while waiting | `404` | — | ### Errors | Code | Condition | | ----- | ------------------ | | `404` | Session not found. | *** ## DELETE /sessions/ Delete a session and free its resources. ### Path parameters The session UUID. ### Response **`204 No Content`** It is safe to call this endpoint while a turn is running. The in-progress task is cancelled and the worker process is killed immediately. ### Errors | Code | Condition | | ----- | ------------------ | | `404` | Session not found. | # Webhooks Source: https://docs.motus.lithosai.com/reference/api/webhooks Receive agent turn results via HTTP callback instead of polling. Include a webhook field in your message request and the server will POST the result to your URL when the turn completes. Instead of polling `GET /sessions/{id}` for the result of a turn, you can include a `webhook` field in your `POST /sessions/{id}/messages` request body. When the agent turn completes (successfully, with an error, or on cancellation), the server delivers a `WebhookPayload` to your URL. ### Complete example ```json theme={null} { "content": "hello", "webhook": { "url": "https://example.com/hook", "token": "secret", "include_messages": false } } ``` *** ## WebhookSpec Fields you include in the `webhook` property of a `MessageRequest`. The URL the server will POST the `WebhookPayload` to after the turn completes. When set, the server includes an `Authorization: Bearer ` header in the delivery request. Use this to authenticate incoming webhook calls on your end. When `true`, the full conversation history is included as the `messages` field in the `WebhookPayload`. Useful if you want the entire transcript without making a separate `GET /sessions/{id}/messages` call. *** ## WebhookPayload The JSON body the server POSTs to your webhook URL after the turn completes. ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle", "response": { "role": "assistant", "content": "hi there" }, "error": null, "messages": null, "trace_metrics": null } ``` The session UUID that completed the turn. Final status of the turn: `"idle"` on success or `"error"` on failure. The agent's response message. Populated when `status` is `"idle"`. Error message from the agent. Populated when `status` is `"error"`. Full conversation history for the session. Only included when `include_messages` was `true` in the `WebhookSpec`. `null` otherwise. Turn metrics from the agent runtime. Included when the runtime has tracing enabled; `null` otherwise. *** ## TraceMetrics Metrics collected from the agent runtime and attached to the webhook payload when available. Trace identifier for this turn. `null` if tracing is not enabled. Total wall-clock time for the turn, in seconds. Total number of tokens consumed during the turn across all model calls. `true` if the turn encountered an error, even if it was partially handled. *** ## Delivery behavior * Webhooks are delivered **asynchronously** after the turn completes. Delivery does not block the turn itself or affect the session state. * Each delivery attempt uses a **10-second timeout**. * If delivery fails — due to a network error, a non-2xx response, or a timeout — the failure is logged but **does not affect the turn result**. The session state remains unchanged and no retry is attempted. * When `token` is set, the delivery request includes an `Authorization: Bearer ` header. If you need guaranteed delivery, poll `GET /sessions/{id}` with `wait=true` as a fallback in case your webhook endpoint is temporarily unavailable. # motus deploy Source: https://docs.motus.lithosai.com/reference/cli/deploy Package and deploy an agent to Motus Cloud. Package and deploy your agent to Motus Cloud. `motus deploy` opens an authentication dialog similar to [`motus login`](/reference/cli/login.mdx) when the user is unauthenticated. ## Usage ```bash theme={null} motus deploy [import-path] [options] ``` ## Options | Flag | Default | Description | | -------------- | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `import-path` | Read from `motus.toml` | Python import path to your agent in `module:variable` format (e.g., `myapp:agent`) | | `--name` | — | Project name. Creates a new project if none exists with this name. Mutually exclusive with `--project-id`. | | `--project-id` | Read from `motus.toml` | ID of an existing project to deploy to. Mutually exclusive with `--name`. | | `--git-url` | — | Git repository URL. When provided, the build service clones the repo instead of uploading local files. | | `--git-ref` | — | Branch, tag, or commit SHA to check out. Requires `--git-url`. | | `--secret` | — | `KEY=VALUE` secret injected into the agent container. Use `KEY` alone to read the value from your local environment. Repeatable. | On your first deploy, you must provide either `--name` or `--project-id`. After a successful deploy, Motus writes `project_id`, `build_id`, and `import_path` to `motus.toml` in your project directory, so subsequent deploys can run without any flags. ### How it works When you run `motus deploy`, it: 1. Validates your import path by importing it locally. 2. Resolves or creates the project using `--project-id` or `--name`. 3. Packs your project files into a `.tar.zst` archive (skipping dotfiles, `__pycache__`, `.pyc`, virtualenvs, and `.gitignore`-excluded paths) and uploads them — or, for Git deploys, instructs the build service to clone the repository directly. 4. Streams build status via SSE: `queued` → `building` → `built` → `deploying` → `deployed` → `healthy`. ## Examples ### First deploy ```bash theme={null} motus deploy --name my-project myapp:agent ``` ### Subsequent deploy ```bash theme={null} # Reads project ID and import path from motus.toml motus deploy ``` ### Deploy with secrets ```bash theme={null} motus deploy --secret OPENAI_API_KEY=sk-123 --secret DATABASE_URL ``` ### Deploy from a Git repository ```bash theme={null} motus deploy --name my-project \ --git-url https://github.com/org/repo.git \ --git-ref main \ myapp:agent ``` # motus login Source: https://docs.motus.lithosai.com/reference/cli/login Authenticate with Motus Cloud via browser-based OAuth. Authenticate with Motus Cloud. Opens a browser-based OAuth flow and stores your credentials in `~/.motus/credentials.json`. You only need to run this once: credentials persist across sessions. ## Usage ```bash theme={null} motus login [options] ``` ## Options | Flag | Default | Description | | ----------- | ---------------------------- | ---------------------------------------------------------------------------------------------- | | `--api-url` | `https://api.lithosai.cloud` | Motus Cloud API endpoint URL. Can also be set via the `LITHOSAI_API_URL` environment variable. | For CI environments where a browser is unavailable, set the `LITHOSAI_API_KEY` environment variable instead of running `motus login`. The environment variable takes precedence over the stored credential file. ## Examples ### Log in to Motus Cloud ```bash theme={null} motus login ``` ### Log in to a custom API endpoint ```bash theme={null} motus login --api-url https://api.staging.lithosai.cloud ``` ### CI authentication via environment variable ```bash theme={null} export LITHOSAI_API_KEY=your-api-key motus deploy myapp:agent ``` # motus logout Source: https://docs.motus.lithosai.com/reference/cli/logout Revoke credentials and clear the local credential file. Revoke your credentials and clear the local credential file. ## Usage ```bash theme={null} motus logout ``` ## Examples ### Log out ```bash theme={null} motus logout Logged out. ``` # Overview Source: https://docs.motus.lithosai.com/reference/cli/overview The Motus CLI. Build, serve, and deploy AI agents from the command line. The `motus` command is the entry point for the Motus Agent Framework CLI. Use it to run agents locally as HTTP servers, interact with them from the terminal, and deploy them to Motus Cloud. ## Usage ```bash theme={null} motus [options] ``` If no command is given, `motus` prints the help message and exits. ## Environment variables | Variable | Description | | ------------------ | ------------------------------------------------------------------------------------------------------------------------- | | `LITHOSAI_API_KEY` | API key for Motus Cloud. When set, replaces credential-file authentication for `deploy`, `login`, `logout`, and `whoami`. | | `LITHOSAI_API_URL` | Override the default Motus Cloud API endpoint (`https://api.lithosai.cloud`). | ## Configuration files Motus reads project-level settings from `motus.toml` in the working directory. After a successful deploy, the CLI writes `project_id`, `build_id`, and `import_path` to this file so that subsequent commands can run without flags. See [Configuration](/getting-started/configuration) for details. ## Subcommands | Command | Description | | ---------------------------------------------- | ----------------------------------------------------------- | | [`motus serve`](/reference/cli/serve/overview) | Start an agent HTTP server and interact with it | | [`motus deploy`](/reference/cli/deploy) | Package and deploy an agent to Motus Cloud | | [`motus login`](/reference/cli/login) | Authenticate with Motus Cloud | | [`motus logout`](/reference/cli/logout) | Revoke credentials and clear the local credential file | | [`motus whoami`](/reference/cli/whoami) | Print the identity associated with your current credentials | # motus serve chat Source: https://docs.motus.lithosai.com/reference/cli/serve/chat Send a single message to a running agent or enter an interactive REPL. Send a single message to a running agent or enter an interactive REPL. ## Usage ```bash theme={null} motus serve chat [message] [options] ``` ## Options | Flag | Default | Description | | ----------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | | `--session` | — | Resume an existing session by ID instead of creating a new one | | `--param` | — | `KEY=VALUE` parameter passed to the agent as `user_params` (repeatable). Numeric values are auto-coerced to `int` or `float`. | Sessions are kept on exit so traces remain viewable in the cloud console. The session ID is printed when a new session is created — copy it to resume later with `--session`. ## Examples ### Single message ```bash theme={null} motus serve chat http://localhost:8000 "What is 2+2?" Session: 550e8400-e29b-41d4-a716-446655440000 (use --session to resume) 4 ``` ### Interactive REPL ```bash theme={null} motus serve chat http://localhost:8000 Session: 550e8400-e29b-41d4-a716-446655440000 (use --session to resume) Chat session started (Ctrl+C to quit) > hello hi there > how are you? I'm doing well! ^C Bye! ``` ### Resume an existing session ```bash theme={null} motus serve chat http://localhost:8000 --session 550e8400-e29b-41d4-a716-446655440000 > where were we? ``` ### Delete a session manually ```bash theme={null} motus serve delete http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 ``` # motus serve create Source: https://docs.motus.lithosai.com/reference/cli/serve/create Create a new session on a running Motus agent server. Create a new session and print its ID. This maps directly to `POST /sessions` described in the [Sessions API reference](/reference/api/sessions). ## Usage ```bash theme={null} motus serve create ``` ## Examples ### Create a session ```bash theme={null} motus serve create http://localhost:8000 ``` ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle" } ``` # motus serve delete Source: https://docs.motus.lithosai.com/reference/cli/serve/delete Delete a session on a running Motus agent server. Delete a session. This maps directly to `DELETE /sessions/{id}` described in the [Sessions API reference](/reference/api/sessions). ## Usage ```bash theme={null} motus serve delete ``` It is safe to delete a session while a turn is running. The running task is cancelled and the worker process is killed immediately. ## Examples ### Delete a session ```bash theme={null} motus serve delete http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 Deleted session 550e8400-e29b-41d4-a716-446655440000 ``` # motus serve get Source: https://docs.motus.lithosai.com/reference/cli/serve/get Get details for a session, with optional long-polling. Get details for a session. Supports long-polling so you can block until a running turn completes. This maps directly to `GET /sessions/{id}` described in the [Sessions API reference](/reference/api/sessions). ## Usage ```bash theme={null} motus serve get [options] ``` ## Options | Flag | Default | Description | | ----------- | ------- | ---------------------------------------------------- | | `--wait` | `false` | Block until the session is no longer `"running"` | | `--timeout` | — | Maximum seconds to wait (only applies with `--wait`) | ## Examples ### Get session details ```bash theme={null} motus serve get http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 ``` ### Long-poll until complete ```bash theme={null} motus serve get http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 --wait --timeout 30 ``` ```json theme={null} { "session_id": "550e8400-e29b-41d4-a716-446655440000", "status": "idle", "response": { "role": "assistant", "content": "hi there" } } ``` # motus serve health Source: https://docs.motus.lithosai.com/reference/cli/serve/health Check the health of a running Motus agent server. Check the health of a running server. Hits the `/health` endpoint and prints a summary of the server's current state. ## Usage ```bash theme={null} motus serve health ``` ## Examples ### Check server health ```bash theme={null} motus serve health http://localhost:8000 ``` ``` Status: ok Workers: 2/4 Total sessions: 2 ``` # motus serve messages Source: https://docs.motus.lithosai.com/reference/cli/serve/messages Retrieve the full conversation history for a session. Retrieve the full conversation history for a session. This maps directly to `GET /sessions/{id}/messages` described in the [Messages API reference](/reference/api/messages). ## Usage ```bash theme={null} motus serve messages ``` ## Examples ### Get message history ```bash theme={null} motus serve messages http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 ``` ```json theme={null} [ { "role": "user", "content": "hello" }, { "role": "assistant", "content": "hi there" } ] ``` # Overview Source: https://docs.motus.lithosai.com/reference/cli/serve/overview CLI reference for the motus serve command group. Start an agent HTTP server and interact with it from the terminal. The `motus serve` command group starts an agent HTTP server and provides subcommands to interact with it (chatting, managing sessions, and sending messages) all from the terminal. It's a frontend to the [API](/reference/api/overview). ## Usage ```bash theme={null} motus serve [options] ``` ## Subcommands | Command | Description | | ------------------------------------------------------- | ------------------------------------------- | | [`motus serve start`](/reference/cli/serve/start) | Start an HTTP server that wraps an agent | | [`motus serve chat`](/reference/cli/serve/chat) | Send a message or enter an interactive REPL | | [`motus serve health`](/reference/cli/serve/health) | Check the health of a running server | | [`motus serve create`](/reference/cli/serve/create) | Create a new session | | [`motus serve sessions`](/reference/cli/serve/sessions) | List all active sessions | | [`motus serve get`](/reference/cli/serve/get) | Get details for a session | | [`motus serve delete`](/reference/cli/serve/delete) | Delete a session | | [`motus serve messages`](/reference/cli/serve/messages) | Retrieve conversation history for a session | | [`motus serve send`](/reference/cli/serve/send) | Send a message to an existing session | # motus serve send Source: https://docs.motus.lithosai.com/reference/cli/serve/send Send a message to an existing session on a running Motus agent server. Send a message to an existing session. Returns immediately unless `--wait` is used. This maps directly to `POST /sessions/{id}/messages` described in the [Messages API reference](/reference/api/messages). ## Usage ```bash theme={null} motus serve send [options] ``` ## Options | Flag | Default | Description | | ---------------------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------- | | `--role` | `user` | Message role: `system`, `user`, `assistant`, or `tool` | | `--wait` | `false` | Wait for the turn to complete and print the final session state | | `--timeout` | — | Maximum seconds to wait (only applies with `--wait`) | | `--webhook-url` | — | URL to POST the turn result to when it completes | | `--webhook-token` | — | Bearer token for the webhook `Authorization` header | | `--webhook-include-messages` | `false` | Include the full message history in the webhook payload | | `--param` | — | `KEY=VALUE` parameter passed to the agent as `user_params` (repeatable). Numeric values are auto-coerced to `int` or `float`. | ## Examples ### Fire and forget ```bash theme={null} motus serve send http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 "hello" ``` ### Wait for the turn to finish ```bash theme={null} motus serve send http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 "hello" --wait ``` ### Send with a webhook ```bash theme={null} motus serve send http://localhost:8000 550e8400-e29b-41d4-a716-446655440000 "hello" \ --webhook-url https://example.com/hook \ --webhook-token secret ``` # motus serve sessions Source: https://docs.motus.lithosai.com/reference/cli/serve/sessions List all active sessions on a running Motus agent server. List all active sessions. This maps directly to `GET /sessions` described in the [Sessions API reference](/reference/api/sessions). ## Usage ```bash theme={null} motus serve sessions ``` ## Examples ### List sessions ```bash theme={null} motus serve sessions http://localhost:8000 ``` ```json theme={null} [ { "session_id": "550e8400-e29b-41d4-a716-446655440000", "total_messages": 4, "status": "idle" } ] ``` # motus serve start Source: https://docs.motus.lithosai.com/reference/cli/serve/start Start an HTTP server that wraps an agent at a given import path. Start an HTTP server that wraps the agent at the given import path. ## Usage ```bash theme={null} motus serve start [options] ``` ### Positional argument Python import path to the agent object in `module:variable` format. For example, `myapp:my_agent` imports `my_agent` from `myapp.py`. The object must be importable from the working directory. ## Options | Flag | Default | Description | | -------------------- | --------- | ------------------------------------------------------------------------------------------ | | `--host` | `0.0.0.0` | Network address to bind to | | `--port` | `8000` | Port to listen on | | `--workers` | CPU count | Maximum concurrent worker processes | | `--ttl` | `0` | Idle/error session expiry in seconds (`0` disables expiry) | | `--timeout` | `0` | Maximum seconds per agent turn before the worker is killed (`0` means no limit) | | `--max-sessions` | `0` | Maximum number of concurrent sessions (`0` means unlimited) | | `--shutdown-timeout` | `0` | Seconds to wait for in-flight tasks on shutdown before cancelling (`0` waits indefinitely) | | `--allow-custom-ids` | `false` | Enable `PUT /sessions/{id}` so clients can specify their own session IDs | | `--log-level` | `info` | Log verbosity: `debug`, `info`, `warning`, or `error` | ## Examples ### Start with custom port and workers ```bash theme={null} motus serve start myapp:my_agent --port 8080 --workers 8 ``` ### Enable custom session IDs with idle TTL ```bash theme={null} motus serve start myapp:my_agent --allow-custom-ids --ttl 3600 ``` ### Limit concurrency and set a per-turn timeout ```bash theme={null} motus serve start myapp:my_agent --max-sessions 50 --timeout 30 ``` # motus whoami Source: https://docs.motus.lithosai.com/reference/cli/whoami Print the identity associated with your current Motus Cloud credentials. Print the identity associated with your current credentials. Use this to confirm which account your CLI is authenticated. ## Usage ```bash theme={null} motus whoami ``` ## Examples ### Check current identity ```bash theme={null} motus whoami API URL: https://api.lithosai.cloud API key: sk-1234abcd... ``` ### When not logged in ```bash theme={null} motus whoami Not logged in. Run: motus login --api-url ```