The Control Loop: From Prompt to Tool Result
OpenManus's central mechanism is a bounded control loop: a request enters agent memory, the language model proposes either text or a structured tool call, the runtime acts on that proposal, and the resulting observation becomes context for what happens next. This chapter follows that loop closely so you can predict what a single call does, what information survives into the next decision, and why “the agent stopped” can mean completion, failure, or a resource limit.
A run begins as stateful memory
The first important distinction is between a prompt and a run. A prompt is a piece of input. A run is a lifecycle with state, memory, steps, and cleanup. When OpenManus receives a request, BaseAgent.run adds it to the agent's memory as a user message. The agent then enters its running state and begins counting steps. The request is no longer an isolated string sent once to a model; it is the first event in a conversation that the runtime will continue to update.
Memory is a typed message store. It can contain system, user, assistant, and tool messages, along with tool-call identifiers and, when supported, image data. That distinction matters because the next model request needs more than the original question. It may need to see what it previously proposed, which tool actually ran, what the tool returned, and what instructions surround the interaction. OpenManus keeps a bounded recent history rather than an unlimited transcript. The default memory retains the newest 100 messages, which prevents unbounded growth but also means older context can disappear without being summarized.
Each iteration increases the current step and invokes step. In the ReAct-shaped layer, one step has two conceptual halves: think decides whether the current context calls for an action, and act performs the action selected during thinking. The names are less important than the ownership boundary. The model-facing part decides what should happen next; the execution part deals with tool names, arguments, results, and side effects. OpenManus does not implement a separate symbolic reasoner behind think. The language model's response is the source of the proposed decision.
The running state is also more than a label for logs. OpenManus uses a state context to move an agent into RUNNING, mark it as ERROR if an exception escapes the protected block, and restore the previous state when the block ends. A special tool such as termination can set the state to FINISHED. The loop checks both the step limit and the finished state before starting another iteration. This gives the runtime a simple answer to a difficult operational question: when should it stop asking the model for more work?
That answer is deliberately bounded. The loop does not assume that a capable model will always recognize the end of a task. A maximum number of steps prevents indefinite repetition. A duplicate-response check looks for repeated assistant content and, when it detects a likely loop, adds a prompt asking the agent to change strategy. This is a liveness aid, not a semantic understanding of whether the task is stuck. Two different responses can repeat the same failed plan, while two identical responses could be harmless in a particular task. The check can encourage recovery, but it cannot establish progress.
At the end of a normal run, resource cleanup is part of the lifecycle rather than an optional afterthought. The base run path invokes cleanup for the global sandbox client, and the tool-calling agent also has cleanup behavior for tools that own resources. This matters especially when an agent can hold a shell session, an MCP connection, a browser process, or a container. A returned sentence is not the whole result of a run; open resources and the state in which the agent is left are part of the system's behavior.
Think, act, and observe
The tool-calling half of the loop begins when ToolCallAgent.think prepares the model request. It adds the next-step instruction to memory, supplies the configured system prompt, converts every available tool into a function-style schema, and asks the LLM wrapper for a tool-aware response. The model therefore sees both the conversation so far and a description of the operations that the runtime is prepared to perform. A schema can tell the model that a tool needs a path or a query, what type that value should have, and what the operation is meant to do. It does not make the requested operation wise or authorized.
The model can return ordinary content, structured tool calls, or both. In automatic tool-choice mode, content without a call is recorded as an assistant message and ends the current action step. That does not necessarily finish the entire agent run: unless the state becomes FINISHED, the outer loop may ask for another step. This is a useful but subtle distinction. “The model answered in text” means the current response contained no executable command; it does not automatically mean that the user-requested work has been verified or that the runtime has ended.
When a structured call is present, OpenManus first records an assistant message containing the model's content and tool-call metadata. It then moves to act. The tool call must have a usable function name. The runtime checks that the name exists in the available tool map and parses the argument string as JSON. If the name is unknown or the arguments are malformed, the observation becomes an error rather than an attempt to guess what the model meant. This is the first concrete boundary between model intent and execution: the runtime accepts only a call that matches its registered interface.
The collection then dispatches the call to the corresponding tool. Calls returned in one response are executed sequentially, not as a transaction and not as an automatically parallel batch. Each tool produces a standardized result that can contain output, an error, an image, or other system information. The agent turns that result into a tool message associated with the original call ID. The model will encounter the result on a later request, not through some hidden change to its internal state.
Consider a simple request: inspect a small dataset, calculate a summary, and save the result. The model does not receive a magical “dataset capability.” It sees the descriptions and schemas of the tools currently registered. It might choose Python execution and return a JSON argument containing code. The runtime parses the call, finds the matching tool, starts the operation, captures the tool's returned observation, and stores the call and result in the conversation. On the next iteration, the model can decide that the calculation is enough, ask for a file edit, or recover from an error. The useful behavior comes from the handoff between these stages, not from any one stage alone.
That handoff also explains why the assistant message and the tool message are separate. The assistant message records what the model asked to do; the tool message records what the runtime says happened. If the model requests an unknown tool, OpenManus returns an error instead of silently substituting another capability. If the argument string is not valid JSON, parsing fails and the error becomes an observation. The runtime protects the interface boundary, but it does not invent a better intention for the model. A later decision must interpret the failure and choose whether to correct the call, change tools, or stop.
Tool-choice modes make the boundary more explicit. In automatic mode, the model may answer with content or call a tool. In a required mode, the absence of a call is treated as a problem for the action phase. A none mode disables tool use for that request and warns if the model tries to call one. These modes do not make the model's decision correct; they constrain what kinds of response the runtime will accept. They are useful when an application knows that a step should be explanatory, executable, or deliberately tool-free.
Sequential execution has a second consequence: a response containing multiple calls is not a transaction. If the first operation changes a file and the second operation fails, the runtime does not automatically undo the first change merely because the group did not reach its intended outcome. Likewise, a tool result can report an error after an external service has already performed part of an action. The agent loop can observe the failure and try again, but recovery depends on the tool's own behavior and on the model understanding what already happened. This is why side effects and idempotence matter even though the model-facing interface looks like a list of functions.
A final coupling is tool naming. The collection exports a name-to-tool map and skips a new tool when its name conflicts with one already present. Adding a capability is therefore not only a matter of implementing its operation; it also requires a unique, accurate name and a description the model can use. A poorly described tool can be syntactically callable but practically confusing. A collision can leave the intended implementation unavailable without changing the model's general ability to call tools. Interface quality is part of control quality.
This sequence is easier to see as a conversation among the runtime's actors:
The diagram shows why the observation is a control surface. If a file tool reports success, that report becomes part of the next model context. If a browser tool returns a screenshot, the agent may add it as an image-bearing message. If a command fails, the error is also context that can lead to a corrected call. The runtime does not independently establish that a successful-looking string is true, nor does it roll back a side effect because the next model response is dissatisfied. The model acts on the observation it receives.
Images follow the same basic path but add another compatibility boundary. A tool result can carry base64 image data, which the agent stores separately as a user message after the tool observation. The LLM wrapper converts that field into image content only for models recognized as multimodal. For other models, it removes the image while retaining the textual part of the message. Thus “the agent saw a screenshot” is not a universal property of the runtime. It depends on the tool returning an image, the agent preserving it, and the configured model accepting it.
The amount of observation also matters. Some agents configure a maximum observation size, and the tool-calling layer truncates a result before storing it when such a limit is active. Truncation protects the next model request from being overwhelmed by a large command or document, but it can remove the very detail needed to decide what to do next. A smaller context is easier to operate within and cheaper to send; a smaller context can also make the agent act on a partial picture. The limit is therefore a control trade-off, not merely a formatting preference.
The LLM wrapper adds another limit before the call reaches a provider. It estimates message, tool-schema, tool-call, and image tokens. If an optional cumulative input budget would be exceeded, it raises a token-limit exception instead of sending the request. The tool-calling agent handles that special condition by recording a final explanatory assistant message and marking the state finished. That is a controlled stop, but it is not evidence that the user's task was completed. It means the runtime could no longer continue within the configured context budget.
Several endings, several meanings
OpenManus can leave the loop through different paths. An explicit termination tool is a deliberate control signal: the special-tool handling marks the agent finished after the call. A remote or local tool can instead return an error, allowing the model to see the failure and perhaps try a different path. A duplicate response can trigger a strategy-changing prompt. A token limit can end the run before another request is made. A maximum-step condition can stop repetition even when the task is unfinished. An uncaught exception can mark the protected state as erroneous and pass failure outward.
These endings should not be flattened into a single success flag. The source provides a mechanism for stopping, not a general outcome verifier. A model may call termination too early. A tool may report an operation completed while the intended result is absent. A maximum-step stop may be operationally clean but substantively incomplete. Even a final piece of text may only summarize what the model believes happened. OpenManus supplies the loop and the boundaries through which evidence returns; the application using it still needs a way to decide whether the requested outcome is real.
The control loop is consequently both the project's main strength and its central responsibility. It gives OpenManus a reusable way to connect a model to many capabilities without rewriting the lifecycle for every new task. It also concentrates meaning in interfaces: schemas must be accurate, tool results must be informative, context must preserve what matters, and stopping conditions must be interpreted correctly. The next architectural question follows directly from this mechanism. If a run depends on state, memory, schemas, execution, and cleanup, which part of OpenManus owns each responsibility—and what remains outside the loop altogether?